diff --git a/Content.Client/Access/UI/AccessOverriderBoundUserInterface.cs b/Content.Client/Access/UI/AccessOverriderBoundUserInterface.cs index c1b63dc4d05..d80c600c03e 100644 --- a/Content.Client/Access/UI/AccessOverriderBoundUserInterface.cs +++ b/Content.Client/Access/UI/AccessOverriderBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Containers.ItemSlots; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; using static Content.Shared.Access.Components.AccessOverriderComponent; @@ -23,6 +24,28 @@ protected override void Open() { base.Open(); + _window = this.CreateWindow<AccessOverriderWindow>(); + RefreshAccess(); + _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; + _window.OnSubmit += SubmitData; + + _window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId)); + } + + public override void OnProtoReload(PrototypesReloadedEventArgs args) + { + base.OnProtoReload(args); + if (!args.WasModified<AccessLevelPrototype>()) + return; + + RefreshAccess(); + + if (State != null) + _window?.UpdateState(_prototypeManager, (AccessOverriderBoundUserInterfaceState) State); + } + + private void RefreshAccess() + { List<ProtoId<AccessLevelPrototype>> accessLevels; if (EntMan.TryGetComponent<AccessOverriderComponent>(Owner, out var accessOverrider)) @@ -30,38 +53,20 @@ protected override void Open() accessLevels = accessOverrider.AccessLevels; accessLevels.Sort(); } - else { accessLevels = new List<ProtoId<AccessLevelPrototype>>(); _accessOverriderSystem.Log.Error($"No AccessOverrider component found for {EntMan.ToPrettyString(Owner)}!"); } - _window = new AccessOverriderWindow(this, _prototypeManager, accessLevels) - { - Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName - }; - - _window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId)); - - _window.OnClose += Close; - _window.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _window?.Dispose(); + _window?.SetAccessLevels(_prototypeManager, accessLevels); } protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); var castState = (AccessOverriderBoundUserInterfaceState) state; - _window?.UpdateState(castState); + _window?.UpdateState(_prototypeManager, castState); } public void SubmitData(List<ProtoId<AccessLevelPrototype>> newAccessList) diff --git a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs index b5c480ff71b..ef6a3bb6717 100644 --- a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs +++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs @@ -13,26 +13,24 @@ namespace Content.Client.Access.UI [GenerateTypedNameReferences] public sealed partial class AccessOverriderWindow : DefaultWindow { - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - private readonly AccessOverriderBoundUserInterface _owner; private readonly Dictionary<string, Button> _accessButtons = new(); - public AccessOverriderWindow(AccessOverriderBoundUserInterface owner, IPrototypeManager prototypeManager, - List<ProtoId<AccessLevelPrototype>> accessLevels) + public event Action<List<ProtoId<AccessLevelPrototype>>>? OnSubmit; + + public AccessOverriderWindow() { RobustXamlLoader.Load(this); - IoCManager.InjectDependencies(this); - var logMill = _logManager.GetSawmill(SharedAccessOverriderSystem.Sawmill); + } - _owner = owner; + public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels) + { + _accessButtons.Clear(); + AccessLevelGrid.DisposeAllChildren(); foreach (var access in accessLevels) { - if (!prototypeManager.TryIndex(access, out var accessLevel)) + if (!protoManager.TryIndex(access, out var accessLevel)) { - logMill.Error($"Unable to find access level for {access}"); continue; } @@ -44,11 +42,16 @@ public AccessOverriderWindow(AccessOverriderBoundUserInterface owner, IPrototype AccessLevelGrid.AddChild(newButton); _accessButtons.Add(accessLevel.ID, newButton); - newButton.OnPressed += _ => SubmitData(); + newButton.OnPressed += _ => + { + OnSubmit?.Invoke( + // Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair + _accessButtons.Where(x => x.Value.Pressed).Select(x => new ProtoId<AccessLevelPrototype>(x.Key)).ToList()); + }; } } - public void UpdateState(AccessOverriderBoundUserInterfaceState state) + public void UpdateState(IPrototypeManager protoManager, AccessOverriderBoundUserInterfaceState state) { PrivilegedIdLabel.Text = state.PrivilegedIdName; PrivilegedIdButton.Text = state.IsPrivilegedIdPresent @@ -70,7 +73,7 @@ public void UpdateState(AccessOverriderBoundUserInterfaceState state) foreach (string tag in state.MissingPrivilegesList) { - var privilege = Loc.GetString(_prototypeManager.Index<AccessLevelPrototype>(tag)?.Name ?? "generic-unknown"); + var privilege = Loc.GetString(protoManager.Index<AccessLevelPrototype>(tag)?.Name ?? "generic-unknown"); missingPrivileges.Add(privilege); } @@ -90,13 +93,5 @@ public void UpdateState(AccessOverriderBoundUserInterfaceState state) button.Disabled = (!state.AllowedModifyAccessList?.Contains<ProtoId<AccessLevelPrototype>>(accessName)) ?? true; } } - - private void SubmitData() => - _owner.SubmitData( - // Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair - _accessButtons.Where(x => x.Value.Pressed) - .Select(x => new ProtoId<AccessLevelPrototype>(x.Key)) - .ToList() - ); } } diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs index c3fac8cb92a..09724f14a2c 100644 --- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs +++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs @@ -1,5 +1,9 @@ using Content.Shared.Access.Systems; +using Content.Shared.StatusIcon; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; +using Robust.Shared.Prototypes; namespace Content.Client.Access.UI { @@ -18,16 +22,18 @@ protected override void Open() { base.Open(); - _window?.Dispose(); - _window = new AgentIDCardWindow(this); - if (State != null) - UpdateState(State); + _window = this.CreateWindow<AgentIDCardWindow>(); - _window.OpenCentered(); - - _window.OnClose += Close; _window.OnNameChanged += OnNameChanged; _window.OnJobChanged += OnJobChanged; + _window.OnJobIconChanged += OnJobIconChanged; + _window.OnNumberChanged += OnNumberChanged; // DeltaV + } + + // DeltaV - Add number change handler + private void OnNumberChanged(uint newNumber) + { + SendMessage(new AgentIDCardNumberChangedMessage(newNumber)); } private void OnNameChanged(string newName) @@ -40,7 +46,7 @@ private void OnJobChanged(string newJob) SendMessage(new AgentIDCardJobChangedMessage(newJob)); } - public void OnJobIconChanged(string newJobIconId) + public void OnJobIconChanged(ProtoId<JobIconPrototype> newJobIconId) { SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId)); } @@ -57,16 +63,8 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.SetCurrentName(cast.CurrentName); _window.SetCurrentJob(cast.CurrentJob); - _window.SetAllowedIcons(cast.Icons, cast.CurrentJobIconId); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _window?.Dispose(); + _window.SetAllowedIcons(cast.CurrentJobIconId); + _window.SetCurrentNumber(cast.CurrentNumber); // DeltaV } } } diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml b/Content.Client/Access/UI/AgentIDCardWindow.xaml index 89de793714d..a2ddd1c417d 100644 --- a/Content.Client/Access/UI/AgentIDCardWindow.xaml +++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml @@ -6,12 +6,13 @@ <LineEdit Name="NameLineEdit" /> <Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" /> <LineEdit Name="JobLineEdit" /> - <BoxContainer Orientation="Horizontal"> - <Label Text="{Loc 'agent-id-card-job-icon-label'}"/> - <Control HorizontalExpand="True" MinSize="50 0"/> - <GridContainer Name="IconGrid" Columns="10"> - <!-- Job icon buttons are generated in the code --> - </GridContainer> - </BoxContainer> + <!-- DeltaV - Add NanoChat number field --> + <Label Name="CurrentNumber" Text="{Loc 'agent-id-card-current-number'}" /> + <LineEdit Name="NumberLineEdit" PlaceHolder="#0000" /> + <!-- DeltaV end --> + <Label Text="{Loc 'agent-id-card-job-icon-label'}"/> + <GridContainer Name="IconGrid" Columns="10"> + <!-- Job icon buttons are generated in the code --> + </GridContainer> </BoxContainer> </DefaultWindow> diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs index 9a38c0c4853..a342013d314 100644 --- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs +++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs @@ -8,6 +8,7 @@ using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; using System.Numerics; +using System.Linq; namespace Content.Client.Access.UI { @@ -17,40 +18,72 @@ public sealed partial class AgentIDCardWindow : DefaultWindow [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystem = default!; private readonly SpriteSystem _spriteSystem; - private readonly AgentIDCardBoundUserInterface _bui; private const int JobIconColumnCount = 10; + private const int MaxNumberLength = 4; // DeltaV - Same as NewChatPopup + public event Action<string>? OnNameChanged; public event Action<string>? OnJobChanged; - public AgentIDCardWindow(AgentIDCardBoundUserInterface bui) + public event Action<uint>? OnNumberChanged; // DeltaV - Add event for number changes + + public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged; + + public AgentIDCardWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>(); - _bui = bui; NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text); NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text); JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text); JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text); + + // DeltaV - Add handlers for number changes + NumberLineEdit.OnTextEntered += OnNumberEntered; + NumberLineEdit.OnFocusExit += OnNumberEntered; + + // DeltaV - Filter to only allow digits + NumberLineEdit.OnTextChanged += args => + { + if (args.Text.Length > MaxNumberLength) + { + NumberLineEdit.Text = args.Text[..MaxNumberLength]; + } + + // Filter to digits only + var newText = string.Concat(args.Text.Where(char.IsDigit)); + if (newText != args.Text) + NumberLineEdit.Text = newText; + }; } - public void SetAllowedIcons(HashSet<string> icons, string currentJobIconId) + // DeltaV - Add number validation and event + private void OnNumberEntered(LineEdit.LineEditEventArgs args) + { + if (uint.TryParse(args.Text, out var number) && number > 0) + OnNumberChanged?.Invoke(number); + } + + // DeltaV - Add setter for current number + public void SetCurrentNumber(uint? number) + { + NumberLineEdit.Text = number?.ToString("D4") ?? ""; + } + + public void SetAllowedIcons(string currentJobIconId) { IconGrid.DisposeAllChildren(); - var jobIconGroup = new ButtonGroup(); + var jobIconButtonGroup = new ButtonGroup(); var i = 0; - foreach (var jobIconId in icons) + var icons = _prototypeManager.EnumeratePrototypes<JobIconPrototype>().Where(icon => icon.AllowSelection).ToList(); + icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture)); + foreach (var jobIcon in icons) { - if (!_prototypeManager.TryIndex<StatusIconPrototype>(jobIconId, out var jobIcon)) - { - continue; - } - String styleBase = StyleBase.ButtonOpenBoth; var modulo = i % JobIconColumnCount; if (modulo == 0) @@ -64,12 +97,13 @@ public void SetAllowedIcons(HashSet<string> icons, string currentJobIconId) Access = AccessLevel.Public, StyleClasses = { styleBase }, MaxSize = new Vector2(42, 28), - Group = jobIconGroup, - Pressed = i == 0, + Group = jobIconButtonGroup, + Pressed = currentJobIconId == jobIcon.ID, + ToolTip = jobIcon.LocalizedJobName }; // Generate buttons textures - TextureRect jobIconTexture = new TextureRect + var jobIconTexture = new TextureRect { Texture = _spriteSystem.Frame0(jobIcon.Icon), TextureScale = new Vector2(2.5f, 2.5f), @@ -77,12 +111,9 @@ public void SetAllowedIcons(HashSet<string> icons, string currentJobIconId) }; jobIconButton.AddChild(jobIconTexture); - jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIcon.ID); + jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(jobIcon.ID); IconGrid.AddChild(jobIconButton); - if (jobIconId.Equals(currentJobIconId)) - jobIconButton.Pressed = true; - i++; } } diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 0bc65eb9358..48c53869e6c 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -49,6 +49,7 @@ public override void Initialize() SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState); SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState); SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState); + SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState); } private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args) @@ -78,6 +79,18 @@ private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent BaseHandleState<WorldTargetActionComponent>(uid, component, state); } + private void OnEntityWorldTargetHandleState(EntityUid uid, + EntityWorldTargetActionComponent component, + ref ComponentHandleState args) + { + if (args.Current is not EntityWorldTargetActionComponentState state) + return; + + component.Whitelist = state.Whitelist; + component.CanTargetSelf = state.CanTargetSelf; + BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state); + } + private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent { // TODO ACTIONS use auto comp states @@ -248,12 +261,6 @@ public void TriggerAction(EntityUid actionId, BaseActionComponent action) if (action.ClientExclusive) { - if (instantAction.Event != null) - { - instantAction.Event.Performer = user; - instantAction.Event.Action = actionId; - } - PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime); } else diff --git a/Content.Client/Administration/Systems/AdminFrozenSystem.cs b/Content.Client/Administration/Systems/AdminFrozenSystem.cs new file mode 100644 index 00000000000..885585f985c --- /dev/null +++ b/Content.Client/Administration/Systems/AdminFrozenSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Administration; + +namespace Content.Client.Administration.Systems; + +public sealed class AdminFrozenSystem : SharedAdminFrozenSystem +{ +} diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml b/Content.Client/Administration/UI/AdminMenuWindow.xaml index 311d67b826c..d3d3df02d93 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml @@ -6,7 +6,8 @@ xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs" xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab" xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab" - xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"> + xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab" + xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"> <TabContainer Name="MasterTabContainer"> <adminTab:AdminTab /> <adminbusTab:AdminbusTab /> @@ -14,6 +15,7 @@ <tabs:RoundTab /> <tabs:ServerTab /> <panic:PanicBunkerTab Name="PanicBunkerControl" Access="Public" /> + <baby:BabyJailTab Name="BabyJailControl" Access="Public" /> <playerTab:PlayerTab Name="PlayerTabControl" Access="Public" /> <objectsTab:ObjectsTab Name="ObjectsTabControl" Access="Public" /> </TabContainer> diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs index c3ea67a3edb..d5c43e2a500 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs @@ -3,34 +3,57 @@ using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -namespace Content.Client.Administration.UI +namespace Content.Client.Administration.UI; + +[GenerateTypedNameReferences] +public sealed partial class AdminMenuWindow : DefaultWindow { - [GenerateTypedNameReferences] - public sealed partial class AdminMenuWindow : DefaultWindow + public event Action? OnDisposed; + + public AdminMenuWindow() + { + MinSize = new Vector2(650, 250); + Title = Loc.GetString("admin-menu-title"); + RobustXamlLoader.Load(this); + MasterTabContainer.SetTabTitle((int) TabIndex.Admin, Loc.GetString("admin-menu-admin-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.Adminbus, Loc.GetString("admin-menu-adminbus-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.Atmos, Loc.GetString("admin-menu-atmos-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.Round, Loc.GetString("admin-menu-round-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.Server, Loc.GetString("admin-menu-server-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.PanicBunker, Loc.GetString("admin-menu-panic-bunker-tab")); + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + MasterTabContainer.SetTabTitle((int) TabIndex.BabyJail, Loc.GetString("admin-menu-baby-jail-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.Players, Loc.GetString("admin-menu-players-tab")); + MasterTabContainer.SetTabTitle((int) TabIndex.Objects, Loc.GetString("admin-menu-objects-tab")); + MasterTabContainer.OnTabChanged += OnTabChanged; + } + + private void OnTabChanged(int tabIndex) { - public event Action? OnDisposed; + var tabEnum = (TabIndex)tabIndex; + if (tabEnum == TabIndex.Objects) + ObjectsTabControl.RefreshObjectList(); + } - public AdminMenuWindow() - { - MinSize = new Vector2(650, 250); - Title = Loc.GetString("admin-menu-title"); - RobustXamlLoader.Load(this); - MasterTabContainer.SetTabTitle(0, Loc.GetString("admin-menu-admin-tab")); - MasterTabContainer.SetTabTitle(1, Loc.GetString("admin-menu-adminbus-tab")); - MasterTabContainer.SetTabTitle(2, Loc.GetString("admin-menu-atmos-tab")); - MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab")); - MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab")); - MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab")); - MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-players-tab")); - MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-objects-tab")); - } + protected override void Dispose(bool disposing) + { + OnDisposed?.Invoke(); + base.Dispose(disposing); + OnDisposed = null; + } - protected override void Dispose(bool disposing) - { - OnDisposed?.Invoke(); - base.Dispose(disposing); - OnDisposed = null; - } + private enum TabIndex + { + Admin = 0, + Adminbus, + Atmos, + Round, + Server, + PanicBunker, + BabyJail, + Players, + Objects, } } - diff --git a/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs b/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs index c95f8f204dd..fad55f96273 100644 --- a/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs +++ b/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs @@ -1,4 +1,5 @@ using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs index dc263d6055c..a034f503b4b 100644 --- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs +++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs @@ -22,11 +22,11 @@ namespace Content.Client.Administration.UI.BanPanel; [GenerateTypedNameReferences] public sealed partial class BanPanel : DefaultWindow { - public event Action<string?, (IPAddress, int)?, bool, byte[]?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted; + public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted; public event Action<string>? PlayerChanged; private string? PlayerUsername { get; set; } private (IPAddress, int)? IpAddress { get; set; } - private byte[]? Hwid { get; set; } + private ImmutableTypedHwid? Hwid { get; set; } private double TimeEntered { get; set; } private uint Multiplier { get; set; } private bool HasBanFlag { get; set; } @@ -371,9 +371,8 @@ private void OnIpChanged() private void OnHwidChanged() { var hwidString = HwidLine.Text; - var length = 3 * (hwidString.Length / 4) - hwidString.TakeLast(2).Count(c => c == '='); - Hwid = new byte[length]; - if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !Convert.TryFromBase64String(hwidString, Hwid, out _)) + ImmutableTypedHwid? hwid = null; + if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !ImmutableTypedHwid.TryParse(hwidString, out hwid)) { ErrorLevel |= ErrorLevelEnum.Hwid; HwidLine.ModulateSelfOverride = Color.Red; @@ -390,7 +389,7 @@ private void OnHwidChanged() Hwid = null; return; } - Hwid = Convert.FromHexString(hwidString); + Hwid = hwid; } private void OnTypeChanged() diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs index af977f763c6..8becf8afe37 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs @@ -11,9 +11,8 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Network; -using Robust.Shared.Utility; -using Robust.Shared.Timing; using Robust.Shared.Configuration; +using Robust.Shared.Utility; namespace Content.Client.Administration.UI.Bwoink { @@ -88,26 +87,51 @@ public BwoinkControl() var ach = AHelpHelper.EnsurePanel(a.SessionId); var bch = AHelpHelper.EnsurePanel(b.SessionId); - // First, sort by unread. Any chat with unread messages appears first. We just sort based on unread - // status, not number of unread messages, so that more recent unread messages take priority. + // Pinned players first + if (a.IsPinned != b.IsPinned) + return a.IsPinned ? -1 : 1; + + // First, sort by unread. Any chat with unread messages appears first. var aUnread = ach.Unread > 0; var bUnread = bch.Unread > 0; if (aUnread != bUnread) return aUnread ? -1 : 1; + // Sort by recent messages during the current round. + var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue; + var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue; + if (aRecent != bRecent) + return aRecent ? -1 : 1; + // Next, sort by connection status. Any disconnected players are grouped towards the end. if (a.Connected != b.Connected) return a.Connected ? -1 : 1; - // Next, group by whether or not the players have participated in this round. - // The ahelp window shows all players that have connected since server restart, this groups them all towards the bottom. - if (a.ActiveThisRound != b.ActiveThisRound) - return a.ActiveThisRound ? -1 : 1; + // Sort connected players by New Player status, then by Antag status + if (a.Connected && b.Connected) + { + var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)); + var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)); + + if (aNewPlayer != bNewPlayer) + return aNewPlayer ? -1 : 1; + + if (a.Antag != b.Antag) + return a.Antag ? -1 : 1; + } + + // Sort disconnected players by participation in the round + if (!a.Connected && !b.Connected) + { + if (a.ActiveThisRound != b.ActiveThisRound) + return a.ActiveThisRound ? -1 : 1; + } // Finally, sort by the most recent message. return bch.LastMessage.CompareTo(ach.LastMessage); }; + Bans.OnPressed += _ => { if (_currentPlayer is not null) @@ -253,7 +277,20 @@ private void SwitchToChannel(NetUserId? ch) public void PopulateList() { + // Maintain existing pin statuses + var pinnedPlayers = ChannelSelector.PlayerInfo.Where(p => p.IsPinned).ToDictionary(p => p.SessionId); + ChannelSelector.PopulateList(); + + // Restore pin statuses + foreach (var player in ChannelSelector.PlayerInfo) + { + if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer)) + { + player.IsPinned = pinnedPlayer.IsPinned; + } + } + UpdateButtons(); } } diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index b09cd727ef8..c7fbf6c2dc0 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -21,11 +21,11 @@ public sealed partial class PlayerListControl : BoxContainer private readonly IEntityManager _entManager; private readonly IUserInterfaceManager _uiManager; - + private PlayerInfo? _selectedPlayer; private List<PlayerInfo> _playerList = new(); - private readonly List<PlayerInfo> _sortedPlayerList = new(); + private List<PlayerInfo> _sortedPlayerList = new(); public Comparison<PlayerInfo>? Comparison; public Func<PlayerInfo, string, string>? OverrideText; @@ -110,19 +110,30 @@ private void FilterList() if (Comparison != null) _sortedPlayerList.Sort((a, b) => Comparison(a, b)); - // Ensure pinned players are always at the top - _sortedPlayerList.Sort((a, b) => a.IsPinned != b.IsPinned && a.IsPinned ? -1 : 1); - PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList()); if (_selectedPlayer != null) PlayerListContainer.Select(new PlayerListData(_selectedPlayer)); } + public void PopulateList(IReadOnlyList<PlayerInfo>? players = null) { + // Maintain existing pin statuses + var pinnedPlayers = _playerList.Where(p => p.IsPinned).ToDictionary(p => p.SessionId); + players ??= _adminSystem.PlayerList; _playerList = players.ToList(); + + // Restore pin statuses + foreach (var player in _playerList) + { + if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer)) + { + player.IsPinned = pinnedPlayer.IsPinned; + } + } + if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer)) _selectedPlayer = null; diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml new file mode 100644 index 00000000000..8feec273b47 --- /dev/null +++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml @@ -0,0 +1,36 @@ +<ui:FancyWindow + xmlns="https://spacestation14.io" + xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" + xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls" + Title="{Loc ban-panel-title}" MinSize="300 300"> + <BoxContainer Orientation="Vertical"> + <BoxContainer Orientation="Horizontal"> + <Label Name="PlayerName"/> + <Button Name="UsernameCopyButton" Text="{Loc player-panel-copy-username}"/> + </BoxContainer> + <BoxContainer Orientation="Horizontal"> + <Label Name="Whitelisted"/> + <controls:ConfirmButton Name="WhitelistToggle" Text="{Loc 'player-panel-false'}" Visible="False"></controls:ConfirmButton> + </BoxContainer> + <Label Name="Playtime"/> + <Label Name="Notes"/> + <Label Name="Bans"/> + <Label Name="RoleBans"/> + <Label Name="SharedConnections"/> + + <BoxContainer Align="Center"> + <GridContainer Rows="5"> + <Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/> + <Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/> + <Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/> + <controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/> + <controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/> + <Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/> + <Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/> + <Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/> + <Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/> + <controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/> + </GridContainer> + </BoxContainer> + </BoxContainer> +</ui:FancyWindow> diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs new file mode 100644 index 00000000000..824d9eb6c77 --- /dev/null +++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs @@ -0,0 +1,132 @@ +using Content.Client.Administration.Managers; +using Content.Client.UserInterface.Controls; +using Content.Shared.Administration; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Network; +using Robust.Shared.Utility; + +namespace Content.Client.Administration.UI.PlayerPanel; + +[GenerateTypedNameReferences] +public sealed partial class PlayerPanel : FancyWindow +{ + private readonly IClientAdminManager _adminManager; + + public event Action<string>? OnUsernameCopy; + public event Action<NetUserId?>? OnOpenNotes; + public event Action<NetUserId?>? OnOpenBans; + public event Action<NetUserId?>? OnAhelp; + public event Action<string?>? OnKick; + public event Action<NetUserId?>? OnOpenBanPanel; + public event Action<NetUserId?, bool>? OnWhitelistToggle; + public event Action? OnFreezeAndMuteToggle; + public event Action? OnFreeze; + public event Action? OnLogs; + public event Action? OnDelete; + public event Action? OnRejuvenate; + + public NetUserId? TargetPlayer; + public string? TargetUsername; + private bool _isWhitelisted; + + public PlayerPanel(IClientAdminManager adminManager) + { + RobustXamlLoader.Load(this); + _adminManager = adminManager; + + UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? ""); + BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer); + KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername); + NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer); + ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer); + AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer); + WhitelistToggle.OnPressed += _ => + { + OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted); + SetWhitelisted(!_isWhitelisted); + }; + FreezeButton.OnPressed += _ => OnFreeze?.Invoke(); + FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke(); + LogsButton.OnPressed += _ => OnLogs?.Invoke(); + DeleteButton.OnPressed += _ => OnDelete?.Invoke(); + RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke(); + } + + public void SetUsername(string player) + { + Title = Loc.GetString("player-panel-title", ("player", player)); + PlayerName.Text = Loc.GetString("player-panel-username", ("player", player)); + } + + public void SetWhitelisted(bool? whitelisted) + { + if (whitelisted == null) + { + Whitelisted.Text = null; + WhitelistToggle.Visible = false; + } + else + { + Whitelisted.Text = Loc.GetString("player-panel-whitelisted"); + WhitelistToggle.Text = whitelisted.Value.ToString(); + WhitelistToggle.Visible = true; + _isWhitelisted = whitelisted.Value; + } + } + + public void SetBans(int? totalBans, int? totalRoleBans) + { + // If one value exists then so should the other. + DebugTools.Assert(totalBans.HasValue && totalRoleBans.HasValue || totalBans == null && totalRoleBans == null); + + Bans.Text = totalBans != null ? Loc.GetString("player-panel-bans", ("totalBans", totalBans)) : null; + + RoleBans.Text = totalRoleBans != null ? Loc.GetString("player-panel-rolebans", ("totalRoleBans", totalRoleBans)) : null; + } + + public void SetNotes(int? totalNotes) + { + Notes.Text = totalNotes != null ? Loc.GetString("player-panel-notes", ("totalNotes", totalNotes)) : null; + } + + public void SetSharedConnections(int sharedConnections) + { + SharedConnections.Text = Loc.GetString("player-panel-shared-connections", ("sharedConnections", sharedConnections)); + } + + public void SetPlaytime(TimeSpan playtime) + { + Playtime.Text = Loc.GetString("player-panel-playtime", + ("days", playtime.Days), + ("hours", playtime.Hours % 24), + ("minutes", playtime.Minutes % (24 * 60))); + } + + public void SetFrozen(bool canFreeze, bool frozen) + { + FreezeAndMuteToggleButton.Disabled = !canFreeze; + FreezeButton.Disabled = !canFreeze || frozen; + + FreezeAndMuteToggleButton.Text = Loc.GetString(!frozen ? "player-panel-freeze-and-mute" : "player-panel-unfreeze"); + } + + public void SetAhelp(bool canAhelp) + { + AhelpButton.Disabled = !canAhelp; + } + + public void SetButtons() + { + BanButton.Disabled = !_adminManager.CanCommand("banpanel"); + KickButton.Disabled = !_adminManager.CanCommand("kick"); + NotesButton.Disabled = !_adminManager.CanCommand("adminnotes"); + ShowBansButton.Disabled = !_adminManager.CanCommand("banlist"); + WhitelistToggle.Disabled = + !(_adminManager.CanCommand("addwhitelist") && _adminManager.CanCommand("removewhitelist")); + LogsButton.Disabled = !_adminManager.CanCommand("adminlogs"); + RejuvenateButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug); + DeleteButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug); + } +} diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs new file mode 100644 index 00000000000..87ce7560463 --- /dev/null +++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs @@ -0,0 +1,72 @@ +using Content.Client.Administration.Managers; +using Content.Client.Eui; +using Content.Shared.Administration; +using Content.Shared.Eui; +using JetBrains.Annotations; +using Robust.Client.Console; +using Robust.Client.UserInterface; + +namespace Content.Client.Administration.UI.PlayerPanel; + +[UsedImplicitly] +public sealed class PlayerPanelEui : BaseEui +{ + [Dependency] private readonly IClientConsoleHost _console = default!; + [Dependency] private readonly IClientAdminManager _admin = default!; + [Dependency] private readonly IClipboardManager _clipboard = default!; + + private PlayerPanel PlayerPanel { get; } + + public PlayerPanelEui() + { + PlayerPanel = new PlayerPanel(_admin); + + PlayerPanel.OnUsernameCopy += username => _clipboard.SetText(username); + PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\""); + // Kick command does not support GUIDs + PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\""); + PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\""); + PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\""); + PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\""); + PlayerPanel.OnWhitelistToggle += (id, whitelisted) => + { + _console.ExecuteCommand(whitelisted ? $"whitelistremove \"{id}\"" : $"whitelistadd \"{id}\""); + }; + + PlayerPanel.OnFreezeAndMuteToggle += () => SendMessage(new PlayerPanelFreezeMessage(true)); + PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage()); + PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage()); + PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage()); + PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage()); + + PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage()); + } + + public override void Opened() + { + PlayerPanel.OpenCentered(); + } + + public override void Closed() + { + PlayerPanel.Close(); + } + + public override void HandleState(EuiStateBase state) + { + if (state is not PlayerPanelEuiState s) + return; + + PlayerPanel.TargetPlayer = s.Guid; + PlayerPanel.TargetUsername = s.Username; + PlayerPanel.SetUsername(s.Username); + PlayerPanel.SetPlaytime(s.Playtime); + PlayerPanel.SetBans(s.TotalBans, s.TotalRoleBans); + PlayerPanel.SetNotes(s.TotalNotes); + PlayerPanel.SetWhitelisted(s.Whitelisted); + PlayerPanel.SetSharedConnections(s.SharedConnections); + PlayerPanel.SetFrozen(s.CanFreeze, s.Frozen); + PlayerPanel.SetAhelp(s.CanAhelp); + PlayerPanel.SetButtons(); + } +} diff --git a/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs b/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs index a2faf208d92..7cb32b43df5 100644 --- a/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs +++ b/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Client.UserInterface.Controls; using Content.Shared.Roles; using Robust.Client.AutoGenerated; using Robust.Client.Console; diff --git a/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs b/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs index 5f187cad794..b0d8a946ec5 100644 --- a/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs +++ b/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.Console; +using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; @@ -22,7 +23,7 @@ public sealed partial class SpawnExplosionWindow : DefaultWindow [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IEntityManager _entMan = default!; - + private readonly SharedTransformSystem _transform = default!; private readonly SpawnExplosionEui _eui; private List<MapId> _mapData = new(); @@ -37,6 +38,7 @@ public SpawnExplosionWindow(SpawnExplosionEui eui) { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _transform = _entMan.System<TransformSystem>(); _eui = eui; ExplosionOption.OnItemSelected += ExplosionSelected; @@ -104,7 +106,7 @@ private void SetLocation() _pausePreview = true; MapOptions.Select(_mapData.IndexOf(transform.MapID)); - (MapX.Value, MapY.Value) = transform.MapPosition.Position; + (MapX.Value, MapY.Value) = _transform.GetMapCoordinates(_playerManager.LocalEntity!.Value, xform: transform).Position; _pausePreview = false; UpdatePreview(); diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml new file mode 100644 index 00000000000..b8034faf52a --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml @@ -0,0 +1,6 @@ +<controls:BabyJailStatusWindow + xmlns="https://spacestation14.io" + xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab" + Title="{Loc admin-ui-baby-jail-window-title}"> + <RichTextLabel Name="MessageLabel" Access="Public" /> +</controls:BabyJailStatusWindow> diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml.cs new file mode 100644 index 00000000000..9e1d53818f2 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml.cs @@ -0,0 +1,21 @@ +using Content.Client.Message; +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Administration.UI.Tabs.BabyJailTab; + +/* + * TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + +[GenerateTypedNameReferences] +public sealed partial class BabyJailStatusWindow : FancyWindow +{ + public BabyJailStatusWindow() + { + RobustXamlLoader.Load(this); + MessageLabel.SetMarkup(Loc.GetString("admin-ui-baby-jail-is-enabled")); + } +} diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml new file mode 100644 index 00000000000..bee94e59c11 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml @@ -0,0 +1,26 @@ +<controls:BabyJailTab + xmlns="https://spacestation14.io" + xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab" + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" + Margin="4"> + <BoxContainer Orientation="Vertical"> + <cc:CommandButton Name="EnabledButton" Command="babyjail" ToggleMode="True" + Text="{Loc admin-ui-baby-jail-disabled}" + ToolTip="{Loc admin-ui-baby-jail-tooltip}" /> + <cc:CommandButton Name="ShowReasonButton" Command="babyjail_show_reason" + ToggleMode="True" Text="{Loc admin-ui-baby-jail-show-reason}" + ToolTip="{Loc admin-ui-baby-jail-show-reason-tooltip}" /> + <BoxContainer Orientation="Vertical" Margin="0 10 0 0"> + <BoxContainer Orientation="Horizontal" Margin="2"> + <Label Text="{Loc admin-ui-baby-jail-max-account-age}" MinWidth="175" /> + <LineEdit Name="MaxAccountAge" MinWidth="50" Margin="0 0 5 0" /> + <Label Text="{Loc generic-hours}" /> + </BoxContainer> + <BoxContainer Orientation="Horizontal" Margin="2"> + <Label Text="{Loc admin-ui-baby-jail-max-overall-hours}" MinWidth="175" /> + <LineEdit Name="MaxOverallHours" MinWidth="50" Margin="0 0 5 0" /> + <Label Text="{Loc generic-hours}" /> + </BoxContainer> + </BoxContainer> + </BoxContainer> +</controls:BabyJailTab> diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml.cs b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml.cs new file mode 100644 index 00000000000..3ac50c4ff10 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml.cs @@ -0,0 +1,75 @@ +using Content.Shared.Administration.Events; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Console; + +/* + * TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + +namespace Content.Client.Administration.UI.Tabs.BabyJailTab; + +[GenerateTypedNameReferences] +public sealed partial class BabyJailTab : Control +{ + [Dependency] private readonly IConsoleHost _console = default!; + + private string _maxAccountAge; + private string _maxOverallHours; + + public BabyJailTab() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text); + MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text); + _maxAccountAge = MaxAccountAge.Text; + + MaxOverallHours.OnTextEntered += args => SendMaxOverallHours(args.Text); + MaxOverallHours.OnFocusExit += args => SendMaxOverallHours(args.Text); + _maxOverallHours = MaxOverallHours.Text; + } + + private void SendMaxAccountAge(string text) + { + if (string.IsNullOrWhiteSpace(text) || + text == _maxAccountAge || + !int.TryParse(text, out var hours)) + { + return; + } + + _console.ExecuteCommand($"babyjail_max_account_age {hours}"); + } + + private void SendMaxOverallHours(string text) + { + if (string.IsNullOrWhiteSpace(text) || + text == _maxOverallHours || + !int.TryParse(text, out var hours)) + { + return; + } + + _console.ExecuteCommand($"babyjail_max_overall_hours {hours}"); + } + + public void UpdateStatus(BabyJailStatus status) + { + EnabledButton.Pressed = status.Enabled; + EnabledButton.Text = Loc.GetString(status.Enabled + ? "admin-ui-baby-jail-enabled" + : "admin-ui-baby-jail-disabled" + ); + EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null; + ShowReasonButton.Pressed = status.ShowReason; + + MaxAccountAge.Text = status.MaxAccountAgeHours.ToString(); + _maxAccountAge = MaxAccountAge.Text; + + MaxOverallHours.Text = status.MaxOverallHours.ToString(); + _maxOverallHours = MaxOverallHours.Text; + } +} diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml index fb68e6c7908..f4298bbc00f 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml @@ -1,15 +1,20 @@ <Control xmlns="https://spacestation14.io" - xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"> + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:ot="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab" + xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"> <BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Horizontal"> - <Label HorizontalExpand="True" SizeFlagsStretchRatio="0.50" - Text="{Loc Object type:}" /> - <OptionButton Name="ObjectTypeOptions" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"/> + <Label Text="{Loc object-tab-object-type}" /> + <OptionButton Name="ObjectTypeOptions" HorizontalAlignment="Left" /> + <LineEdit Name="SearchLineEdit" PlaceHolder="{Loc object-tab-object-search}" HorizontalExpand="True" + SizeFlagsStretchRatio="1" /> + <Button Name="RefreshListButton" Text="{Loc object-tab-refresh-button}" ToggleMode="False" /> + </BoxContainer> + <cc:HSeparator /> + <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True"> + <ot:ObjectsTabHeader Name="ListHeader" /> + <cc:HSeparator /> + <co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True" /> </BoxContainer> - <cc:HSeparator/> - <ScrollContainer HorizontalExpand="True" VerticalExpand="True"> - <BoxContainer Orientation="Vertical" Name="ObjectList"> - </BoxContainer> - </ScrollContainer> </BoxContainer> </Control> diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs index a5c30084365..78eefa34628 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs @@ -1,6 +1,9 @@ using Content.Client.Station; +using Content.Client.UserInterface.Controls; using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map.Components; using Robust.Shared.Timing; @@ -10,20 +13,17 @@ namespace Content.Client.Administration.UI.Tabs.ObjectsTab; [GenerateTypedNameReferences] public sealed partial class ObjectsTab : Control { - [Dependency] private readonly EntityManager _entityManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IGameTiming _timing = default!; - private readonly List<ObjectsTabEntry> _objects = new(); - private List<ObjectsTabSelection> _selections = new(); + private readonly Color _altColor = Color.FromHex("#292B38"); + private readonly Color _defaultColor = Color.FromHex("#2F2F3B"); - public event Action<ObjectsTabEntry, GUIBoundKeyEventArgs>? OnEntryKeyBindDown; + private bool _ascending; + private ObjectsTabHeader.Header _headerClicked = ObjectsTabHeader.Header.ObjectName; - // Listen I could either have like 4 different event subscribers (for map / grid / station changes) and manage their lifetimes in AdminUIController - // OR - // I can do this. - private TimeSpan _updateFrequency = TimeSpan.FromSeconds(2); - - private TimeSpan _nextUpdate = TimeSpan.FromSeconds(2); + private readonly List<ObjectsTabSelection> _selections = []; + public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown; public ObjectsTab() { @@ -36,16 +36,25 @@ public ObjectsTab() RefreshObjectList(_selections[ev.Id]); }; - foreach (var type in Enum.GetValues(typeof(ObjectsTabSelection))) + foreach (var type in Enum.GetValues<ObjectsTabSelection>()) { - _selections.Add((ObjectsTabSelection)type!); - ObjectTypeOptions.AddItem(Enum.GetName((ObjectsTabSelection)type)!); + _selections.Add(type); + ObjectTypeOptions.AddItem(GetLocalizedEnumValue(type)); } - RefreshObjectList(); + ListHeader.OnHeaderClicked += HeaderClicked; + SearchList.SearchBar = SearchLineEdit; + SearchList.GenerateItem += GenerateButton; + SearchList.DataFilterCondition += DataFilterCondition; + SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data); + RefreshListButton.OnPressed += _ => RefreshObjectList(); + + var defaultSelection = ObjectsTabSelection.Grids; + ObjectTypeOptions.SelectId((int) defaultSelection); + RefreshObjectList(defaultSelection); } - private void RefreshObjectList() + public void RefreshObjectList() { RefreshObjectList(_selections[ObjectTypeOptions.SelectedId]); } @@ -75,41 +84,96 @@ private void RefreshObjectList(ObjectsTabSelection selection) { entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid))); } + break; } default: throw new ArgumentOutOfRangeException(nameof(selection), selection, null); } - foreach (var control in _objects) + entities.Sort((a, b) => { - ObjectList.RemoveChild(control); + var valueA = GetComparableValue(a, _headerClicked); + var valueB = GetComparableValue(b, _headerClicked); + return _ascending + ? Comparer<object>.Default.Compare(valueA, valueB) + : Comparer<object>.Default.Compare(valueB, valueA); + }); + + var listData = new List<ObjectsListData>(); + for (var index = 0; index < entities.Count; index++) + { + var info = entities[index]; + listData.Add(new ObjectsListData(info, + $"{info.Name} {info.Entity}", + index % 2 == 0 ? _altColor : _defaultColor)); } - _objects.Clear(); + SearchList.PopulateList(listData); + } - foreach (var (name, nent) in entities) - { - var ctrl = new ObjectsTabEntry(name, nent); - _objects.Add(ctrl); - ObjectList.AddChild(ctrl); - ctrl.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(ctrl, args); - } + private void GenerateButton(ListData data, ListContainerButton button) + { + if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor }) + return; + + var entry = new ObjectsTabEntry(info.Name, + info.Entity, + new StyleBoxFlat { BackgroundColor = backgroundColor }); + button.ToolTip = $"{info.Name}, {info.Entity}"; + + button.AddChild(entry); } - protected override void FrameUpdate(FrameEventArgs args) + private bool DataFilterCondition(string filter, ListData listData) { - base.FrameUpdate(args); + if (listData is not ObjectsListData { FilteringString: var filteringString }) + return false; - if (_timing.CurTime < _nextUpdate) - return; + // If the filter is empty, do not filter out any entries + if (string.IsNullOrEmpty(filter)) + return true; + + return filteringString.Contains(filter, StringComparison.CurrentCultureIgnoreCase); + } - // I do not care for precision. - _nextUpdate = _timing.CurTime + _updateFrequency; + private object GetComparableValue((string Name, NetEntity Entity) entity, ObjectsTabHeader.Header header) + { + return header switch + { + ObjectsTabHeader.Header.ObjectName => entity.Name, + ObjectsTabHeader.Header.EntityID => entity.Entity.ToString(), + _ => entity.Name, + }; + } + private void HeaderClicked(ObjectsTabHeader.Header header) + { + if (_headerClicked == header) + { + _ascending = !_ascending; + } + else + { + _headerClicked = header; + _ascending = true; + } + + ListHeader.UpdateHeaderSymbols(_headerClicked, _ascending); RefreshObjectList(); } + private string GetLocalizedEnumValue(ObjectsTabSelection selection) + { + return selection switch + { + ObjectsTabSelection.Grids => Loc.GetString("object-tab-object-type-grids"), + ObjectsTabSelection.Maps => Loc.GetString("object-tab-object-type-maps"), + ObjectsTabSelection.Stations => Loc.GetString("object-tab-object-type-stations"), + _ => throw new ArgumentOutOfRangeException(nameof(selection), selection, null), + }; + } + private enum ObjectsTabSelection { Grids, @@ -118,3 +182,5 @@ private enum ObjectsTabSelection } } +public record ObjectsListData((string Name, NetEntity Entity) Info, string FilteringString, Color BackgroundColor) + : ListData; diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml index 0f6975e3656..83c4cc5697f 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml @@ -1,6 +1,6 @@ -<ContainerButton xmlns="https://spacestation14.io" - xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"> - <PanelContainer Name="BackgroundColorPanel"/> +<PanelContainer xmlns="https://spacestation14.io" + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls" + Name="BackgroundColorPanel"> <BoxContainer Orientation="Horizontal" HorizontalExpand="True" SeparationOverride="4"> @@ -14,4 +14,4 @@ HorizontalExpand="True" ClipText="True"/> </BoxContainer> -</ContainerButton> +</PanelContainer> diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs index c9b2cd8b572..aab06c6ccd0 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs @@ -1,19 +1,21 @@ using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; namespace Content.Client.Administration.UI.Tabs.ObjectsTab; [GenerateTypedNameReferences] -public sealed partial class ObjectsTabEntry : ContainerButton +public sealed partial class ObjectsTabEntry : PanelContainer { public NetEntity AssocEntity; - public ObjectsTabEntry(string name, NetEntity nent) + public ObjectsTabEntry(string name, NetEntity nent, StyleBox styleBox) { RobustXamlLoader.Load(this); AssocEntity = nent; EIDLabel.Text = nent.ToString(); NameLabel.Text = name; + BackgroundColorPanel.PanelOverride = styleBox; } } diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml new file mode 100644 index 00000000000..71a1f5c7bc6 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml @@ -0,0 +1,21 @@ +<Control xmlns="https://spacestation14.io" + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"> + <PanelContainer Name="BackgroundColorPanel" Access="Public"/> + <BoxContainer Orientation="Horizontal" + HorizontalExpand="True" + SeparationOverride="4"> + <Label Name="ObjectNameLabel" + SizeFlagsStretchRatio="3" + HorizontalExpand="True" + ClipText="True" + Text="{Loc object-tab-object-name}" + MouseFilter="Pass"/> + <cc:VSeparator/> + <Label Name="EntityIDLabel" + SizeFlagsStretchRatio="3" + HorizontalExpand="True" + ClipText="True" + Text="{Loc object-tab-entity-id}" + MouseFilter="Pass"/> + </BoxContainer> +</Control> diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs new file mode 100644 index 00000000000..3a91b5b9483 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs @@ -0,0 +1,86 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Input; + +namespace Content.Client.Administration.UI.Tabs.ObjectsTab +{ + [GenerateTypedNameReferences] + public sealed partial class ObjectsTabHeader : Control + { + public event Action<Header>? OnHeaderClicked; + + private const string ArrowUp = "↑"; + private const string ArrowDown = "↓"; + + public ObjectsTabHeader() + { + RobustXamlLoader.Load(this); + + ObjectNameLabel.OnKeyBindDown += ObjectNameClicked; + EntityIDLabel.OnKeyBindDown += EntityIDClicked; + } + + public Label GetHeader(Header header) + { + return header switch + { + Header.ObjectName => ObjectNameLabel, + Header.EntityID => EntityIDLabel, + _ => throw new ArgumentOutOfRangeException(nameof(header), header, null) + }; + } + + public void ResetHeaderText() + { + ObjectNameLabel.Text = Loc.GetString("object-tab-object-name"); + EntityIDLabel.Text = Loc.GetString("object-tab-entity-id"); + } + + public void UpdateHeaderSymbols(Header headerClicked, bool ascending) + { + ResetHeaderText(); + var arrow = ascending ? ArrowUp : ArrowDown; + GetHeader(headerClicked).Text += $" {arrow}"; + } + + private void HeaderClicked(GUIBoundKeyEventArgs args, Header header) + { + if (args.Function != EngineKeyFunctions.UIClick) + { + return; + } + + OnHeaderClicked?.Invoke(header); + args.Handle(); + } + + private void ObjectNameClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.ObjectName); + } + + private void EntityIDClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.EntityID); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + ObjectNameLabel.OnKeyBindDown -= ObjectNameClicked; + EntityIDLabel.OnKeyBindDown -= EntityIDClicked; + } + } + + public enum Header + { + ObjectName, + EntityID + } + } +} diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs index a8bfaddecf4..826945e7cc0 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs @@ -53,6 +53,7 @@ public PlayerTab() SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data); RefreshPlayerList(_adminSystem.PlayerList); + } #region Antag Overlay @@ -110,7 +111,9 @@ private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players) _players = players; PlayerCount.Text = $"Players: {_playerMan.PlayerCount}"; - var sortedPlayers = new List<PlayerInfo>(players); + var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList(); + + var sortedPlayers = new List<PlayerInfo>(filteredPlayers); sortedPlayers.Sort(Compare); UpdateHeaderSymbols(); diff --git a/Content.Client/Ame/UI/AmeControllerBoundUserInterface.cs b/Content.Client/Ame/UI/AmeControllerBoundUserInterface.cs index e84cf5d34de..3d65f751899 100644 --- a/Content.Client/Ame/UI/AmeControllerBoundUserInterface.cs +++ b/Content.Client/Ame/UI/AmeControllerBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Ame.Components; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Ame.UI { @@ -16,9 +17,8 @@ protected override void Open() { base.Open(); - _window = new AmeWindow(this); - _window.OnClose += Close; - _window.OpenCentered(); + _window = this.CreateWindow<AmeWindow>(); + _window.OnAmeButton += ButtonPressed; } /// <summary> @@ -40,15 +40,5 @@ public void ButtonPressed(UiButton button) { SendMessage(new UiButtonPressedMessage(button)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } } } diff --git a/Content.Client/Ame/UI/AmeWindow.xaml.cs b/Content.Client/Ame/UI/AmeWindow.xaml.cs index 8b91ec59660..d6d580bcdaf 100644 --- a/Content.Client/Ame/UI/AmeWindow.xaml.cs +++ b/Content.Client/Ame/UI/AmeWindow.xaml.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Client.UserInterface; using Content.Shared.Ame.Components; using Robust.Client.AutoGenerated; @@ -9,15 +10,17 @@ namespace Content.Client.Ame.UI [GenerateTypedNameReferences] public sealed partial class AmeWindow : DefaultWindow { - public AmeWindow(AmeControllerBoundUserInterface ui) + public event Action<UiButton>? OnAmeButton; + + public AmeWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - EjectButton.OnPressed += _ => ui.ButtonPressed(UiButton.Eject); - ToggleInjection.OnPressed += _ => ui.ButtonPressed(UiButton.ToggleInjection); - IncreaseFuelButton.OnPressed += _ => ui.ButtonPressed(UiButton.IncreaseFuel); - DecreaseFuelButton.OnPressed += _ => ui.ButtonPressed(UiButton.DecreaseFuel); + EjectButton.OnPressed += _ => OnAmeButton?.Invoke(UiButton.Eject); + ToggleInjection.OnPressed += _ => OnAmeButton?.Invoke(UiButton.ToggleInjection); + IncreaseFuelButton.OnPressed += _ => OnAmeButton?.Invoke(UiButton.IncreaseFuel); + DecreaseFuelButton.OnPressed += _ => OnAmeButton?.Invoke(UiButton.DecreaseFuel); } /// <summary> @@ -29,7 +32,7 @@ public void UpdateState(BoundUserInterfaceState state) var castState = (AmeControllerBoundUserInterfaceState) state; // Disable all buttons if not powered - if (Contents.Children != null) + if (Contents.Children.Any()) { ButtonHelpers.SetButtonDisabledRecursive(Contents, !castState.HasPower); EjectButton.Disabled = false; @@ -65,8 +68,8 @@ public void UpdateState(BoundUserInterfaceState state) CoreCount.Text = $"{castState.CoreCount}"; InjectionAmount.Text = $"{castState.InjectionAmount}"; // format power statistics to pretty numbers - CurrentPowerSupply.Text = $"{castState.CurrentPowerSupply.ToString("N1")}"; - TargetedPowerSupply.Text = $"{castState.TargetedPowerSupply.ToString("N1")}"; + CurrentPowerSupply.Text = $"{castState.CurrentPowerSupply:N1}"; + TargetedPowerSupply.Text = $"{castState.TargetedPowerSupply:N1}"; } } } diff --git a/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs b/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs index 5764d0a097d..f088ac1976b 100644 --- a/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs +++ b/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs @@ -1,7 +1,6 @@ using Content.Shared.Anomaly; -using Content.Shared.Gravity; using JetBrains.Annotations; -using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Anomaly.Ui; @@ -18,10 +17,8 @@ protected override void Open() { base.Open(); - _window = new(Owner); - - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<AnomalyGeneratorWindow>(); + _window.SetEntity(Owner); _window.OnGenerateButtonPressed += () => { @@ -37,18 +34,5 @@ protected override void UpdateState(BoundUserInterfaceState state) return; _window?.UpdateState(msg); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - - _window?.Dispose(); - } - - public void SetPowerSwitch(bool on) - { - SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(on)); - } } diff --git a/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs index 08438e2a1b2..82d41192dd0 100644 --- a/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs +++ b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs @@ -18,17 +18,21 @@ public sealed partial class AnomalyGeneratorWindow : FancyWindow public Action? OnGenerateButtonPressed; - public AnomalyGeneratorWindow(EntityUid gen) + public AnomalyGeneratorWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - EntityView.SetEntity(gen); EntityView.SpriteOffset = false; GenerateButton.OnPressed += _ => OnGenerateButtonPressed?.Invoke(); } + public void SetEntity(EntityUid uid) + { + EntityView.SetEntity(uid); + } + public void UpdateState(AnomalyGeneratorUserInterfaceState state) { _cooldownEnd = state.CooldownEndTime; diff --git a/Content.Client/Antag/AntagStatusIconSystem.cs b/Content.Client/Antag/AntagStatusIconSystem.cs deleted file mode 100644 index 3f0f391e48e..00000000000 --- a/Content.Client/Antag/AntagStatusIconSystem.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Content.Shared.Antag; -using Content.Shared.Revolutionary.Components; -using Content.Shared.StatusIcon; -using Content.Shared.StatusIcon.Components; -using Content.Shared.WhiteDream.BloodCult.BloodCultist; -using Content.Shared.WhiteDream.BloodCult.Components; -using Content.Shared.WhiteDream.BloodCult.Constructs; -using Content.Shared.Zombies; -using Robust.Client.Player; -using Robust.Shared.Prototypes; - -namespace Content.Client.Antag; - -/// <summary> -/// Used for assigning specified icons for antags. -/// </summary> -public sealed class AntagStatusIconSystem : SharedStatusIconSystem -{ - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IPlayerManager _player = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<RevolutionaryComponent, GetStatusIconsEvent>(GetRevIcon); - SubscribeLocalEvent<ZombieComponent, GetStatusIconsEvent>(GetIcon); - SubscribeLocalEvent<HeadRevolutionaryComponent, GetStatusIconsEvent>(GetIcon); - SubscribeLocalEvent<InitialInfectedComponent, GetStatusIconsEvent>(GetIcon); - - SubscribeLocalEvent<ConstructComponent, GetStatusIconsEvent>(GetIcon); - SubscribeLocalEvent<BloodCultistComponent, GetStatusIconsEvent>(GetBloodCultIcon); - SubscribeLocalEvent<BloodCultLeaderComponent, GetStatusIconsEvent>(GetIcon); - } - - /// <summary> - /// Adds a Status Icon on an entity if the player is supposed to see it. - /// </summary> - private void GetIcon<T>(EntityUid uid, T comp, ref GetStatusIconsEvent ev) where T: IAntagStatusIconComponent - { - var ent = _player.LocalSession?.AttachedEntity; - - var canEv = new CanDisplayStatusIconsEvent(ent); - RaiseLocalEvent(uid, ref canEv); - - if (!canEv.Cancelled) - ev.StatusIcons.Add(_prototype.Index(comp.StatusIcon)); - } - - /// <summary> - /// Adds the Rev Icon on an entity if the player is supposed to see it. This additional function is needed to deal - /// with a special case where if someone is a head rev we only want to display the headrev icon. - /// </summary> - private void GetRevIcon(EntityUid uid, RevolutionaryComponent comp, ref GetStatusIconsEvent ev) - { - if (HasComp<HeadRevolutionaryComponent>(uid)) - return; - - GetIcon(uid, comp, ref ev); - } - - private void GetBloodCultIcon(EntityUid uid, BloodCultistComponent comp, ref GetStatusIconsEvent ev) - { - if (HasComp<BloodCultLeaderComponent>(uid)) - return; - - GetIcon(uid, comp, ref ev); - } -} diff --git a/Content.Client/Arcade/BlockGameMenu.cs b/Content.Client/Arcade/BlockGameMenu.cs index eeda2a31020..4a579fc4bf4 100644 --- a/Content.Client/Arcade/BlockGameMenu.cs +++ b/Content.Client/Arcade/BlockGameMenu.cs @@ -28,8 +28,6 @@ public sealed class BlockGameMenu : DefaultWindow private static readonly Vector2 BlockSize = new(15, 15); - private readonly BlockGameBoundUserInterface _owner; - private readonly PanelContainer _mainPanel; private readonly BoxContainer _gameRootContainer; @@ -58,10 +56,11 @@ public sealed class BlockGameMenu : DefaultWindow private bool _isPlayer = false; private bool _gameOver = false; - public BlockGameMenu(BlockGameBoundUserInterface owner) + public event Action<BlockGamePlayerAction>? OnAction; + + public BlockGameMenu() { Title = Loc.GetString("blockgame-menu-title"); - _owner = owner; MinSize = SetSize = new Vector2(410, 490); @@ -176,7 +175,7 @@ public BlockGameMenu(BlockGameBoundUserInterface owner) }; _newGameButton.OnPressed += (e) => { - _owner.SendAction(BlockGamePlayerAction.NewGame); + OnAction?.Invoke(BlockGamePlayerAction.NewGame); }; pauseMenuContainer.AddChild(_newGameButton); pauseMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) }); @@ -186,7 +185,10 @@ public BlockGameMenu(BlockGameBoundUserInterface owner) Text = Loc.GetString("blockgame-menu-button-scoreboard"), TextAlign = Label.AlignMode.Center }; - _scoreBoardButton.OnPressed += (e) => _owner.SendAction(BlockGamePlayerAction.ShowHighscores); + _scoreBoardButton.OnPressed += (e) => + { + OnAction?.Invoke(BlockGamePlayerAction.ShowHighscores); + }; pauseMenuContainer.AddChild(_scoreBoardButton); _unpauseButtonMargin = new Control { MinSize = new Vector2(1, 10), Visible = false }; pauseMenuContainer.AddChild(_unpauseButtonMargin); @@ -199,7 +201,7 @@ public BlockGameMenu(BlockGameBoundUserInterface owner) }; _unpauseButton.OnPressed += (e) => { - _owner.SendAction(BlockGamePlayerAction.Unpause); + OnAction?.Invoke(BlockGamePlayerAction.Unpause); }; pauseMenuContainer.AddChild(_unpauseButton); @@ -257,7 +259,7 @@ public BlockGameMenu(BlockGameBoundUserInterface owner) }; _finalNewGameButton.OnPressed += (e) => { - _owner.SendAction(BlockGamePlayerAction.NewGame); + OnAction?.Invoke(BlockGamePlayerAction.NewGame); }; gameOverMenuContainer.AddChild(_finalNewGameButton); @@ -327,7 +329,10 @@ public BlockGameMenu(BlockGameBoundUserInterface owner) Text = Loc.GetString("blockgame-menu-button-back"), TextAlign = Label.AlignMode.Center }; - _highscoreBackButton.OnPressed += (e) => _owner.SendAction(BlockGamePlayerAction.Pause); + _highscoreBackButton.OnPressed += (e) => + { + OnAction?.Invoke(BlockGamePlayerAction.Pause); + }; menuContainer.AddChild(_highscoreBackButton); menuInnerPanel.AddChild(menuContainer); @@ -473,7 +478,7 @@ protected override void KeyboardFocusExited() private void TryPause() { - _owner.SendAction(BlockGamePlayerAction.Pause); + OnAction?.Invoke(BlockGamePlayerAction.Pause); } public void SetStarted() @@ -576,19 +581,19 @@ protected override void KeyBindDown(GUIBoundKeyEventArgs args) return; else if (args.Function == ContentKeyFunctions.ArcadeLeft) - _owner.SendAction(BlockGamePlayerAction.StartLeft); + OnAction?.Invoke(BlockGamePlayerAction.StartLeft); else if (args.Function == ContentKeyFunctions.ArcadeRight) - _owner.SendAction(BlockGamePlayerAction.StartRight); + OnAction?.Invoke(BlockGamePlayerAction.StartRight); else if (args.Function == ContentKeyFunctions.ArcadeUp) - _owner.SendAction(BlockGamePlayerAction.Rotate); + OnAction?.Invoke(BlockGamePlayerAction.Rotate); else if (args.Function == ContentKeyFunctions.Arcade3) - _owner.SendAction(BlockGamePlayerAction.CounterRotate); + OnAction?.Invoke(BlockGamePlayerAction.CounterRotate); else if (args.Function == ContentKeyFunctions.ArcadeDown) - _owner.SendAction(BlockGamePlayerAction.SoftdropStart); + OnAction?.Invoke(BlockGamePlayerAction.SoftdropStart); else if (args.Function == ContentKeyFunctions.Arcade2) - _owner.SendAction(BlockGamePlayerAction.Hold); + OnAction?.Invoke(BlockGamePlayerAction.Hold); else if (args.Function == ContentKeyFunctions.Arcade1) - _owner.SendAction(BlockGamePlayerAction.Harddrop); + OnAction?.Invoke(BlockGamePlayerAction.Harddrop); } protected override void KeyBindUp(GUIBoundKeyEventArgs args) @@ -599,11 +604,11 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) return; else if (args.Function == ContentKeyFunctions.ArcadeLeft) - _owner.SendAction(BlockGamePlayerAction.EndLeft); + OnAction?.Invoke(BlockGamePlayerAction.EndLeft); else if (args.Function == ContentKeyFunctions.ArcadeRight) - _owner.SendAction(BlockGamePlayerAction.EndRight); + OnAction?.Invoke(BlockGamePlayerAction.EndRight); else if (args.Function == ContentKeyFunctions.ArcadeDown) - _owner.SendAction(BlockGamePlayerAction.SoftdropEnd); + OnAction?.Invoke(BlockGamePlayerAction.SoftdropEnd); } public void UpdateNextBlock(BlockGameBlock[] blocks) diff --git a/Content.Client/Arcade/SpaceVillainArcadeMenu.cs b/Content.Client/Arcade/SpaceVillainArcadeMenu.cs index e5542a5848e..1ee4c268184 100644 --- a/Content.Client/Arcade/SpaceVillainArcadeMenu.cs +++ b/Content.Client/Arcade/SpaceVillainArcadeMenu.cs @@ -8,8 +8,6 @@ namespace Content.Client.Arcade { public sealed class SpaceVillainArcadeMenu : DefaultWindow { - public SpaceVillainArcadeBoundUserInterface Owner { get; set; } - private readonly Label _enemyNameLabel; private readonly Label _playerInfoLabel; private readonly Label _enemyInfoLabel; @@ -17,11 +15,13 @@ public sealed class SpaceVillainArcadeMenu : DefaultWindow private readonly Label _enemyActionLabel; private readonly Button[] _gameButtons = new Button[3]; //used to disable/enable all game buttons - public SpaceVillainArcadeMenu(SpaceVillainArcadeBoundUserInterface owner) + + public event Action<SharedSpaceVillainArcadeComponent.PlayerAction>? OnPlayerAction; + + public SpaceVillainArcadeMenu() { MinSize = SetSize = new Vector2(300, 225); Title = Loc.GetString("spacevillain-menu-title"); - Owner = owner; var grid = new GridContainer { Columns = 1 }; @@ -47,32 +47,43 @@ public SpaceVillainArcadeMenu(SpaceVillainArcadeBoundUserInterface owner) grid.AddChild(_enemyActionLabel); var buttonGrid = new GridContainer { Columns = 3 }; - _gameButtons[0] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Attack) + _gameButtons[0] = new Button() { Text = Loc.GetString("spacevillain-menu-button-attack") }; + + _gameButtons[0].OnPressed += + _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Attack); buttonGrid.AddChild(_gameButtons[0]); - _gameButtons[1] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Heal) + _gameButtons[1] = new Button() { Text = Loc.GetString("spacevillain-menu-button-heal") }; + + _gameButtons[1].OnPressed += + _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Heal); buttonGrid.AddChild(_gameButtons[1]); - _gameButtons[2] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Recharge) + _gameButtons[2] = new Button() { Text = Loc.GetString("spacevillain-menu-button-recharge") }; + + _gameButtons[2].OnPressed += + _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Recharge); buttonGrid.AddChild(_gameButtons[2]); centerContainer = new CenterContainer(); centerContainer.AddChild(buttonGrid); grid.AddChild(centerContainer); - var newGame = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.NewGame) + var newGame = new Button() { Text = Loc.GetString("spacevillain-menu-button-new-game") }; + + newGame.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.NewGame); grid.AddChild(newGame); Contents.AddChild(grid); @@ -99,23 +110,5 @@ public void UpdateInfo(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeDataU _playerActionLabel.Text = message.PlayerActionMessage; _enemyActionLabel.Text = message.EnemyActionMessage; } - - private sealed class ActionButton : Button - { - private readonly SpaceVillainArcadeBoundUserInterface _owner; - private readonly SharedSpaceVillainArcadeComponent.PlayerAction _playerAction; - - public ActionButton(SpaceVillainArcadeBoundUserInterface owner, SharedSpaceVillainArcadeComponent.PlayerAction playerAction) - { - _owner = owner; - _playerAction = playerAction; - OnPressed += Clicked; - } - - private void Clicked(ButtonEventArgs e) - { - _owner.SendAction(_playerAction); - } - } } } diff --git a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs index 1a3422dec0f..8fa8035afd6 100644 --- a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Arcade; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Arcade.UI; @@ -15,9 +16,7 @@ protected override void Open() { base.Open(); - _menu = new BlockGameMenu(this); - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<BlockGameMenu>(); } protected override void ReceiveMessage(BoundUserInterfaceMessage message) diff --git a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs index 40bbe8b2d8c..c0704530de2 100644 --- a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs @@ -1,4 +1,5 @@ using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.GameObjects; using Robust.Shared.ViewVariables; using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; @@ -9,8 +10,6 @@ public sealed class SpaceVillainArcadeBoundUserInterface : BoundUserInterface { [ViewVariables] private SpaceVillainArcadeMenu? _menu; - //public SharedSpaceVillainArcadeComponent SpaceVillainArcade; - public SpaceVillainArcadeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { SendAction(PlayerAction.RequestData); @@ -25,10 +24,7 @@ protected override void Open() { base.Open(); - _menu = new SpaceVillainArcadeMenu(this); - - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<SpaceVillainArcadeMenu>(); } protected override void ReceiveMessage(BoundUserInterfaceMessage message) @@ -36,12 +32,4 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) if (message is SpaceVillainArcadeDataUpdateMessage msg) _menu?.UpdateInfo(msg); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - _menu?.Dispose(); - } } diff --git a/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs b/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs index 8f3b507c806..2ae15188355 100644 --- a/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs +++ b/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Monitor.Components; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -20,16 +21,9 @@ protected override void Open() { base.Open(); - _window = new AirAlarmWindow(this); + _window = this.CreateWindow<AirAlarmWindow>(); + _window.SetEntity(Owner); - if (State != null) - { - UpdateState(State); - } - - _window.OpenCentered(); - - _window.OnClose += Close; _window.AtmosDeviceDataChanged += OnDeviceDataChanged; _window.AtmosDeviceDataCopied += OnDeviceDataCopied; _window.AtmosAlarmThresholdChanged += OnThresholdChanged; diff --git a/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs b/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs index 43be67c9d6b..eeec11c7660 100644 --- a/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs +++ b/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml.cs @@ -47,7 +47,7 @@ public sealed partial class AirAlarmWindow : FancyWindow private CheckBox _autoMode => AutoModeCheckBox; - public AirAlarmWindow(BoundUserInterface owner) + public AirAlarmWindow() { RobustXamlLoader.Load(this); @@ -95,8 +95,11 @@ public AirAlarmWindow(BoundUserInterface owner) _sensors.Clear(); ResyncAllRequested!.Invoke(); }; + } - EntityView.SetEntity(owner.Owner); + public void SetEntity(EntityUid uid) + { + EntityView.SetEntity(uid); } public void UpdateState(AirAlarmUIState state) diff --git a/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs index a5e316a8def..7bf9b396d5e 100644 --- a/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Atmos.Piping.Binary.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Atmos.UI { @@ -21,14 +22,8 @@ protected override void Open() { base.Open(); - _window = new GasCanisterWindow(); + _window = this.CreateWindow<GasCanisterWindow>(); - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; _window.ReleaseValveCloseButtonPressed += OnReleaseValveClosePressed; _window.ReleaseValveOpenButtonPressed += OnReleaseValveOpenPressed; _window.ReleasePressureSet += OnReleasePressureSet; diff --git a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs index 1904e2b3402..2b8020924cf 100644 --- a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs @@ -3,6 +3,7 @@ using Content.Shared.Atmos.Piping.Trinary.Components; using Content.Shared.Localizations; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Atmos.UI { @@ -28,14 +29,8 @@ protected override void Open() var atmosSystem = EntMan.System<AtmosphereSystem>(); - _window = new GasFilterWindow(atmosSystem.Gases); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<GasFilterWindow>(); + _window.PopulateGasList(atmosSystem.Gases); _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; _window.FilterTransferRateChanged += OnFilterTransferRatePressed; diff --git a/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs b/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs index 28766c688a0..62748b52592 100644 --- a/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs @@ -26,10 +26,9 @@ public sealed partial class GasFilterWindow : DefaultWindow public event Action<string>? FilterTransferRateChanged; public event Action? SelectGasPressed; - public GasFilterWindow(IEnumerable<GasPrototype> gases) + public GasFilterWindow() { RobustXamlLoader.Load(this); - PopulateGasList(gases); ToggleStatusButton.OnPressed += _ => SetFilterStatus(!FilterStatus); ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke(); @@ -73,7 +72,7 @@ public void SetGasFiltered(string? id, string name) SelectGasButton.Disabled = true; } - private void PopulateGasList(IEnumerable<GasPrototype> gases) + public void PopulateGasList(IEnumerable<GasPrototype> gases) { GasList.Add(new ItemList.Item(GasList) { @@ -81,7 +80,7 @@ private void PopulateGasList(IEnumerable<GasPrototype> gases) Text = Loc.GetString("comp-gas-filter-ui-filter-gas-none") }); - foreach (GasPrototype gas in gases) + foreach (var gas in gases) { var gasName = Loc.GetString(gas.Name); GasList.Add(GetGasItem(gas.ID, gasName, GasList)); diff --git a/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs b/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs index 709c06517cb..392fbf1cd9a 100644 --- a/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs +++ b/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs @@ -2,7 +2,7 @@ using Content.Shared.Atmos.Piping.Trinary.Components; using Content.Shared.Localizations; using JetBrains.Annotations; -using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Atmos.UI { @@ -26,14 +26,7 @@ protected override void Open() { base.Open(); - _window = new GasMixerWindow(); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<GasMixerWindow>(); _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; _window.MixerOutputPressureChanged += OnMixerOutputPressurePressed; @@ -83,12 +76,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.SetOutputPressure(cast.OutputPressure); _window.SetNodePercentages(cast.NodeOne); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } } diff --git a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs index 6eba2e0d215..220fdbe875c 100644 --- a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs @@ -3,6 +3,7 @@ using Content.Shared.Localizations; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Atmos.UI { @@ -26,14 +27,7 @@ protected override void Open() { base.Open(); - _window = new GasPressurePumpWindow(); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<GasPressurePumpWindow>(); _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; _window.PumpOutputPressureChanged += OnPumpOutputPressurePressed; @@ -67,12 +61,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.SetPumpStatus(cast.Enabled); _window.SetOutputPressure(cast.OutputPressure); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } } diff --git a/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs b/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs index 1664c8b9d75..d62be8f4bb4 100644 --- a/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Atmos.Piping.Unary.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Atmos.UI { @@ -31,14 +32,7 @@ protected override void Open() { base.Open(); - _window = new GasThermomachineWindow(); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<GasThermomachineWindow>(); _window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed(); _window.TemperatureSpinbox.OnValueChanged += _ => OnTemperatureChanged(_window.TemperatureSpinbox.Value); @@ -91,12 +85,5 @@ protected override void UpdateState(BoundUserInterfaceState state) true => Loc.GetString("comp-gas-thermomachine-ui-title-heater") }; } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } } diff --git a/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs index 1b39306181a..642f34c2f92 100644 --- a/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs @@ -3,6 +3,7 @@ using Content.Shared.Localizations; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Atmos.UI { @@ -26,14 +27,7 @@ protected override void Open() { base.Open(); - _window = new GasVolumePumpWindow(); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<GasVolumePumpWindow>(); _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; _window.PumpTransferRateChanged += OnPumpTransferRatePressed; @@ -64,16 +58,9 @@ protected override void UpdateState(BoundUserInterfaceState state) if (_window == null || state is not GasVolumePumpBoundUserInterfaceState cast) return; - _window.Title = (cast.PumpLabel); + _window.Title = cast.PumpLabel; _window.SetPumpStatus(cast.Enabled); _window.SetTransferRate(cast.TransferRate); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } } diff --git a/Content.Client/Atmos/UI/SpaceHeaterBoundUserInterface.cs b/Content.Client/Atmos/UI/SpaceHeaterBoundUserInterface.cs index 4d8d1191e91..e70426575d4 100644 --- a/Content.Client/Atmos/UI/SpaceHeaterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/SpaceHeaterBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Atmos.Piping.Portable.Components; using JetBrains.Annotations; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; namespace Content.Client.Atmos.UI; @@ -21,14 +22,7 @@ protected override void Open() { base.Open(); - _window = new SpaceHeaterWindow(); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<SpaceHeaterWindow>(); _window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed(); _window.IncreaseTempRange.OnPressed += _ => OnTemperatureRangeChanged(_window.TemperatureChangeDelta); diff --git a/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs index 60fe339069a..865dfc478d0 100644 --- a/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs +++ b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs @@ -1,8 +1,7 @@ using Content.Shared.Audio.Jukebox; using Robust.Client.Audio; -using Robust.Client.Player; +using Robust.Client.UserInterface; using Robust.Shared.Audio.Components; -using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Client.Audio.Jukebox; @@ -23,9 +22,7 @@ protected override void Open() { base.Open(); - _menu = new JukeboxMenu(); - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<JukeboxMenu>(); _menu.OnPlayPressed += args => { @@ -100,19 +97,5 @@ public void SetTime(float time) SendMessage(new JukeboxSetTimeMessage(sentTime)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - if (_menu == null) - return; - - _menu.OnClose -= Close; - _menu.Dispose(); - _menu = null; - } } diff --git a/Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs b/Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs index ffab1625483..09f3cec8fbf 100644 --- a/Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs +++ b/Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Bed.Cryostorage; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Bed.Cryostorage; @@ -17,9 +18,7 @@ protected override void Open() { base.Open(); - _menu = new(); - - _menu.OnClose += Close; + _menu = this.CreateWindow<CryostorageMenu>(); _menu.SlotRemoveButtonPressed += (ent, slot) => { @@ -30,8 +29,6 @@ protected override void Open() { SendMessage(new CryostorageRemoveItemBuiMessage(ent, hand, CryostorageRemoveItemBuiMessage.RemovalType.Hand)); }; - - _menu.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -45,12 +42,4 @@ protected override void UpdateState(BoundUserInterfaceState state) break; } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Dispose(); - } } diff --git a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs index d3365702bcf..44c40143d83 100644 --- a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Client.Cargo.UI; using Content.Shared.Cargo.Components; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Cargo.BUI; @@ -18,9 +19,7 @@ protected override void Open() { base.Open(); - _menu = new(); - - _menu.OnClose += Close; + _menu = this.CreateWindow<CargoBountyMenu>(); _menu.OnLabelButtonPressed += id => { @@ -31,8 +30,6 @@ protected override void Open() { SendMessage(new BountySkipMessage(id)); }; - - _menu.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState message) @@ -44,14 +41,4 @@ protected override void UpdateState(BoundUserInterfaceState message) _menu?.UpdateEntries(state.Bounties, state.UntilNextSkip); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (!disposing) - return; - - _menu?.Dispose(); - } } diff --git a/Content.Client/Cargo/BUI/CargoPalletConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoPalletConsoleBoundUserInterface.cs index 20c23a48a0d..2461dafb5f3 100644 --- a/Content.Client/Cargo/BUI/CargoPalletConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoPalletConsoleBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Cargo.BUI; using Content.Shared.Cargo.Events; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Cargo.BUI; @@ -18,21 +19,9 @@ protected override void Open() { base.Open(); - _menu = new CargoPalletMenu(); + _menu = this.CreateWindow<CargoPalletMenu>(); _menu.AppraiseRequested += OnAppraisal; _menu.SellRequested += OnSell; - _menu.OnClose += Close; - - _menu.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - _menu?.Dispose(); - } } private void OnAppraisal() diff --git a/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs index 422d03707a0..02b721b9020 100644 --- a/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Cargo.BUI; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.Cargo.BUI; @@ -9,6 +10,8 @@ namespace Content.Client.Cargo.BUI; [UsedImplicitly] public sealed class CargoShuttleConsoleBoundUserInterface : BoundUserInterface { + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [ViewVariables] private CargoShuttleMenu? _menu; @@ -19,24 +22,7 @@ public CargoShuttleConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base protected override void Open() { base.Open(); - var collection = IoCManager.Instance; - - if (collection == null) - return; - - _menu = new CargoShuttleMenu(collection.Resolve<IPrototypeManager>(), collection.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>()); - _menu.OnClose += Close; - - _menu.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - _menu?.Dispose(); - } + _menu = this.CreateWindow<CargoShuttleMenu>(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -45,6 +31,6 @@ protected override void UpdateState(BoundUserInterfaceState state) if (state is not CargoShuttleConsoleBoundUserInterfaceState cargoState) return; _menu?.SetAccountName(cargoState.AccountName); _menu?.SetShuttleName(cargoState.ShuttleName); - _menu?.SetOrders(cargoState.Orders); + _menu?.SetOrders(EntMan.System<SpriteSystem>(), _protoManager, cargoState.Orders); } } diff --git a/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs b/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs index c591f917da3..43b00089e16 100644 --- a/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs +++ b/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs @@ -12,14 +12,9 @@ namespace Content.Client.Cargo.UI [GenerateTypedNameReferences] public sealed partial class CargoShuttleMenu : FancyWindow { - private readonly IPrototypeManager _protoManager; - private readonly SpriteSystem _spriteSystem; - - public CargoShuttleMenu(IPrototypeManager protoManager, SpriteSystem spriteSystem) + public CargoShuttleMenu() { RobustXamlLoader.Load(this); - _protoManager = protoManager; - _spriteSystem = spriteSystem; Title = Loc.GetString("cargo-shuttle-console-menu-title"); } @@ -33,19 +28,19 @@ public void SetShuttleName(string name) ShuttleNameLabel.Text = name; } - public void SetOrders(List<CargoOrderData> orders) + public void SetOrders(SpriteSystem sprites, IPrototypeManager protoManager, List<CargoOrderData> orders) { Orders.DisposeAllChildren(); foreach (var order in orders) { - var product = _protoManager.Index<EntityPrototype>(order.ProductId); + var product = protoManager.Index<EntityPrototype>(order.ProductId); var productName = product.Name; var row = new CargoOrderRow { Order = order, - Icon = { Texture = _spriteSystem.Frame0(product) }, + Icon = { Texture = sprites.Frame0(product) }, ProductName = { Text = Loc.GetString( diff --git a/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs index 273707cb6ea..27ddd51815e 100644 --- a/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs +++ b/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs @@ -1,4 +1,5 @@ -using Content.Shared.CrewManifest; +using Content.Client.CrewManifest.UI; +using Content.Shared.CrewManifest; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs index d28d3228c94..aaf3900beee 100644 --- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs +++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs @@ -23,6 +23,6 @@ public override void UpdateState(BoundUserInterfaceState state) if (state is not LogProbeUiState logProbeUiState) return; - _fragment?.UpdateState(logProbeUiState.PulledLogs); + _fragment?.UpdateState(logProbeUiState); // DeltaV - just take the state } } diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml index d12fb55cdce..a0769590e91 100644 --- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml +++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml @@ -9,10 +9,30 @@ BorderColor="#5a5a5a" BorderThickness="0 0 0 1"/> </PanelContainer.PanelOverride> - <BoxContainer Orientation="Horizontal" Margin="4 8"> - <Label Align="Right" SetWidth="26" ClipText="True" Text="{Loc 'log-probe-label-number'}"/> - <Label Align="Center" SetWidth="100" ClipText="True" Text="{Loc 'log-probe-label-time'}"/> - <Label Align="Left" SetWidth="390" ClipText="True" Text="{Loc 'log-probe-label-accessor'}"/> + <BoxContainer Orientation="Vertical" Margin="4 8"> + <!-- DeltaV begin - Add title label --> + <Label Name="TitleLabel" + Text="{Loc 'log-probe-header-access'}" + StyleClasses="LabelHeading" + HorizontalAlignment="Center" + Margin="0 0 0 8"/> + <!-- DeltaV end --> + + <!-- DeltaV begin - Add card number display --> + <Label Name="CardNumberLabel" + StyleClasses="LabelSubText" + HorizontalAlignment="Center" + Margin="0 0 0 8" + Visible="False"/> + <!-- DeltaV end --> + + <!-- DeltaV begin - Adjust column headers --> + <BoxContainer Orientation="Horizontal"> + <Label Align="Right" SetWidth="26" ClipText="True" Text="{Loc 'log-probe-label-number'}"/> + <Label Align="Center" SetWidth="100" ClipText="True" Text="{Loc 'log-probe-label-time'}"/> + <Label Name="ContentLabel" Align="Left" SetWidth="390" ClipText="True" Text="{Loc 'log-probe-label-accessor'}"/> + </BoxContainer> + <!-- DeltaV end --> </BoxContainer> </PanelContainer> <ScrollContainer VerticalExpand="True" HScrollEnabled="True"> diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs index b22e0bc1964..5fa93bb40db 100644 --- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs +++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs @@ -1,4 +1,7 @@ -using Content.Shared.CartridgeLoader.Cartridges; +using System.Linq; // DeltaV +using Content.Client.DeltaV.CartridgeLoader.Cartridges; // DeltaV +using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; @@ -13,10 +16,112 @@ public LogProbeUiFragment() RobustXamlLoader.Load(this); } - public void UpdateState(List<PulledAccessLog> logs) + // DeltaV begin - Update to handle both types of data + public void UpdateState(LogProbeUiState state) { ProbedDeviceContainer.RemoveAllChildren(); + if (state.NanoChatData != null) + { + SetupNanoChatView(state.NanoChatData.Value); + DisplayNanoChatData(state.NanoChatData.Value); + } + else + { + SetupAccessLogView(); + if (state.PulledLogs.Count > 0) + DisplayAccessLogs(state.PulledLogs); + } + } + + private void SetupNanoChatView(NanoChatData data) + { + TitleLabel.Text = Loc.GetString("log-probe-header-nanochat"); + ContentLabel.Text = Loc.GetString("log-probe-label-message"); + + // Show card info if available + var cardInfo = new List<string>(); + if (data.CardNumber != null) + cardInfo.Add(Loc.GetString("log-probe-card-number", ("number", $"#{data.CardNumber:D4}"))); + + // Add recipient count + cardInfo.Add(Loc.GetString("log-probe-recipients", ("count", data.Recipients.Count))); + + CardNumberLabel.Text = string.Join(" | ", cardInfo); + CardNumberLabel.Visible = true; + } + + private void SetupAccessLogView() + { + TitleLabel.Text = Loc.GetString("log-probe-header-access"); + ContentLabel.Text = Loc.GetString("log-probe-label-accessor"); + CardNumberLabel.Visible = false; + } + + private void DisplayNanoChatData(NanoChatData data) + { + // First add a recipient list entry + var recipientsList = Loc.GetString("log-probe-recipient-list") + "\n" + string.Join("\n", + data.Recipients.Values + .OrderBy(r => r.Name) + .Select(r => $" {r.Name}" + + (string.IsNullOrEmpty(r.JobTitle) ? "" : $" ({r.JobTitle})") + + $" | #{r.Number:D4}")); + + var recipientsEntry = new LogProbeUiEntry(0, "---", recipientsList); + ProbedDeviceContainer.AddChild(recipientsEntry); + + var count = 1; + foreach (var (partnerId, messages) in data.Messages) + { + // Show only successfully delivered incoming messages + var incomingMessages = messages + .Where(msg => msg.SenderId == partnerId && !msg.DeliveryFailed) + .OrderByDescending(msg => msg.Timestamp); + + foreach (var msg in incomingMessages) + { + var messageText = Loc.GetString("log-probe-message-format", + ("sender", $"#{msg.SenderId:D4}"), + ("recipient", $"#{data.CardNumber:D4}"), + ("content", msg.Content)); + + var entry = new NanoChatLogEntry( + count, + TimeSpan.FromSeconds(Math.Truncate(msg.Timestamp.TotalSeconds)).ToString(), + messageText); + + ProbedDeviceContainer.AddChild(entry); + count++; + } + + // Show only successfully delivered outgoing messages + var outgoingMessages = messages + .Where(msg => msg.SenderId == data.CardNumber && !msg.DeliveryFailed) + .OrderByDescending(msg => msg.Timestamp); + + foreach (var msg in outgoingMessages) + { + var messageText = Loc.GetString("log-probe-message-format", + ("sender", $"#{msg.SenderId:D4}"), + ("recipient", $"#{partnerId:D4}"), + ("content", msg.Content)); + + var entry = new NanoChatLogEntry( + count, + TimeSpan.FromSeconds(Math.Truncate(msg.Timestamp.TotalSeconds)).ToString(), + messageText); + + ProbedDeviceContainer.AddChild(entry); + count++; + } + } + } + // DeltaV end + + // DeltaV - Handle this in a separate method + private void DisplayAccessLogs(List<PulledAccessLog> logs) + { //Reverse the list so the oldest entries appear at the bottom logs.Reverse(); diff --git a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs index 988fea7978b..3ef7f0ae73e 100644 --- a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Containers.ItemSlots; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Chemistry.UI { @@ -27,13 +28,8 @@ protected override void Open() base.Open(); // Setup window layout/elements - _window = new ChemMasterWindow - { - Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName, - }; - - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<ChemMasterWindow>(); + _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; // Setup static button actions. _window.InputEjectButton.OnPressed += _ => SendMessage( @@ -75,15 +71,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.UpdateState(castState); // Update window state } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } } } diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs index a0871c16a1f..d312a69e7a6 100644 --- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs +++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Numerics; using Content.Shared.FixedPoint; +using Robust.Client.Graphics; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Chemistry.UI @@ -90,10 +91,41 @@ public ChemMasterWindow() private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass) { - var button = new ReagentButton(text, amount, id, isBuffer, styleClass); - button.OnPressed += args - => OnReagentButtonPressed?.Invoke(args, button); - return button; + var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass); + reagentTransferButton.OnPressed += args + => OnReagentButtonPressed?.Invoke(args, reagentTransferButton); + return reagentTransferButton; + } + /// <summary> + /// Conditionally generates a set of reagent buttons based on the supplied boolean argument. + /// This was moved outside of BuildReagentRow to facilitate conditional logic, stops indentation depth getting out of hand as well. + /// </summary> + private List<ReagentButton> CreateReagentTransferButtons(ReagentId reagent, bool isBuffer, bool addReagentButtons) + { + if (!addReagentButtons) + return new List<ReagentButton>(); // Return an empty list if reagentTransferButton creation is disabled. + + var buttons = new List<ReagentButton>(); + var names = Enum.GetNames<ChemMasterReagentAmount>(); + var values = Enum.GetValues<ChemMasterReagentAmount>(); + + for (int i = 0; i < names.Length; i++) + { + var name = names[i]; + var reagentAmount = values[i]; + + var reagentTransferButton = MakeReagentButton( + name, + reagentAmount, + reagent, + isBuffer, + i != names.Length - 1 ? StyleBase.ButtonOpenBoth : StyleBase.ButtonOpenLeft + ); + + buttons.Add(reagentTransferButton); + } + + return buttons; } /// <summary> @@ -102,24 +134,35 @@ private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amo /// <param name="state">State data sent by the server.</param> public void UpdateState(BoundUserInterfaceState state) { - var castState = (ChemMasterBoundUserInterfaceState) state; + var castState = (ChemMasterBoundUserInterfaceState)state; + if (castState.UpdateLabel) LabelLine = GenerateLabel(castState); - UpdatePanelInfo(castState); - var output = castState.OutputContainerInfo; + // Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on + UpdatePanelInfo(castState); BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u"; InputEjectButton.Disabled = castState.InputContainerInfo is null; - OutputEjectButton.Disabled = output is null; - CreateBottleButton.Disabled = output?.Reagents == null; - CreatePillButton.Disabled = output?.Entities == null; + OutputEjectButton.Disabled = castState.OutputContainerInfo is null; + CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null; + CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null; + UpdateDosageFields(castState); + } + + //assign default values for pill and bottle fields. + private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState) + { + var output = castState.OutputContainerInfo; var remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int(); var holdsReagents = output?.Reagents != null; var pillNumberMax = holdsReagents ? 0 : remainingCapacity; var bottleAmountMax = holdsReagents ? remainingCapacity : 0; + var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0; + + PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit); PillTypeButtons[castState.SelectedPillType].Pressed = true; PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax; @@ -130,8 +173,19 @@ public void UpdateState(BoundUserInterfaceState state) PillNumber.Value = pillNumberMax; if (BottleDosage.Value > bottleAmountMax) BottleDosage.Value = bottleAmountMax; - } + // Avoid division by zero + if (PillDosage.Value > 0) + { + PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax); + } + else + { + PillNumber.Value = 0; + } + + BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume); + } /// <summary> /// Generate a product label based on reagents in the buffer. /// </summary> @@ -178,43 +232,20 @@ private void UpdatePanelInfo(ChemMasterBoundUserInterfaceState state) var bufferVol = new Label { Text = $"{state.BufferCurrentVolume}u", - StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + StyleClasses = { StyleNano.StyleClassLabelSecondaryColor } }; bufferHBox.AddChild(bufferVol); + // initialises rowCount to allow for striped rows + + var rowCount = 0; foreach (var (reagent, quantity) in state.BufferReagents.OrderBy(x => x.Reagent.Prototype)) { - // Try to get the prototype for the given reagent. This gives us its name. - _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto); + var reagentId = reagent; + _prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto); var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text"); - - if (proto != null) - { - BufferInfo.Children.Add(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label {Text = $"{name}: "}, - new Label - { - Text = $"{quantity}u", - StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} - }, - - // Padding - new Control {HorizontalExpand = true}, - - MakeReagentButton("1", ChemMasterReagentAmount.U1, reagent, true, StyleBase.ButtonOpenRight), - MakeReagentButton("5", ChemMasterReagentAmount.U5, reagent, true, StyleBase.ButtonOpenBoth), - MakeReagentButton("10", ChemMasterReagentAmount.U10, reagent, true, StyleBase.ButtonOpenBoth), - MakeReagentButton("25", ChemMasterReagentAmount.U25, reagent, true, StyleBase.ButtonOpenBoth), - MakeReagentButton("50", ChemMasterReagentAmount.U50, reagent, true, StyleBase.ButtonOpenBoth), - MakeReagentButton("100", ChemMasterReagentAmount.U100, reagent, true, StyleBase.ButtonOpenBoth), - MakeReagentButton(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, reagent, true, StyleBase.ButtonOpenLeft), - } - }); - } + var reagentColor = proto?.SubstanceColor ?? default(Color); + BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true)); } } @@ -228,104 +259,111 @@ private void BuildContainerUI(Control control, ContainerInfo? info, bool addReag { Text = Loc.GetString("chem-master-window-no-container-loaded-text") }); + return; } - else + + // Name of the container and its fill status (Ex: 44/100u) + control.Children.Add(new BoxContainer { - // Name of the container and its fill status (Ex: 44/100u) - control.Children.Add(new BoxContainer + Orientation = LayoutOrientation.Horizontal, + Children = { - Orientation = LayoutOrientation.Horizontal, - Children = + new Label { Text = $"{info.DisplayName}: " }, + new Label { - new Label {Text = $"{info.DisplayName}: "}, - new Label - { - Text = $"{info.CurrentVolume}/{info.MaxVolume}", - StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} - } + Text = $"{info.CurrentVolume}/{info.MaxVolume}", + StyleClasses = { StyleNano.StyleClassLabelSecondaryColor } } - }); - - IEnumerable<(string Name, ReagentId Id, FixedPoint2 Quantity)> contents; + } + }); + // Initialises rowCount to allow for striped rows + var rowCount = 0; - if (info.Entities != null) + // Handle entities if they are not null + if (info.Entities != null) + { + foreach (var (id, quantity) in info.Entities.Select(x => (x.Id, x.Quantity))) { - contents = info.Entities.Select(x => (x.Id, default(ReagentId), x.Quantity)); + control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons)); } - else if (info.Reagents != null) - { - contents = info.Reagents.Select(x => - { - _prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto); - var name = proto?.LocalizedName - ?? Loc.GetString("chem-master-window-unknown-reagent-text"); + } - return (name, Id: x.Reagent, x.Quantity); - }) - .OrderBy(r => r.Item1); - } - else + // Handle reagents if they are not null + if (info.Reagents != null) + { + foreach (var reagent in info.Reagents) { - return; - } + _prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? proto); + var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text"); + var reagentColor = proto?.SubstanceColor ?? default(Color); + control.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagent.Reagent, reagent.Quantity, false, addReagentButtons)); + } + } + } + /// <summary> + /// Take reagent/entity data and present rows, labels, and buttons appropriately. todo sprites? + /// </summary> + private Control BuildReagentRow(Color reagentColor, int rowCount, string name, ReagentId reagent, FixedPoint2 quantity, bool isBuffer, bool addReagentButtons) + { + //Colors rows and sets fallback for reagentcolor to the same as background, this will hide colorPanel for entities hopefully + var rowColor1 = Color.FromHex("#1B1B1E"); + var rowColor2 = Color.FromHex("#202025"); + var currentRowColor = (rowCount % 2 == 1) ? rowColor1 : rowColor2; + if ((reagentColor == default(Color))|(!addReagentButtons)) + { + reagentColor = currentRowColor; + } + //this calls the separated button builder, and stores the return to render after labels + var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons); - foreach (var (name, id, quantity) in contents) + // Create the row layout with the color panel + var rowContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + Children = { - var inner = new BoxContainer + new Label { Text = $"{name}: " }, + new Label { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label { Text = $"{name}: " }, - new Label - { - Text = $"{quantity}u", - StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }, - } - } - }; - - if (addReagentButtons) + Text = $"{quantity}u", + StyleClasses = { StyleNano.StyleClassLabelSecondaryColor } + }, + + // Padding + new Control { HorizontalExpand = true }, + // Colored panels for reagents + new PanelContainer { - var cs = inner.Children; - - // Padding - cs.Add(new Control { HorizontalExpand = true }); - - cs.Add(MakeReagentButton( - "1", ChemMasterReagentAmount.U1, id, false, StyleBase.ButtonOpenRight)); - cs.Add(MakeReagentButton( - "5", ChemMasterReagentAmount.U5, id, false, StyleBase.ButtonOpenBoth)); - cs.Add(MakeReagentButton( - "10", ChemMasterReagentAmount.U10, id, false, StyleBase.ButtonOpenBoth)); - cs.Add(MakeReagentButton( - "25", ChemMasterReagentAmount.U25, id, false, StyleBase.ButtonOpenBoth)); - cs.Add(MakeReagentButton( - "50", ChemMasterReagentAmount.U50, id, false, StyleBase.ButtonOpenBoth)); - cs.Add(MakeReagentButton( - "100", ChemMasterReagentAmount.U100, id, false, StyleBase.ButtonOpenBoth)); - cs.Add(MakeReagentButton( - Loc.GetString("chem-master-window-buffer-all-amount"), - ChemMasterReagentAmount.All, id, false, StyleBase.ButtonOpenLeft)); + Name = "colorPanel", + VerticalExpand = true, + MinWidth = 4, + PanelOverride = new StyleBoxFlat + { + BackgroundColor = reagentColor + }, + Margin = new Thickness(0, 1) } - - control.Children.Add(inner); } + }; + // Add the reagent buttons after the color panel + foreach (var reagentTransferButton in reagentButtonConstructors) + { + rowContainer.AddChild(reagentTransferButton); } + //Apply panencontainer to allow for striped rows + return new PanelContainer + { + PanelOverride = new StyleBoxFlat(currentRowColor), + Children = { rowContainer } + }; } - public String LabelLine + public string LabelLine { - get - { - return LabelLineEdit.Text; - } - set - { - LabelLineEdit.Text = value; - } + get => LabelLineEdit.Text; + set => LabelLineEdit.Text = value; } } diff --git a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs index 99e5a3d3953..2ad1b718887 100644 --- a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs @@ -3,6 +3,7 @@ using Content.Shared.Containers.ItemSlots; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Chemistry.UI { @@ -15,9 +16,6 @@ public sealed class ReagentDispenserBoundUserInterface : BoundUserInterface [ViewVariables] private ReagentDispenserWindow? _window; - [ViewVariables] - private ReagentDispenserBoundUserInterfaceState? _lastState; - public ReagentDispenserBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -32,14 +30,9 @@ protected override void Open() base.Open(); // Setup window layout/elements - _window = new() - { - Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName, - HelpGuidebookIds = EntMan.GetComponent<GuideHelpComponent>(Owner).Guides - }; - - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<ReagentDispenserWindow>(); + _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; + _window.HelpGuidebookIds = EntMan.GetComponent<GuideHelpComponent>(Owner).Guides; // Setup static button actions. _window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName)); @@ -63,19 +56,7 @@ protected override void UpdateState(BoundUserInterfaceState state) base.UpdateState(state); var castState = (ReagentDispenserBoundUserInterfaceState) state; - _lastState = castState; - _window?.UpdateState(castState); //Update window state } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } } } diff --git a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs index 35df131312d..f1cb27a62a4 100644 --- a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Chemistry.UI { @@ -18,7 +19,7 @@ public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owne protected override void Open() { base.Open(); - _window = new TransferAmountWindow(); + _window = this.CreateWindow<TransferAmountWindow>(); _window.ApplyButton.OnPressed += _ => { @@ -28,15 +29,6 @@ protected override void Open() _window.Close(); } }; - _window.OnClose += Close; - _window.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); } } } diff --git a/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs b/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs index 26f0994701e..62a02f37186 100644 --- a/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs +++ b/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs @@ -1,6 +1,7 @@ using JetBrains.Annotations; using Robust.Client.GameObjects; using Content.Shared.Cloning.CloningConsole; +using Robust.Client.UserInterface; namespace Content.Client.CloningConsole.UI { @@ -17,13 +18,11 @@ public CloningConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owne protected override void Open() { base.Open(); - _window = new CloningConsoleWindow - { - Title = Loc.GetString("cloning-console-window-title") - }; - _window.OnClose += Close; + + _window = this.CreateWindow<CloningConsoleWindow>(); + _window.Title = Loc.GetString("cloning-console-window-title"); + _window.CloneButton.OnPressed += _ => SendMessage(new UiButtonPressedMessage(UiButton.Clone)); - _window.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -32,19 +31,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.Populate((CloningConsoleBoundUserInterfaceState) state); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - if (_window != null) - { - _window.OnClose -= Close; - _window.CloneButton.OnPressed -= _ => SendMessage(new UiButtonPressedMessage(UiButton.Clone)); - } - _window?.Dispose(); - } } } diff --git a/Content.Client/Clothing/MagbootsSystem.cs b/Content.Client/Clothing/MagbootsSystem.cs deleted file mode 100644 index a3d39eafded..00000000000 --- a/Content.Client/Clothing/MagbootsSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.Clothing; - -namespace Content.Client.Clothing; - -public sealed class MagbootsSystem : SharedMagbootsSystem -{ - -} diff --git a/Content.Client/Clothing/Systems/CursedMaskSystem.cs b/Content.Client/Clothing/Systems/CursedMaskSystem.cs new file mode 100644 index 00000000000..bc931d15fd5 --- /dev/null +++ b/Content.Client/Clothing/Systems/CursedMaskSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Clothing; + +namespace Content.Client.Clothing.Systems; + +/// <inheritdoc/> +public sealed class CursedMaskSystem : SharedCursedMaskSystem; diff --git a/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs b/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs new file mode 100644 index 00000000000..c04cf0a60ba --- /dev/null +++ b/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs @@ -0,0 +1,19 @@ +using Content.Shared.Clothing.Components; +using Robust.Client.Physics; + +namespace Content.Client.Clothing.Systems; + +public sealed partial class PilotedByClothingSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<PilotedByClothingComponent, UpdateIsPredictedEvent>(OnUpdatePredicted); + } + + private void OnUpdatePredicted(Entity<PilotedByClothingComponent> entity, ref UpdateIsPredictedEvent args) + { + args.BlockPrediction = true; + } +} diff --git a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs index 5b0d5fcf21f..83f6ba15662 100644 --- a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs +++ b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Clothing.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Clothing.UI; @@ -22,10 +23,8 @@ protected override void Open() { base.Open(); - _menu = new ChameleonMenu(); - _menu.OnClose += Close; + _menu = this.CreateWindow<ChameleonMenu>(); _menu.OnIdSelected += OnIdSelected; - _menu.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -42,15 +41,4 @@ private void OnIdSelected(string selectedId) { SendMessage(new ChameleonPrototypeSelectedMessage(selectedId)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _menu?.Close(); - _menu = null; - } - } } diff --git a/Content.Client/Commands/HideMechanismsCommand.cs b/Content.Client/Commands/HideMechanismsCommand.cs index 5f9afc78b98..97d792628a6 100644 --- a/Content.Client/Commands/HideMechanismsCommand.cs +++ b/Content.Client/Commands/HideMechanismsCommand.cs @@ -30,7 +30,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) sprite.ContainerOccluded = false; var tempParent = uid; - while (containerSys.TryGetContainingContainer(tempParent, out var container)) + while (containerSys.TryGetContainingContainer((tempParent, null, null), out var container)) { if (!container.ShowContents) { diff --git a/Content.Client/Commands/ShowHealthBarsCommand.cs b/Content.Client/Commands/ShowHealthBarsCommand.cs index bd3e21718f0..ef918313a0d 100644 --- a/Content.Client/Commands/ShowHealthBarsCommand.cs +++ b/Content.Client/Commands/ShowHealthBarsCommand.cs @@ -35,6 +35,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var showHealthBarsComponent = new ShowHealthBarsComponent { DamageContainers = args.ToList(), + HealthStatusIcon = "", NetSyncEnabled = false }; diff --git a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs index 1c94d32bf8d..0310e91eeb0 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Communications; +using Robust.Client.UserInterface; using Robust.Shared.Configuration; using Robust.Shared.Timing; @@ -8,34 +9,11 @@ namespace Content.Client.Communications.UI { public sealed class CommunicationsConsoleBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [ViewVariables] private CommunicationsConsoleMenu? _menu; - [ViewVariables] - public bool CanAnnounce { get; private set; } - [ViewVariables] - public bool CanBroadcast { get; private set; } - - [ViewVariables] - public bool CanCall { get; private set; } - - [ViewVariables] - public bool CountdownStarted { get; private set; } - - [ViewVariables] - public bool AlertLevelSelectable { get; private set; } - - [ViewVariables] - public string CurrentLevel { get; private set; } = default!; - - [ViewVariables] - private TimeSpan? _expectedCountdownTime; - - public int Countdown => _expectedCountdownTime == null ? 0 : Math.Max((int) _expectedCountdownTime.Value.Subtract(_gameTiming.CurTime).TotalSeconds, 0); - public CommunicationsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -44,23 +22,25 @@ protected override void Open() { base.Open(); - _menu = new CommunicationsConsoleMenu(this); - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<CommunicationsConsoleMenu>(); + _menu.OnAnnounce += AnnounceButtonPressed; + _menu.OnBroadcast += BroadcastButtonPressed; + _menu.OnAlertLevel += AlertLevelSelected; + _menu.OnEmergencyLevel += EmergencyShuttleButtonPressed; } public void AlertLevelSelected(string level) { - if (AlertLevelSelectable) + if (_menu!.AlertLevelSelectable) { - CurrentLevel = level; + _menu.CurrentLevel = level; SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level)); } } public void EmergencyShuttleButtonPressed() { - if (CountdownStarted) + if (_menu!.CountdownStarted) RecallShuttle(); else CallShuttle(); @@ -95,31 +75,23 @@ protected override void UpdateState(BoundUserInterfaceState state) if (state is not CommunicationsConsoleInterfaceState commsState) return; - CanAnnounce = commsState.CanAnnounce; - CanBroadcast = commsState.CanBroadcast; - CanCall = commsState.CanCall; - _expectedCountdownTime = commsState.ExpectedCountdownEnd; - CountdownStarted = commsState.CountdownStarted; - AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0; - CurrentLevel = commsState.CurrentAlert; - if (_menu != null) { + _menu.CanAnnounce = commsState.CanAnnounce; + _menu.CanBroadcast = commsState.CanBroadcast; + _menu.CanCall = commsState.CanCall; + _menu.CountdownStarted = commsState.CountdownStarted; + _menu.AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0; + _menu.CurrentLevel = commsState.CurrentAlert; + _menu.CountdownEnd = commsState.ExpectedCountdownEnd; + _menu.UpdateCountdown(); - _menu.UpdateAlertLevels(commsState.AlertLevels, CurrentLevel); - _menu.AlertLevelButton.Disabled = !AlertLevelSelectable; - _menu.EmergencyShuttleButton.Disabled = !CanCall; - _menu.AnnounceButton.Disabled = !CanAnnounce; - _menu.BroadcastButton.Disabled = !CanBroadcast; + _menu.UpdateAlertLevels(commsState.AlertLevels, _menu.CurrentLevel); + _menu.AlertLevelButton.Disabled = !_menu.AlertLevelSelectable; + _menu.EmergencyShuttleButton.Disabled = !_menu.CanCall; + _menu.AnnounceButton.Disabled = !_menu.CanAnnounce; + _menu.BroadcastButton.Disabled = !_menu.CanBroadcast; } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - - _menu?.Dispose(); - } } } diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml index ea2f77d457d..b74df979cf4 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml +++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml @@ -1,17 +1,62 @@ <controls:FancyWindow xmlns="https://spacestation14.io" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" Title="{Loc 'comms-console-menu-title'}" - MinSize="400 225"> - <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="5"> - <TextEdit Name="MessageInput" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 0 5" MinHeight="100" /> - <Button Name="AnnounceButton" Text="{Loc 'comms-console-menu-announcement-button'}" StyleClasses="OpenLeft" Access="Public" /> - <Button Name="BroadcastButton" Text="{Loc 'comms-console-menu-broadcast-button'}" StyleClasses="OpenLeft" Access="Public" /> + MinSize="400 300"> - <OptionButton Name="AlertLevelButton" StyleClasses="OpenRight" Access="Public" /> + <!-- Main Container --> + <BoxContainer Orientation="Vertical" + HorizontalExpand="False" + VerticalExpand="True" + Margin="6 6 6 5"> - <Control MinSize="10 10" /> + <TextEdit Name="MessageInput" + VerticalExpand="True" + HorizontalExpand="True" + VerticalAlignment="Stretch" + HorizontalAlignment="Stretch" + MinHeight="100"/> - <RichTextLabel Name="CountdownLabel" VerticalExpand="True" /> - <Button Name="EmergencyShuttleButton" Text="Placeholder Text" Access="Public" /> + <!-- ButtonsPart --> + <BoxContainer Orientation="Vertical" + VerticalAlignment="Bottom" + SeparationOverride="4"> + + <!-- AnnouncePart --> + <BoxContainer Orientation="Vertical" + Margin="0 2"> + + <Button Name="AnnounceButton" + Access="Public" + Text="{Loc 'comms-console-menu-announcement-button'}" + ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}" + StyleClasses="OpenLeft" + Margin="0 0 1 0" + Disabled="True"/> + + <Button Name="BroadcastButton" + Access="Public" + Text="{Loc 'comms-console-menu-broadcast-button'}" + ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}" + StyleClasses="OpenBoth"/> + + <OptionButton Name="AlertLevelButton" + Access="Public" + ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}" + StyleClasses="OpenRight"/> + + </BoxContainer> + + <!-- EmergencyPart --> + <BoxContainer Orientation="Vertical" + SeparationOverride="6"> + + <RichTextLabel Name="CountdownLabel"/> + + <Button Name="EmergencyShuttleButton" + Access="Public" + Text="Placeholder Text" + ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/> + </BoxContainer> + </BoxContainer> </BoxContainer> </controls:FancyWindow> diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs index 4d8dd86a4dc..56604ba526d 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs @@ -1,31 +1,40 @@ -using Content.Client.UserInterface.Controls; -using System.Threading; +using System.Globalization; +using Content.Client.UserInterface.Controls; using Content.Shared.CCVar; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; +using Robust.Shared.Timing; using Robust.Shared.Utility; -using Timer = Robust.Shared.Timing.Timer; namespace Content.Client.Communications.UI { [GenerateTypedNameReferences] public sealed partial class CommunicationsConsoleMenu : FancyWindow { - private CommunicationsConsoleBoundUserInterface Owner { get; set; } - private readonly CancellationTokenSource _timerCancelTokenSource = new(); - [Dependency] private readonly IConfigurationManager _cfg = default!; - - public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner) + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ILocalizationManager _loc = default!; + + public bool CanAnnounce; + public bool CanBroadcast; + public bool CanCall; + public bool AlertLevelSelectable; + public bool CountdownStarted; + public string CurrentLevel = string.Empty; + public TimeSpan? CountdownEnd; + + public event Action? OnEmergencyLevel; + public event Action<string>? OnAlertLevel; + public event Action<string>? OnAnnounce; + public event Action<string>? OnBroadcast; + + public CommunicationsConsoleMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - Owner = owner; - - var loc = IoCManager.Resolve<ILocalizationManager>(); - MessageInput.Placeholder = new Rope.Leaf(loc.GetString("comms-console-menu-announcement-placeholder")); + MessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder")); var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength); MessageInput.OnTextChanged += (args) => @@ -37,33 +46,38 @@ public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner) } else { - AnnounceButton.Disabled = !owner.CanAnnounce; + AnnounceButton.Disabled = !CanAnnounce; AnnounceButton.ToolTip = null; } }; - AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(Rope.Collapse(MessageInput.TextRope)); - AnnounceButton.Disabled = !owner.CanAnnounce; + AnnounceButton.OnPressed += _ => OnAnnounce?.Invoke(Rope.Collapse(MessageInput.TextRope)); + AnnounceButton.Disabled = !CanAnnounce; - BroadcastButton.OnPressed += (_) => Owner.BroadcastButtonPressed(Rope.Collapse(MessageInput.TextRope)); - BroadcastButton.Disabled = !owner.CanBroadcast; + BroadcastButton.OnPressed += _ => OnBroadcast?.Invoke(Rope.Collapse(MessageInput.TextRope)); + BroadcastButton.Disabled = !CanBroadcast; AlertLevelButton.OnItemSelected += args => { var metadata = AlertLevelButton.GetItemMetadata(args.Id); if (metadata != null && metadata is string cast) { - Owner.AlertLevelSelected(cast); + OnAlertLevel?.Invoke(cast); } }; - AlertLevelButton.Disabled = !owner.AlertLevelSelectable; - EmergencyShuttleButton.OnPressed += (_) => Owner.EmergencyShuttleButtonPressed(); - EmergencyShuttleButton.Disabled = !owner.CanCall; + AlertLevelButton.Disabled = !AlertLevelSelectable; + + EmergencyShuttleButton.OnPressed += _ => OnEmergencyLevel?.Invoke(); + EmergencyShuttleButton.Disabled = !CanCall; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); UpdateCountdown(); - Timer.SpawnRepeating(1000, UpdateCountdown, _timerCancelTokenSource.Token); } // The current alert could make levels unselectable, so we need to ensure that the UI reacts properly. @@ -105,30 +119,19 @@ public void UpdateAlertLevels(List<string>? alerts, string currentAlert) public void UpdateCountdown() { - if (!Owner.CountdownStarted) + if (!CountdownStarted) { - CountdownLabel.SetMessage(""); + CountdownLabel.SetMessage(string.Empty); EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-call-shuttle"); return; } - EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-recall-shuttle"); - CountdownLabel.SetMessage($"Time remaining\n{Owner.Countdown.ToString()}s"); - } - - public override void Close() - { - base.Close(); - - _timerCancelTokenSource.Cancel(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); + var diff = MathHelper.Max((CountdownEnd - _timing.CurTime) ?? TimeSpan.Zero, TimeSpan.Zero); - if (disposing) - _timerCancelTokenSource.Cancel(); + EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-recall-shuttle"); + var infoText = Loc.GetString($"comms-console-menu-time-remaining", + ("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture))); + CountdownLabel.SetMessage(infoText); } } } diff --git a/Content.Client/Computer/ComputerBoundUserInterface.cs b/Content.Client/Computer/ComputerBoundUserInterface.cs index bdbfe03fa10..11c26b252e9 100644 --- a/Content.Client/Computer/ComputerBoundUserInterface.cs +++ b/Content.Client/Computer/ComputerBoundUserInterface.cs @@ -1,4 +1,5 @@ using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.CustomControls; namespace Content.Client.Computer @@ -19,10 +20,8 @@ protected override void Open() { base.Open(); - _window = (TWindow) _dynamicTypeFactory.CreateInstance(typeof(TWindow)); + _window = this.CreateWindow<TWindow>(); _window.SetupComputerWindow(this); - _window.OnClose += Close; - _window.OpenCentered(); } // Alas, this constructor has to be copied to the subclass. :( @@ -42,16 +41,6 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.UpdateState((TState) state); } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } - protected override void ReceiveMessage(BoundUserInterfaceMessage message) { _window?.ReceiveMessage(message); diff --git a/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs b/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs index 4fea44f2253..e4966f1ec43 100644 --- a/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs +++ b/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using static Content.Shared.Configurable.ConfigurationComponent; namespace Content.Client.Configurable.UI @@ -9,9 +10,6 @@ public sealed class ConfigurationBoundUserInterface : BoundUserInterface [ViewVariables] private ConfigurationMenu? _menu; - [ViewVariables] - public Regex? Validation { get; internal set; } - public ConfigurationBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -19,10 +17,8 @@ public ConfigurationBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner protected override void Open() { base.Open(); - _menu = new ConfigurationMenu(this); - - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<ConfigurationMenu>(); + _menu.OnConfiguration += SendConfiguration; } protected override void UpdateState(BoundUserInterfaceState state) @@ -30,9 +26,7 @@ protected override void UpdateState(BoundUserInterfaceState state) base.UpdateState(state); if (state is not ConfigurationBoundUserInterfaceState configurationState) - { return; - } _menu?.Populate(configurationState); } @@ -41,9 +35,12 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) { base.ReceiveMessage(message); + if (_menu == null) + return; + if (message is ValidationUpdateMessage msg) { - Validation = new Regex(msg.ValidationString, RegexOptions.Compiled); + _menu.Validation = new Regex(msg.ValidationString, RegexOptions.Compiled); } } @@ -51,16 +48,5 @@ public void SendConfiguration(Dictionary<string, string> config) { SendMessage(new ConfigurationUpdatedMessage(config)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing && _menu != null) - { - _menu.OnClose -= Close; - _menu.Close(); - } - } } } diff --git a/Content.Client/Configurable/UI/ConfigurationMenu.cs b/Content.Client/Configurable/UI/ConfigurationMenu.cs index cc24af28692..29217eef7be 100644 --- a/Content.Client/Configurable/UI/ConfigurationMenu.cs +++ b/Content.Client/Configurable/UI/ConfigurationMenu.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Numerics; +using System.Text.RegularExpressions; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; @@ -13,23 +14,25 @@ namespace Content.Client.Configurable.UI { public sealed class ConfigurationMenu : DefaultWindow { - public ConfigurationBoundUserInterface Owner { get; } - private readonly BoxContainer _column; private readonly BoxContainer _row; private readonly List<(string name, LineEdit input)> _inputs; - public ConfigurationMenu(ConfigurationBoundUserInterface owner) + [ViewVariables] + public Regex? Validation { get; internal set; } + + public event Action<Dictionary<string, string>>? OnConfiguration; + + public ConfigurationMenu() { MinSize = SetSize = new Vector2(300, 250); - Owner = owner; _inputs = new List<(string name, LineEdit input)>(); Title = Loc.GetString("configuration-menu-device-title"); - BoxContainer baseContainer = new BoxContainer + var baseContainer = new BoxContainer { Orientation = LayoutOrientation.Vertical, VerticalExpand = true, @@ -116,14 +119,13 @@ public void Populate(ConfigurationBoundUserInterfaceState state) private void OnConfirm(ButtonEventArgs args) { var config = GenerateDictionary(_inputs, "Text"); - - Owner.SendConfiguration(config); + OnConfiguration?.Invoke(config); Close(); } private bool Validate(string value) { - return Owner.Validation == null || Owner.Validation.IsMatch(value); + return Validation?.IsMatch(value) != false; } private Dictionary<string, string> GenerateDictionary(IEnumerable<(string name, LineEdit input)> inputs, string propertyName) diff --git a/Content.Client/Construction/UI/FlatpackCreatorBoundUserInterface.cs b/Content.Client/Construction/UI/FlatpackCreatorBoundUserInterface.cs index 86f1b8b83c7..887492955e9 100644 --- a/Content.Client/Construction/UI/FlatpackCreatorBoundUserInterface.cs +++ b/Content.Client/Construction/UI/FlatpackCreatorBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Construction.Components; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Construction.UI { @@ -17,8 +18,8 @@ protected override void Open() { base.Open(); - _menu = new FlatpackCreatorMenu(Owner); - _menu.OnClose += Close; + _menu = this.CreateWindow<FlatpackCreatorMenu>(); + _menu.SetEntity(Owner); _menu.PackButtonPressed += () => { @@ -27,14 +28,5 @@ protected override void Open() _menu.OpenCentered(); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Dispose(); - } } } diff --git a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs index ad19bc30f42..f020991224c 100644 --- a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs +++ b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Client.Materials; +using Content.Client.Materials.UI; using Content.Client.Message; using Content.Client.UserInterface.Controls; using Content.Shared.Construction.Components; @@ -25,7 +26,7 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow private readonly MaterialStorageSystem _materialStorage; private readonly SpriteSystem _spriteSystem; - private readonly EntityUid _owner; + private EntityUid _owner; [ValidatePrototypeId<EntityPrototype>] public const string NoBoardEffectId = "FlatpackerNoBoardEffect"; @@ -35,7 +36,7 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow public event Action? PackButtonPressed; - public FlatpackCreatorMenu(EntityUid uid) + public FlatpackCreatorMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); @@ -45,14 +46,17 @@ public FlatpackCreatorMenu(EntityUid uid) _materialStorage = _entityManager.System<MaterialStorageSystem>(); _spriteSystem = _entityManager.System<SpriteSystem>(); - _owner = uid; - PackButton.OnPressed += _ => PackButtonPressed?.Invoke(); - MaterialStorageControl.SetOwner(uid); InsertLabel.SetMarkup(Loc.GetString("flatpacker-ui-insert-board")); } + public void SetEntity(EntityUid uid) + { + _owner = uid; + MaterialStorageControl.SetOwner(uid); + } + protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); diff --git a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs index e2c4d51ecd1..e5be0b1811f 100644 --- a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs +++ b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs @@ -2,12 +2,15 @@ using Content.Shared.Crayon; using Content.Shared.Decals; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.Crayon.UI { public sealed class CrayonBoundUserInterface : BoundUserInterface { + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [ViewVariables] private CrayonWindow? _menu; @@ -18,15 +21,29 @@ public CrayonBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey protected override void Open() { base.Open(); - _menu = new CrayonWindow(this); - - _menu.OnClose += Close; - var prototypeManager = IoCManager.Resolve<IPrototypeManager>(); - var crayonDecals = prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("crayon")); - _menu.Populate(crayonDecals); + _menu = this.CreateWindow<CrayonWindow>(); + _menu.OnColorSelected += SelectColor; + _menu.OnSelected += Select; + PopulateCrayons(); _menu.OpenCenteredLeft(); } + private void PopulateCrayons() + { + var crayonDecals = _protoManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("crayon")); + _menu?.Populate(crayonDecals); + } + + public override void OnProtoReload(PrototypesReloadedEventArgs args) + { + base.OnProtoReload(args); + + if (!args.WasModified<DecalPrototype>()) + return; + + PopulateCrayons(); + } + protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); @@ -43,16 +60,5 @@ public void SelectColor(Color color) { SendMessage(new CrayonColorMessage(color)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _menu?.Close(); - _menu = null; - } - } } } diff --git a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs index 2a5801ccf2d..b97786cd41a 100644 --- a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs +++ b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs @@ -18,18 +18,17 @@ namespace Content.Client.Crayon.UI [GenerateTypedNameReferences] public sealed partial class CrayonWindow : DefaultWindow { - public CrayonBoundUserInterface Owner { get; } - private Dictionary<string, Texture>? _decals; private string? _selected; private Color _color; - public CrayonWindow(CrayonBoundUserInterface owner) + public event Action<Color>? OnColorSelected; + public event Action<string>? OnSelected; + + public CrayonWindow() { RobustXamlLoader.Load(this); - Owner = owner; - Search.OnTextChanged += _ => RefreshList(); ColorSelector.OnColorChanged += SelectColor; } @@ -38,16 +37,16 @@ private void SelectColor(Color color) { _color = color; - Owner.SelectColor(color); - + OnColorSelected?.Invoke(color); RefreshList(); } private void RefreshList() { // Clear - Grid.RemoveAllChildren(); - if (_decals == null) return; + Grid.DisposeAllChildren(); + if (_decals == null) + return; var filter = Search.Text; foreach (var (decal, tex) in _decals) @@ -89,7 +88,6 @@ private void ButtonOnPressed(ButtonEventArgs obj) { if (obj.Button.Name == null) return; - Owner.Select(obj.Button.Name); _selected = obj.Button.Name; RefreshList(); } diff --git a/Content.Client/CrewManifest/CrewManifestSystem.cs b/Content.Client/CrewManifest/CrewManifestSystem.cs index d05acb56802..abf359aad87 100644 --- a/Content.Client/CrewManifest/CrewManifestSystem.cs +++ b/Content.Client/CrewManifest/CrewManifestSystem.cs @@ -40,6 +40,7 @@ private void BuildDepartmentLookup() { _jobDepartmentLookup.Clear(); _departments.Clear(); + foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>()) { _departments.Add(department.ID); @@ -52,25 +53,9 @@ private void BuildDepartmentLookup() _jobDepartmentLookup.Add(department.Roles[i - 1], departments); } - departments.Add(department.ID, i); + if (department.Roles.Count == i) + departments.Add(department.ID, i); } } } - - public int GetDepartmentOrder(string department, string jobPrototype) - { - if (!Departments.Contains(department)) - { - return -1; - } - - if (!_jobDepartmentLookup.TryGetValue(jobPrototype, out var departments)) - { - return -1; - } - - return departments.TryGetValue(department, out var order) - ? order - : -1; - } } diff --git a/Content.Client/CrewManifest/CrewManifestUi.xaml.cs b/Content.Client/CrewManifest/CrewManifestUi.xaml.cs index 4183c908141..f07e54eb65b 100644 --- a/Content.Client/CrewManifest/CrewManifestUi.xaml.cs +++ b/Content.Client/CrewManifest/CrewManifestUi.xaml.cs @@ -1,3 +1,4 @@ +using Content.Client.CrewManifest.UI; using Content.Shared.CrewManifest; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; diff --git a/Content.Client/CrewManifest/UI/CrewManifestSection.cs b/Content.Client/CrewManifest/UI/CrewManifestSection.cs index 4b1b02cb755..29cf850e2b9 100644 --- a/Content.Client/CrewManifest/UI/CrewManifestSection.cs +++ b/Content.Client/CrewManifest/UI/CrewManifestSection.cs @@ -51,7 +51,7 @@ public CrewManifestSection( title.SetMessage(entry.JobTitle); - if (prototypeManager.TryIndex<StatusIconPrototype>(entry.JobIcon, out var jobIcon)) + if (prototypeManager.TryIndex<JobIconPrototype>(entry.JobIcon, out var jobIcon)) { var icon = new TextureRect() { diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/EditChatPopup.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/EditChatPopup.xaml new file mode 100644 index 00000000000..76a8513796b --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/EditChatPopup.xaml @@ -0,0 +1,53 @@ +<DefaultWindow xmlns="https://spacestation14.io" + Title="{Loc nano-chat-new-title}" + MinSize="300 200"> + <PanelContainer StyleClasses="AngleRect"> + <BoxContainer Orientation="Vertical" Margin="4"> + <!-- Number input --> + <BoxContainer Orientation="Vertical" Margin="0 4"> + <Label Text="{Loc nano-chat-number-label}" + StyleClasses="LabelHeading" /> + <PanelContainer StyleClasses="ButtonSquare"> + <LineEdit Name="NumberInput" + PlaceHolder="{Loc nano-chat-number-placeholder}" + Editable="False"/> + </PanelContainer> + </BoxContainer> + + <!-- Name input --> + <BoxContainer Orientation="Vertical" Margin="0 4"> + <Label Text="{Loc nano-chat-name-label}" + StyleClasses="LabelHeading" /> + <PanelContainer StyleClasses="ButtonSquare"> + <LineEdit Name="NameInput" + PlaceHolder="{Loc nano-chat-name-placeholder}" /> + </PanelContainer> + </BoxContainer> + + <!-- Job input (optional) --> + <BoxContainer Orientation="Vertical" Margin="0 4"> + <Label Text="{Loc nano-chat-job-label}" + StyleClasses="LabelHeading" /> + <PanelContainer StyleClasses="ButtonSquare"> + <LineEdit Name="JobInput" + PlaceHolder="{Loc nano-chat-job-placeholder}" /> + </PanelContainer> + </BoxContainer> + + <!-- Action buttons --> + <BoxContainer Orientation="Horizontal" + HorizontalAlignment="Right" + Margin="0 8 0 0"> + <Button Name="CancelButton" + Text="{Loc nano-chat-cancel}" + StyleClasses="OpenRight" + MinSize="80 0" /> + <Button Name="CreateButton" + Text="{Loc nano-chat-create}" + StyleClasses="OpenLeft" + MinSize="80 0" + Disabled="True" /> + </BoxContainer> + </BoxContainer> + </PanelContainer> +</DefaultWindow> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/EditChatPopup.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/EditChatPopup.xaml.cs new file mode 100644 index 00000000000..5ab492bb530 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/EditChatPopup.xaml.cs @@ -0,0 +1,89 @@ +using System.Linq; +using Content.Client.DeltaV.NanoChat; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class EditChatPopup : DefaultWindow +{ + private const int MaxInputLength = 16; + private const int MaxNumberLength = 4; + + public event Action<uint, string, string?>? OnContactEdited; + + public EditChatPopup() + { + RobustXamlLoader.Load(this); + + // margins trolling + ContentsContainer.Margin = new Thickness(3); + + // Button handlers + CancelButton.OnPressed += _ => Close(); + CreateButton.OnPressed += _ => EditChat(); + + // Input validation + NameInput.OnTextChanged += _ => ValidateInputs(); + + NameInput.OnTextChanged += args => + { + if (args.Text.Length > MaxInputLength) + NameInput.Text = args.Text[..MaxInputLength]; + ValidateInputs(); + }; + + JobInput.OnTextChanged += args => + { + if (args.Text.Length > MaxInputLength) + JobInput.Text = args.Text[..MaxInputLength]; + }; + + NumberInput.OnTextChanged += args => + { + if (args.Text.Length > MaxNumberLength) + NumberInput.Text = args.Text[..MaxNumberLength]; + + // Filter to digits only + var newText = string.Concat(NumberInput.Text.Where(char.IsDigit)); + if (newText != NumberInput.Text) + NumberInput.Text = newText; + + ValidateInputs(); + }; + } + + private void ValidateInputs() + { + var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) && + !string.IsNullOrWhiteSpace(NameInput.Text) && + uint.TryParse(NumberInput.Text, out _); + + CreateButton.Disabled = !isValid; + } + + private void EditChat() + { + if (!uint.TryParse(NumberInput.Text, out var number)) + return; + + var name = NameInput.Text.Trim(); + var job = string.IsNullOrWhiteSpace(JobInput.Text) ? null : JobInput.Text.Trim(); + + OnContactEdited?.Invoke(number, name, job); + Close(); + } + + public void ClearInputs() + { + NameInput.Text = string.Empty; + JobInput.Text = string.Empty; + ValidateInputs(); + } + + public void SetNumberInput(string newNumber) => NumberInput.Text = newNumber; + public void SetNameInput(string newName) => NameInput.Text = newName; + public void SetJobInput(string newJob) => JobInput.Text = newJob; +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml new file mode 100644 index 00000000000..0b136133624 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml @@ -0,0 +1,48 @@ +<BoxContainer + xmlns="https://spacestation14.io" + xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" + HorizontalExpand="True"> + <Button Name="ChatButton" + StyleClasses="ButtonSquare" + HorizontalExpand="True" + MaxSize="137 64" + Margin="0 1"> + <BoxContainer Orientation="Horizontal" + HorizontalExpand="True" + VerticalExpand="True" + MinWidth="132" + Margin="6 4" + VerticalAlignment="Center"> + <!-- Unread indicator dot --> + <PanelContainer Name="UnreadIndicator" + MinSize="8 8" + MaxSize="8 8" + VerticalAlignment="Center" + Margin="0 0 6 0"> + <PanelContainer.PanelOverride> + <graphics:StyleBoxFlat + BackgroundColor="#17c622" + BorderColor="#0f7a15" /> + </PanelContainer.PanelOverride> + </PanelContainer> + + <!-- Text container --> + <BoxContainer Orientation="Vertical" + HorizontalExpand="True" + VerticalExpand="True" + VerticalAlignment="Center"> + <RichTextLabel Name="NameLabel" + StyleClasses="LabelHeading" + HorizontalExpand="True" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Margin="0 -2 0 0" /> + <Label Name="JobLabel" + StyleClasses="LabelSubText" + HorizontalExpand="True" + ClipText="False" + HorizontalAlignment="Center" /> + </BoxContainer> + </BoxContainer> + </Button> +</BoxContainer> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs new file mode 100644 index 00000000000..ff4ea9ba9c1 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs @@ -0,0 +1,39 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatEntry : BoxContainer +{ + public event Action<uint>? OnPressed; + private uint _number; + private Action<EventArgs>? _pressHandler; + + public NanoChatEntry() + { + RobustXamlLoader.Load(this); + } + + public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected) + { + // Remove old handler if it exists + if (_pressHandler != null) + ChatButton.OnPressed -= _pressHandler; + + _number = number; + + // Create and store new handler + _pressHandler = _ => OnPressed?.Invoke(_number); + ChatButton.OnPressed += _pressHandler; + + NameLabel.Text = recipient.Name; + JobLabel.Text = recipient.JobTitle ?? ""; + JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle); + UnreadIndicator.Visible = recipient.HasUnread; + + ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null; + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml new file mode 100644 index 00000000000..c87478d6301 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml @@ -0,0 +1,21 @@ +<BoxContainer xmlns="https://spacestation14.io" + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls" + Margin="4" + Orientation="Vertical"> + <BoxContainer Orientation="Horizontal"> + <Label Name="NumberLabel" + Align="Right" + SetWidth="26" + ClipText="True" /> + <Label Name="TimeLabel" + Align="Center" + SetWidth="100" + ClipText="True" /> + <Label Name="MessageLabel" + Align="Left" + MinWidth="390" + HorizontalExpand="True" + ClipText="False" /> + </BoxContainer> + <customControls:HSeparator Margin="0 5 0 5" /> +</BoxContainer> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs new file mode 100644 index 00000000000..b94ea1a18aa --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs @@ -0,0 +1,17 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatLogEntry : BoxContainer +{ + public NanoChatLogEntry(int number, string time, string message) + { + RobustXamlLoader.Load(this); + NumberLabel.Text = number.ToString(); + TimeLabel.Text = time; + MessageLabel.Text = message; + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml new file mode 100644 index 00000000000..84daa2f1c59 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml @@ -0,0 +1,55 @@ +<cartridges:NanoChatMessageBubble + xmlns="https://spacestation14.io" + xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges" + xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" + HorizontalExpand="True"> + + <BoxContainer Name="MessageContainer" + Orientation="Horizontal" + HorizontalExpand="True"> + <!-- Left spacer for other's messages --> + <Control Name="LeftSpacer" + MinSize="12 0" /> + + <!-- Message panel --> + <BoxContainer Name="MessageBox" + Orientation="Vertical" + MaxWidth="320" + HorizontalExpand="True"> + <PanelContainer Name="MessagePanel" + MaxWidth="320" + HorizontalExpand="True"> + <PanelContainer.PanelOverride> + <graphics:StyleBoxFlat + ContentMarginLeftOverride="10" + ContentMarginRightOverride="10" + ContentMarginTopOverride="6" + ContentMarginBottomOverride="6" + BorderThickness="1"> + <!-- Colors set in code based on message sender --> + </graphics:StyleBoxFlat> + </PanelContainer.PanelOverride> + + <RichTextLabel Name="MessageText" + HorizontalExpand="True" /> + </PanelContainer> + + <!-- Delivery failed text --> + <Label Name="DeliveryFailedLabel" + Text="{Loc nano-chat-delivery-failed}" + StyleClasses="LabelSmall" + HorizontalExpand="True" + HorizontalAlignment="Right" + Margin="10 2 10 0" + Visible="False" /> + </BoxContainer> + + <!-- Right spacer for own messages --> + <Control Name="RightSpacer" + MinSize="12 0" /> + + <!-- Flexible space for alignment --> + <Control Name="FlexSpace" + HorizontalExpand="True" /> + </BoxContainer> +</cartridges:NanoChatMessageBubble> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs new file mode 100644 index 00000000000..42725bb09c5 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs @@ -0,0 +1,62 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatMessageBubble : BoxContainer +{ + public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green + public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray + public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border + public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white + public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red + + public NanoChatMessageBubble() + { + RobustXamlLoader.Load(this); + } + + public void SetMessage(NanoChatMessage message, bool isOwnMessage) + { + if (MessagePanel.PanelOverride is not StyleBoxFlat) + return; + + // Configure message appearance + var style = (StyleBoxFlat)MessagePanel.PanelOverride; + style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor; + style.BorderColor = BorderColor; + + // Set message content + MessageText.Text = message.Content; + MessageText.Modulate = TextColor; + + // Show delivery failed text if needed (only for own messages) + DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed; + if (DeliveryFailedLabel.Visible) + DeliveryFailedLabel.Modulate = ErrorColor; + + // For own messages: FlexSpace -> MessagePanel -> RightSpacer + // For other messages: LeftSpacer -> MessagePanel -> FlexSpace + MessageContainer.RemoveAllChildren(); + + // fuuuuuck + MessageBox.Parent?.RemoveChild(MessageBox); + + if (isOwnMessage) + { + MessageContainer.AddChild(FlexSpace); + MessageContainer.AddChild(MessageBox); + MessageContainer.AddChild(RightSpacer); + } + else + { + MessageContainer.AddChild(LeftSpacer); + MessageContainer.AddChild(MessageBox); + MessageContainer.AddChild(FlexSpace); + } + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs new file mode 100644 index 00000000000..9529ff65e18 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs @@ -0,0 +1,43 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class NanoChatUi : UIFragment +{ + private NanoChatUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new(); + + _fragment.ActionSendUiMessage += (type, number, content, job) => + { + SendNanoChatUiMessage(type, number, content, job, userInterface); + }; + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is NanoChatUiState cast) + _fragment?.UpdateState(cast); + } + + private static void SendNanoChatUiMessage(NanoChatUiMessageType type, + uint? number, + string? content, + string? job, + BoundUserInterface userInterface) + { + var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job); + var message = new CartridgeUiMessage(nanoChatMessage); + userInterface.SendMessage(message); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml new file mode 100644 index 00000000000..8fe07cee94c --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml @@ -0,0 +1,175 @@ +<cartridges:NanoChatUiFragment + xmlns="https://spacestation14.io" + xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges" + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" + Orientation="Vertical" + HorizontalExpand="True" + VerticalExpand="True" + Margin="5"> + + <!-- Main container that fills the entire PDA screen --> + <BoxContainer Orientation="Vertical" + HorizontalExpand="True" + VerticalExpand="True"> + <!-- Header with app title and new chat button --> + <controls:StripeBack MinSize="48 48" + VerticalExpand="False"> + <BoxContainer Orientation="Horizontal" + HorizontalExpand="True" + Margin="0"> + <TextureRect Name="AppIcon" + TexturePath="/Textures/Interface/Nano/ntlogo.svg.png" + Stretch="KeepAspectCentered" + VerticalAlignment="Center" + MinSize="32 32" + Margin="8 0 0 0" /> + <Label Text="{Loc nano-chat-title}" + StyleClasses="LabelHeading" + HorizontalExpand="True" + Margin="8 0" + VerticalAlignment="Center" /> + <Label Name="OwnNumberLabel" + Text="#0000" + StyleClasses="LabelSubText" + VerticalAlignment="Center" + Margin="0 0 8 0" /> + <Button Name="EditChatButton" + Text="E" + MaxSize="32 32" + Visible="False" + StyleClasses="OpenBoth" + Margin="0 0 4 0" + ToolTip="{Loc nano-chat-edit}"> + </Button> + <Button Name="DeleteChatButton" + MaxSize="32 32" + Visible="False" + StyleClasses="OpenBoth" + Margin="0 0 4 0" + ToolTip="{Loc nano-chat-delete}"> + <TextureRect StyleClasses="ButtonSquare" + TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png" + Stretch="KeepAspectCentered" + MinSize="18 18" /> + </Button> + <Button Name="MuteButton" + MaxSize="32 32" + StyleClasses="OpenBoth" + Margin="0 0 4 0" + ToolTip="{Loc nano-chat-toggle-mute}"> + <Control HorizontalExpand="True" VerticalExpand="True"> + <TextureRect Name="BellIcon" + StyleClasses="ButtonSquare" + TexturePath="/Textures/DeltaV/Interface/VerbIcons/bell.svg.png" + Stretch="KeepAspectCentered" + MinSize="18 18" /> + <TextureRect Name="BellMutedIcon" + StyleClasses="ButtonSquare" + TexturePath="/Textures/DeltaV/Interface/VerbIcons/bell_muted.png" + Stretch="KeepAspectCentered" + Visible="False" + MinSize="18 18" /> + </Control> + </Button> + <Button Name="NewChatButton" + Text="+" + MinSize="32 32" + MaxSize="32 32" + Margin="0 0 4 0" + StyleClasses="OpenBoth" + ToolTip="{Loc nano-chat-new-chat}" /> + </BoxContainer> + </controls:StripeBack> + + <!-- Main content split --> + <BoxContainer Orientation="Horizontal" + VerticalExpand="True" + HorizontalExpand="True" + Margin="0 5 0 0"> + <!-- Left panel: Chat list --> + <PanelContainer StyleClasses="AngleRect" + VerticalExpand="True" + MaxWidth="150"> + <ScrollContainer VerticalExpand="True" + MinWidth="145" + HorizontalExpand="True" + HScrollEnabled="False"> + <BoxContainer Name="ChatList" + Orientation="Vertical" + VerticalExpand="True" + HorizontalExpand="True" + Margin="4"> + <!-- Chat entries will be added here dynamically --> + <Label Name="NoChatsLabel" + Text="{Loc nano-chat-no-chats}" + HorizontalAlignment="Center" + VerticalAlignment="Center" + StyleClasses="LabelSubText" /> + </BoxContainer> + </ScrollContainer> + </PanelContainer> + + <customControls:VSeparator Margin="3 0" /> + + <!-- Right panel: Current chat --> + <PanelContainer StyleClasses="AngleRect" + VerticalExpand="True" + HorizontalExpand="True"> + <BoxContainer Orientation="Vertical" + VerticalExpand="True" + HorizontalExpand="True"> + <!-- Messages area with centered "select chat" label --> + <BoxContainer Name="MessageArea" + Orientation="Vertical" + VerticalExpand="True" + HorizontalExpand="True" + Margin="0 0 0 4"> + <Label Name="CurrentChatName" + Text="{Loc nano-chat-select-chat}" + HorizontalAlignment="Center" + VerticalAlignment="Center" + VerticalExpand="True" /> + <ScrollContainer Name="MessagesScroll" + VerticalExpand="True" + HorizontalExpand="True" + Visible="False"> + <BoxContainer Name="MessageList" + Orientation="Vertical" + VerticalExpand="True" + HorizontalExpand="True" /> + </ScrollContainer> + </BoxContainer> + + <!-- Message input --> + <BoxContainer Name="MessageInputContainer" + Orientation="Horizontal" + HorizontalExpand="True" + Margin="0 4 0 0" + Visible="False"> + <!-- Character count --> + <Label Name="CharacterCount" + HorizontalAlignment="Right" + StyleClasses="LabelSubText" + Margin="0 0 4 2" + Visible="False" /> + <!-- Input row --> + <LineEdit Name="MessageInput" + PlaceHolder="{Loc nano-chat-message-placeholder}" + HorizontalExpand="True" + StyleClasses="OpenRight" /> + <Button Name="SendButton" + MinSize="32 32" + Disabled="True" + StyleClasses="OpenLeft" + Margin="4 0 0 0"> + <TextureRect StyleClasses="ButtonSquare" + TexturePath="/Textures/Interface/Nano/triangle_right.png" + Stretch="KeepAspectCentered" /> + </Button> + </BoxContainer> + </BoxContainer> + </PanelContainer> + </BoxContainer> + </BoxContainer> +</cartridges:NanoChatUiFragment> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs new file mode 100644 index 00000000000..3b2342e974d --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs @@ -0,0 +1,289 @@ +using System.Linq; +using System.Numerics; +using Content.Client.DeltaV.NanoChat; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.NanoChat; +using Content.Shared.PDA; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Timing; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatUiFragment : BoxContainer +{ + [Dependency] private readonly IGameTiming _timing = default!; + + private const int MaxMessageLength = 256; + + private readonly NewChatPopup _newChatPopup; + private readonly EditChatPopup _editChatPopup; + private uint? _currentChat; + private uint? _pendingChat; + private uint _ownNumber; + private bool _notificationsMuted; + private Dictionary<uint, NanoChatRecipient> _recipients = new(); + private Dictionary<uint, List<NanoChatMessage>> _messages = new(); + + public event Action<NanoChatUiMessageType, uint?, string?, string?>? ActionSendUiMessage; + + public NanoChatUiFragment() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + _newChatPopup = new(); + _editChatPopup = new(); + SetupEventHandlers(); + } + + private void SetupEventHandlers() + { + _newChatPopup.OnChatCreated += (number, name, job) => + { + ActionSendUiMessage?.Invoke(NanoChatUiMessageType.NewChat, number, name, job); + }; + + _editChatPopup.OnContactEdited += (number, name, job) => + { + DeleteCurrentChat(); + ActionSendUiMessage?.Invoke(NanoChatUiMessageType.NewChat, number, name, job); + }; + + NewChatButton.OnPressed += _ => + { + _newChatPopup.ClearInputs(); + _newChatPopup.OpenCentered(); + }; + + MuteButton.OnPressed += _ => + { + _notificationsMuted = !_notificationsMuted; + UpdateMuteButton(); + ActionSendUiMessage?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null); + }; + + MessageInput.OnTextChanged += args => + { + var length = args.Text.Length; + var isValid = !string.IsNullOrWhiteSpace(args.Text) && + length <= MaxMessageLength && + (_currentChat != null || _pendingChat != null); + + SendButton.Disabled = !isValid; + + // Show character count when over limit + CharacterCount.Visible = length > MaxMessageLength; + if (length > MaxMessageLength) + { + CharacterCount.Text = Loc.GetString("nano-chat-message-too-long", + ("current", length), + ("max", MaxMessageLength)); + CharacterCount.StyleClasses.Add("LabelDanger"); + } + }; + + MessageInput.OnTextEntered += _ => SendMessage(); + SendButton.OnPressed += _ => SendMessage(); + EditChatButton.OnPressed += _ => BeginEditChat(); + DeleteChatButton.OnPressed += _ => DeleteCurrentChat(); + } + + private void SendMessage() + { + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text)) + return; + + var messageContent = MessageInput.Text; + + // Add predicted message + var predictedMessage = new NanoChatMessage( + _timing.CurTime, + messageContent, + _ownNumber + ); + + if (!_messages.TryGetValue(activeChat.Value, out var value)) + { + value = new(); + _messages[activeChat.Value] = value; + } + + value.Add(predictedMessage); + + // Update UI with predicted message + UpdateMessages(_messages); + + // Send message event + ActionSendUiMessage?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null); + + // Clear input + MessageInput.Text = string.Empty; + SendButton.Disabled = true; + } + + private void SelectChat(uint number) + { + // Don't reselect the same chat + if (_currentChat == number && _pendingChat == null) + return; + + _pendingChat = number; + + // Predict marking messages as read + if (_recipients.TryGetValue(number, out var recipient)) + { + recipient.HasUnread = false; + _recipients[number] = recipient; + UpdateChatList(_recipients); + } + + ActionSendUiMessage?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null); + UpdateCurrentChat(); + } + + private void DeleteCurrentChat() + { + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null) + return; + + ActionSendUiMessage?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null); + } + + private void BeginEditChat() + { + if (_currentChat == null) + return; + + var recipient = _recipients[_currentChat ?? 0]; + + _editChatPopup.ClearInputs(); + _editChatPopup.SetNumberInput(recipient.Number.ToString()); + _editChatPopup.SetNameInput(recipient.Name); + _editChatPopup.SetJobInput(recipient.JobTitle ?? string.Empty); + _editChatPopup.OpenCentered(); + } + + private void UpdateChatList(Dictionary<uint, NanoChatRecipient> recipients) + { + ChatList.RemoveAllChildren(); + _recipients = recipients; + + NoChatsLabel.Visible = recipients.Count == 0; + if (NoChatsLabel.Parent != ChatList) + { + NoChatsLabel.Parent?.RemoveChild(NoChatsLabel); + ChatList.AddChild(NoChatsLabel); + } + + foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name)) + { + // For pending chat selection, always show it as selected even if unconfirmed + var entry = new NanoChatEntry(); + var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number); + + entry.SetRecipient(recipient, number, isSelected); + entry.OnPressed += SelectChat; + + ChatList.AddChild(entry); + } + } + + private void UpdateCurrentChat() + { + var activeChat = _pendingChat ?? _currentChat; + var hasActiveChat = activeChat != null; + + // Update UI state + MessagesScroll.Visible = hasActiveChat; + CurrentChatName.Visible = !hasActiveChat; + MessageInputContainer.Visible = hasActiveChat; + DeleteChatButton.Visible = hasActiveChat; + EditChatButton.Visible = hasActiveChat; + DeleteChatButton.Disabled = !hasActiveChat; + + if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient)) + { + CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})"); + } + else + { + CurrentChatName.Text = Loc.GetString("nano-chat-select-chat"); + } + + // Scroll to bottom after messages are added + if (MessageList.Parent is ScrollContainer scroll) + scroll.SetScrollValue(new Vector2(0, float.MaxValue)); + } + + private void UpdateMessages(Dictionary<uint, List<NanoChatMessage>> messages) + { + _messages = messages; + MessageList.RemoveAllChildren(); + + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages)) + return; + + foreach (var message in chatMessages) + { + var messageBubble = new NanoChatMessageBubble(); + messageBubble.SetMessage(message, message.SenderId == _ownNumber); + MessageList.AddChild(messageBubble); + + // Add spacing between messages + MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) }); + } + + MessageList.InvalidateMeasure(); + MessagesScroll.InvalidateMeasure(); + + // Scroll to bottom after messages are added + if (MessageList.Parent is ScrollContainer scroll) + scroll.SetScrollValue(new Vector2(0, float.MaxValue)); + } + + private void UpdateMuteButton() + { + if (BellMutedIcon != null) + BellMutedIcon.Visible = _notificationsMuted; + } + + public void UpdateState(NanoChatUiState state) + { + _ownNumber = state.OwnNumber; + _notificationsMuted = state.NotificationsMuted; + OwnNumberLabel.Text = $"#{state.OwnNumber:D4}"; + UpdateMuteButton(); + + // Update new chat button state based on recipient limit + var atLimit = state.Recipients.Count >= state.MaxRecipients; + NewChatButton.Disabled = atLimit; + NewChatButton.ToolTip = atLimit + ? Loc.GetString("nano-chat-max-recipients") + : Loc.GetString("nano-chat-new-chat"); + + // First handle pending chat resolution if we have one + if (_pendingChat != null) + { + if (_pendingChat == state.CurrentChat) + _currentChat = _pendingChat; // Server confirmed our selection + + _pendingChat = null; // Clear pending either way + } + + // No pending chat or it was just cleared, update current directly + if (_pendingChat == null) + _currentChat = state.CurrentChat; + + UpdateCurrentChat(); + UpdateChatList(state.Recipients); + UpdateMessages(state.Messages); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml new file mode 100644 index 00000000000..20095c4fce9 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml @@ -0,0 +1,52 @@ +<DefaultWindow xmlns="https://spacestation14.io" + Title="{Loc nano-chat-new-title}" + MinSize="300 200"> + <PanelContainer StyleClasses="AngleRect"> + <BoxContainer Orientation="Vertical" Margin="4"> + <!-- Number input --> + <BoxContainer Orientation="Vertical" Margin="0 4"> + <Label Text="{Loc nano-chat-number-label}" + StyleClasses="LabelHeading" /> + <PanelContainer StyleClasses="ButtonSquare"> + <LineEdit Name="NumberInput" + PlaceHolder="{Loc nano-chat-number-placeholder}" /> + </PanelContainer> + </BoxContainer> + + <!-- Name input --> + <BoxContainer Orientation="Vertical" Margin="0 4"> + <Label Text="{Loc nano-chat-name-label}" + StyleClasses="LabelHeading" /> + <PanelContainer StyleClasses="ButtonSquare"> + <LineEdit Name="NameInput" + PlaceHolder="{Loc nano-chat-name-placeholder}" /> + </PanelContainer> + </BoxContainer> + + <!-- Job input (optional) --> + <BoxContainer Orientation="Vertical" Margin="0 4"> + <Label Text="{Loc nano-chat-job-label}" + StyleClasses="LabelHeading" /> + <PanelContainer StyleClasses="ButtonSquare"> + <LineEdit Name="JobInput" + PlaceHolder="{Loc nano-chat-job-placeholder}" /> + </PanelContainer> + </BoxContainer> + + <!-- Action buttons --> + <BoxContainer Orientation="Horizontal" + HorizontalAlignment="Right" + Margin="0 8 0 0"> + <Button Name="CancelButton" + Text="{Loc nano-chat-cancel}" + StyleClasses="OpenRight" + MinSize="80 0" /> + <Button Name="CreateButton" + Text="{Loc nano-chat-create}" + StyleClasses="OpenLeft" + MinSize="80 0" + Disabled="True" /> + </BoxContainer> + </BoxContainer> + </PanelContainer> +</DefaultWindow> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs new file mode 100644 index 00000000000..8e47e1ee5d4 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs @@ -0,0 +1,87 @@ +using System.Linq; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NewChatPopup : DefaultWindow +{ + private const int MaxInputLength = 16; + private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer + + public event Action<uint, string, string?>? OnChatCreated; + + public NewChatPopup() + { + RobustXamlLoader.Load(this); + + // margins trolling + ContentsContainer.Margin = new Thickness(3); + + // Button handlers + CancelButton.OnPressed += _ => Close(); + CreateButton.OnPressed += _ => CreateChat(); + + // Input validation + NumberInput.OnTextChanged += _ => ValidateInputs(); + NameInput.OnTextChanged += _ => ValidateInputs(); + + // Input validation + NumberInput.OnTextChanged += args => + { + if (args.Text.Length > MaxNumberLength) + NumberInput.Text = args.Text[..MaxNumberLength]; + + // Filter to digits only + var newText = string.Concat(NumberInput.Text.Where(char.IsDigit)); + if (newText != NumberInput.Text) + NumberInput.Text = newText; + + ValidateInputs(); + }; + + NameInput.OnTextChanged += args => + { + if (args.Text.Length > MaxInputLength) + NameInput.Text = args.Text[..MaxInputLength]; + ValidateInputs(); + }; + + JobInput.OnTextChanged += args => + { + if (args.Text.Length > MaxInputLength) + JobInput.Text = args.Text[..MaxInputLength]; + }; + } + + private void ValidateInputs() + { + var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) && + !string.IsNullOrWhiteSpace(NameInput.Text) && + uint.TryParse(NumberInput.Text, out _); + + CreateButton.Disabled = !isValid; + } + + private void CreateChat() + { + if (!uint.TryParse(NumberInput.Text, out var number)) + return; + + var name = NameInput.Text.Trim(); + var job = string.IsNullOrWhiteSpace(JobInput.Text) ? null : JobInput.Text.Trim(); + + OnChatCreated?.Invoke(number, name, job); + Close(); + } + + public void ClearInputs() + { + NumberInput.Text = string.Empty; + NameInput.Text = string.Empty; + JobInput.Text = string.Empty; + ValidateInputs(); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml new file mode 100644 index 00000000000..058bde07e9c --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml @@ -0,0 +1,25 @@ +<cartridges:PriceHistoryTable + xmlns="https://spacestation14.io" + xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges" + Orientation="Vertical" + HorizontalExpand="True" + Margin="0,5,0,0"> + + <!-- Header --> + <BoxContainer Orientation="Horizontal" HorizontalExpand="True"> + <Label Text="{Loc stock-trading-price-history}" + HorizontalExpand="True" + StyleClasses="LabelSubText" /> + </BoxContainer> + + <!-- Price history panel --> + <PanelContainer Name="HistoryPanel" + HorizontalExpand="True" + Margin="0,2,0,0"> + <BoxContainer Orientation="Horizontal" + HorizontalExpand="True" + HorizontalAlignment="Center"> + <GridContainer Name="PriceGrid" Columns="5" /> + </BoxContainer> + </PanelContainer> +</cartridges:PriceHistoryTable> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs new file mode 100644 index 00000000000..f5798f44c42 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs @@ -0,0 +1,75 @@ +using System.Linq; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class PriceHistoryTable : BoxContainer +{ + public PriceHistoryTable() + { + RobustXamlLoader.Load(this); + + // Create the stylebox here so we can use the colors from StockTradingUi + var styleBox = new StyleBoxFlat + { + BackgroundColor = StockTradingUiFragment.PriceBackgroundColor, + ContentMarginLeftOverride = 6, + ContentMarginRightOverride = 6, + ContentMarginTopOverride = 4, + ContentMarginBottomOverride = 4, + BorderColor = StockTradingUiFragment.BorderColor, + BorderThickness = new Thickness(1), + }; + + HistoryPanel.PanelOverride = styleBox; + } + + public void Update(List<float> priceHistory) + { + PriceGrid.RemoveAllChildren(); + + // Take last 5 prices + var lastFivePrices = priceHistory.TakeLast(5).ToList(); + + for (var i = 0; i < lastFivePrices.Count; i++) + { + var price = lastFivePrices[i]; + var previousPrice = i > 0 ? lastFivePrices[i - 1] : price; + var priceChange = ((price - previousPrice) / previousPrice) * 100; + + var entryContainer = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + MinWidth = 80, + HorizontalAlignment = HAlignment.Center, + }; + + var priceLabel = new Label + { + Text = $"${price:F2}", + HorizontalAlignment = HAlignment.Center, + }; + + var changeLabel = new Label + { + Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%", + HorizontalAlignment = HAlignment.Center, + StyleClasses = { "LabelSubText" }, + Modulate = priceChange switch + { + > 0 => StockTradingUiFragment.PositiveColor, + < 0 => StockTradingUiFragment.NegativeColor, + _ => StockTradingUiFragment.NeutralColor, + } + }; + + entryContainer.AddChild(priceLabel); + entryContainer.AddChild(changeLabel); + PriceGrid.AddChild(entryContainer); + } + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUi.cs new file mode 100644 index 00000000000..45704ee2349 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUi.cs @@ -0,0 +1,45 @@ +using Robust.Client.UserInterface; +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class StockTradingUi : UIFragment +{ + private StockTradingUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new StockTradingUiFragment(); + + _fragment.OnBuyButtonPressed += (company, amount) => + { + SendStockTradingUiMessage(StockTradingUiAction.Buy, company, amount, userInterface); + }; + _fragment.OnSellButtonPressed += (company, amount) => + { + SendStockTradingUiMessage(StockTradingUiAction.Sell, company, amount, userInterface); + }; + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is StockTradingUiState cast) + { + _fragment?.UpdateState(cast); + } + } + + private static void SendStockTradingUiMessage(StockTradingUiAction action, int company, float amount, BoundUserInterface userInterface) + { + var newsMessage = new StockTradingUiMessageEvent(action, company, amount); + var message = new CartridgeUiMessage(newsMessage); + userInterface.SendMessage(message); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml new file mode 100644 index 00000000000..00b45584cc4 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml @@ -0,0 +1,44 @@ +<cartridges:StockTradingUiFragment + xmlns="https://spacestation14.io" + xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges" + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls" + Margin="5" + VerticalExpand="True"> + + <!-- A parent container to hold the balance label and main content --> + <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True"> + <!-- Header section with balance --> + <PanelContainer StyleClasses="AngleRect"> + <BoxContainer Orientation="Horizontal" HorizontalExpand="True"> + <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0,0,5,0"> + <Label Text="{Loc stock-trading-title}" + HorizontalExpand="True" + HorizontalAlignment="Left" /> + </BoxContainer> + <Label Name="Balance" + Text="{Loc stock-trading-balance}" + HorizontalAlignment="Right" /> + </BoxContainer> + </PanelContainer> + + <!-- Horizontal line under header --> + <customControls:HSeparator Margin="5 3 5 5"/> + + <!-- Main content --> + <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True"> + <Label Name="NoEntries" + Text="{Loc stock-trading-no-entries}" + HorizontalExpand="True" + HorizontalAlignment="Center" + Visible="False" /> + <ScrollContainer HorizontalExpand="True" + VerticalExpand="True" + Margin="0,5,0,0"> + <BoxContainer Name="Entries" + Orientation="Vertical" + VerticalAlignment="Top" + HorizontalExpand="True" /> + </ScrollContainer> + </BoxContainer> + </BoxContainer> +</cartridges:StockTradingUiFragment> diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs new file mode 100644 index 00000000000..b44e8f44c70 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs @@ -0,0 +1,269 @@ +using System.Linq; +using Content.Client.Administration.UI.CustomControls; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class StockTradingUiFragment : BoxContainer +{ + private readonly Dictionary<int, CompanyEntry> _companyEntries = new(); + + // Event handlers for the parent UI + public event Action<int, float>? OnBuyButtonPressed; + public event Action<int, float>? OnSellButtonPressed; + + // Define colors + public static readonly Color PositiveColor = Color.FromHex("#00ff00"); // Green + public static readonly Color NegativeColor = Color.FromHex("#ff0000"); // Red + public static readonly Color NeutralColor = Color.FromHex("#ffffff"); // White + public static readonly Color BackgroundColor = Color.FromHex("#25252a"); // Dark grey + public static readonly Color PriceBackgroundColor = Color.FromHex("#1a1a1a"); // Darker grey + public static readonly Color BorderColor = Color.FromHex("#404040"); // Light grey + + public StockTradingUiFragment() + { + RobustXamlLoader.Load(this); + } + + public void UpdateState(StockTradingUiState state) + { + NoEntries.Visible = state.Entries.Count == 0; + Balance.Text = Loc.GetString("stock-trading-balance", ("balance", state.Balance)); + + // Clear all existing entries + foreach (var entry in _companyEntries.Values) + { + entry.Container.RemoveAllChildren(); + } + _companyEntries.Clear(); + Entries.RemoveAllChildren(); + + // Add new entries + for (var i = 0; i < state.Entries.Count; i++) + { + var company = state.Entries[i]; + var entry = new CompanyEntry(i, company.LocalizedDisplayName, OnBuyButtonPressed, OnSellButtonPressed); + _companyEntries[i] = entry; + Entries.AddChild(entry.Container); + + var ownedStocks = state.OwnedStocks.GetValueOrDefault(i, 0); + entry.Update(company, ownedStocks); + } + } + + private sealed class CompanyEntry + { + public readonly BoxContainer Container; + private readonly Label _nameLabel; + private readonly Label _priceLabel; + private readonly Label _changeLabel; + private readonly Button _sellButton; + private readonly Button _buyButton; + private readonly Label _sharesLabel; + private readonly LineEdit _amountEdit; + private readonly PriceHistoryTable _priceHistory; + + public CompanyEntry(int companyIndex, + string displayName, + Action<int, float>? onBuyPressed, + Action<int, float>? onSellPressed) + { + Container = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + Margin = new Thickness(0, 0, 0, 2), + }; + + // Company info panel + var companyPanel = new PanelContainer(); + + var mainContent = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + Margin = new Thickness(8), + }; + + // Top row with company name and price info + var topRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + _nameLabel = new Label + { + HorizontalExpand = true, + Text = displayName, + }; + + // Create a panel for price and change + var pricePanel = new PanelContainer + { + HorizontalAlignment = HAlignment.Right, + }; + + // Style the price panel + var priceStyleBox = new StyleBoxFlat + { + BackgroundColor = BackgroundColor, + ContentMarginLeftOverride = 8, + ContentMarginRightOverride = 8, + ContentMarginTopOverride = 4, + ContentMarginBottomOverride = 4, + BorderColor = BorderColor, + BorderThickness = new Thickness(1), + }; + + pricePanel.PanelOverride = priceStyleBox; + + // Container for price and change labels + var priceContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + }; + + _priceLabel = new Label(); + + _changeLabel = new Label + { + HorizontalAlignment = HAlignment.Right, + Modulate = NeutralColor, + Margin = new Thickness(15, 0, 0, 0), + }; + + priceContainer.AddChild(_priceLabel); + priceContainer.AddChild(_changeLabel); + pricePanel.AddChild(priceContainer); + + topRow.AddChild(_nameLabel); + topRow.AddChild(pricePanel); + + // Add the top row + mainContent.AddChild(topRow); + + // Add the price history table between top and bottom rows + _priceHistory = new PriceHistoryTable(); + mainContent.AddChild(_priceHistory); + + // Trading controls (bottom row) + var bottomRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + Margin = new Thickness(0, 5, 0, 0), + }; + + _sharesLabel = new Label + { + Text = Loc.GetString("stock-trading-owned-shares"), + MinWidth = 100, + }; + + _amountEdit = new LineEdit + { + PlaceHolder = Loc.GetString("stock-trading-amount-placeholder"), + HorizontalExpand = true, + MinWidth = 80, + }; + + var buttonContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalAlignment = HAlignment.Right, + MinWidth = 140, + }; + + _buyButton = new Button + { + Text = Loc.GetString("stock-trading-buy-button"), + MinWidth = 65, + Margin = new Thickness(3, 0, 3, 0), + }; + + _sellButton = new Button + { + Text = Loc.GetString("stock-trading-sell-button"), + MinWidth = 65, + }; + + buttonContainer.AddChild(_buyButton); + buttonContainer.AddChild(_sellButton); + + bottomRow.AddChild(_sharesLabel); + bottomRow.AddChild(_amountEdit); + bottomRow.AddChild(buttonContainer); + + // Add the bottom row last + mainContent.AddChild(bottomRow); + + companyPanel.AddChild(mainContent); + Container.AddChild(companyPanel); + + // Add horizontal separator after the panel + var separator = new HSeparator + { + Margin = new Thickness(5, 3, 5, 5), + }; + Container.AddChild(separator); + + // Button click events + _buyButton.OnPressed += _ => + { + if (float.TryParse(_amountEdit.Text, out var amount) && amount > 0) + onBuyPressed?.Invoke(companyIndex, amount); + }; + + _sellButton.OnPressed += _ => + { + if (float.TryParse(_amountEdit.Text, out var amount) && amount > 0) + onSellPressed?.Invoke(companyIndex, amount); + }; + + // There has to be a better way of doing this + _amountEdit.OnTextChanged += args => + { + var newText = string.Concat(args.Text.Where(char.IsDigit)); + if (newText != args.Text) + _amountEdit.Text = newText; + }; + } + + public void Update(StockCompanyStruct company, int ownedStocks) + { + _nameLabel.Text = company.LocalizedDisplayName; + _priceLabel.Text = $"${company.CurrentPrice:F2}"; + _sharesLabel.Text = Loc.GetString("stock-trading-owned-shares", ("shares", ownedStocks)); + + var priceChange = 0f; + if (company.PriceHistory is { Count: > 0 }) + { + var previousPrice = company.PriceHistory[^1]; + priceChange = (company.CurrentPrice - previousPrice) / previousPrice * 100; + } + + _changeLabel.Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%"; + + // Update color based on price change + _changeLabel.Modulate = priceChange switch + { + > 0 => PositiveColor, + < 0 => NegativeColor, + _ => NeutralColor, + }; + + // Update the price history table if not null + if (company.PriceHistory != null) + _priceHistory.Update(company.PriceHistory); + + // Disable sell button if no shares owned + _sellButton.Disabled = ownedStocks <= 0; + } + } +} diff --git a/Content.Client/DeltaV/NanoChat/NanoChatSystem.cs b/Content.Client/DeltaV/NanoChat/NanoChatSystem.cs new file mode 100644 index 00000000000..242deb05b72 --- /dev/null +++ b/Content.Client/DeltaV/NanoChat/NanoChatSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.DeltaV.NanoChat; + +namespace Content.Client.DeltaV.NanoChat; + +public sealed class NanoChatSystem : SharedNanoChatSystem; diff --git a/Content.Client/DeltaV/VendingMachines/ShopVendorSystem.cs b/Content.Client/DeltaV/VendingMachines/ShopVendorSystem.cs new file mode 100644 index 00000000000..3b7f5744421 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/ShopVendorSystem.cs @@ -0,0 +1,123 @@ +using Content.Shared.DeltaV.VendingMachines; +using Content.Shared.VendingMachines; +using Robust.Client.Animations; +using Robust.Client.GameObjects; + +namespace Content.Client.DeltaV.VendingMachines; + +public sealed class ShopVendorSystem : SharedShopVendorSystem +{ + [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ShopVendorComponent, AppearanceChangeEvent>(OnAppearanceChange); + SubscribeLocalEvent<ShopVendorComponent, AnimationCompletedEvent>(OnAnimationCompleted); + } + + // copied from vending machines because its not reusable in other systems :) + private void OnAnimationCompleted(Entity<ShopVendorComponent> ent, ref AnimationCompletedEvent args) + { + UpdateAppearance((ent, ent.Comp)); + } + + private void OnAppearanceChange(Entity<ShopVendorComponent> ent, ref AppearanceChangeEvent args) + { + UpdateAppearance((ent, ent.Comp, args.Sprite)); + } + + private void UpdateAppearance(Entity<ShopVendorComponent, SpriteComponent?> ent) + { + if (!Resolve(ent, ref ent.Comp2)) + return; + + if (!_appearance.TryGetData<VendingMachineVisualState>(ent, VendingMachineVisuals.VisualState, out var state)) + state = VendingMachineVisualState.Normal; + + var sprite = ent.Comp2; + SetLayerState(VendingMachineVisualLayers.Base, ent.Comp1.OffState, sprite); + SetLayerState(VendingMachineVisualLayers.Screen, ent.Comp1.ScreenState, sprite); + switch (state) + { + case VendingMachineVisualState.Normal: + SetLayerState(VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.NormalState, sprite); + break; + + case VendingMachineVisualState.Deny: + if (ent.Comp1.LoopDenyAnimation) + SetLayerState(VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.DenyState, sprite); + else + PlayAnimation(ent, VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.DenyState, ent.Comp1.DenyDelay, sprite); + break; + + case VendingMachineVisualState.Eject: + PlayAnimation(ent, VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.EjectState, ent.Comp1.EjectDelay, sprite); + break; + + case VendingMachineVisualState.Broken: + HideLayers(sprite); + SetLayerState(VendingMachineVisualLayers.Base, ent.Comp1.BrokenState, sprite); + break; + + case VendingMachineVisualState.Off: + HideLayers(sprite); + break; + } + } + + private static void SetLayerState(VendingMachineVisualLayers layer, string? state, SpriteComponent sprite) + { + if (state == null) + return; + + sprite.LayerSetVisible(layer, true); + sprite.LayerSetAutoAnimated(layer, true); + sprite.LayerSetState(layer, state); + } + + private void PlayAnimation(EntityUid uid, VendingMachineVisualLayers layer, string? state, TimeSpan time, SpriteComponent sprite) + { + if (state == null || _animationPlayer.HasRunningAnimation(uid, state)) + return; + + var animation = GetAnimation(layer, state, time); + sprite.LayerSetVisible(layer, true); + _animationPlayer.Play(uid, animation, state); + } + + private static Animation GetAnimation(VendingMachineVisualLayers layer, string state, TimeSpan time) + { + return new Animation + { + Length = time, + AnimationTracks = + { + new AnimationTrackSpriteFlick + { + LayerKey = layer, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(state, 0f) + } + } + } + }; + } + + private static void HideLayers(SpriteComponent sprite) + { + HideLayer(VendingMachineVisualLayers.BaseUnshaded, sprite); + HideLayer(VendingMachineVisualLayers.Screen, sprite); + } + + private static void HideLayer(VendingMachineVisualLayers layer, SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(layer, out var actualLayer)) + return; + + sprite.LayerSetVisible(actualLayer, false); + } +} diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorBoundUserInterface.cs b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorBoundUserInterface.cs new file mode 100644 index 00000000000..6122aa9ee1e --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorBoundUserInterface.cs @@ -0,0 +1,25 @@ +using Content.Shared.DeltaV.VendingMachines; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.VendingMachines.UI; + +public sealed class ShopVendorBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private ShopVendorWindow? _window; + + public ShopVendorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow<ShopVendorWindow>(); + _window.SetEntity(Owner); + _window.OpenCenteredLeft(); + _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; + _window.OnItemSelected += index => SendMessage(new ShopVendorPurchaseMessage(index)); + } +} diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml new file mode 100644 index 00000000000..4708db20aa9 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml @@ -0,0 +1,13 @@ +<BoxContainer xmlns="https://spacestation14.io" + Orientation="Horizontal" + HorizontalExpand="True" + SeparationOverride="4"> + <EntityPrototypeView + Name="ItemPrototype" + Margin="4 0 0 0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + MinSize="32 32"/> + <Label Name="NameLabel" SizeFlagsStretchRatio="3" HorizontalExpand="True" ClipText="True"/> + <Label Name="CostLabel" SizeFlagsStretchRatio="3" HorizontalAlignment="Right" Margin="8 0"/> +</BoxContainer> diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml.cs b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml.cs new file mode 100644 index 00000000000..4a3c9c4efe5 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml.cs @@ -0,0 +1,21 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client.DeltaV.VendingMachines.UI; + +[GenerateTypedNameReferences] +public sealed partial class ShopVendorItem : BoxContainer +{ + public ShopVendorItem(EntProtoId entProto, string text, uint cost) + { + RobustXamlLoader.Load(this); + + ItemPrototype.SetPrototype(entProto); + + NameLabel.Text = text; + + CostLabel.Text = cost.ToString(); + } +} diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml new file mode 100644 index 00000000000..cfc7f4a4a2c --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml @@ -0,0 +1,24 @@ +<controls:FancyWindow + xmlns="https://spacestation14.io" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" + MinHeight="210"> + <BoxContainer Name="MainContainer" Orientation="Vertical"> + <BoxContainer Orientation="Horizontal"> + <LineEdit Name="SearchBar" PlaceHolder="{Loc 'vending-machine-component-search-filter'}" HorizontalExpand="True" Margin="4 4"/> + <Label Name="BalanceLabel" Margin="4 4"/> + </BoxContainer> + <controls:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/> + <!-- Footer --> + <BoxContainer Orientation="Vertical"> + <PanelContainer StyleClasses="LowDivider" /> + <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom"> + <Label Text="{Loc 'shop-vendor-flavor-left'}" StyleClasses="WindowFooterText" /> + <Label Text="{Loc 'shop-vendor-flavor-right'}" StyleClasses="WindowFooterText" + HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" /> + <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered" + VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/> + </BoxContainer> + </BoxContainer> + </BoxContainer> +</controls:FancyWindow> diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml.cs b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml.cs new file mode 100644 index 00000000000..2b9c4df87a0 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml.cs @@ -0,0 +1,147 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.DeltaV.VendingMachines; +using Content.Shared.Stacks; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using System.Numerics; + +namespace Content.Client.DeltaV.VendingMachines.UI; + +[GenerateTypedNameReferences] +public sealed partial class ShopVendorWindow : FancyWindow +{ + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly IEntityManager _entMan = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + private readonly ShopVendorSystem _vendor; + + /// <summary> + /// Event fired with the listing index to purchase. + /// </summary> + public event Action<int>? OnItemSelected; + + private EntityUid _owner; + private readonly StyleBoxFlat _style = new() { BackgroundColor = new Color(70, 73, 102) }; + private readonly StyleBoxFlat _styleBroke = new() { BackgroundColor = Color.FromHex("#303133") }; + private readonly List<ListContainerButton> _buttons = new(); + private uint _balance = 1; + + public ShopVendorWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _vendor = _entMan.System<ShopVendorSystem>(); + + VendingContents.SearchBar = SearchBar; + VendingContents.DataFilterCondition += DataFilterCondition; + VendingContents.GenerateItem += GenerateButton; + VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(((ShopVendorListingData) data).Index); + } + + public void SetEntity(EntityUid owner) + { + _owner = owner; + + if (!_entMan.TryGetComponent<ShopVendorComponent>(owner, out var comp)) + return; + + var pack = _proto.Index(comp.Pack); + Populate(pack.Listings); + + UpdateBalance(); + } + + private void UpdateBalance(uint balance) + { + if (_balance == balance) + return; + + _balance = balance; + + BalanceLabel.Text = Loc.GetString("shop-vendor-balance", ("points", balance)); + + // disable items that are too expensive to buy + foreach (var button in _buttons) + { + if (button.Data is ShopVendorListingData data) + button.Disabled = data.Cost > balance; + + button.StyleBoxOverride = button.Disabled ? _styleBroke : _style; + } + } + + private void UpdateBalance() + { + if (_player.LocalEntity is {} user) + UpdateBalance(_vendor.GetBalance(_owner, user)); + } + + private bool DataFilterCondition(string filter, ListData data) + { + if (data is not ShopVendorListingData { Text: var text }) + return false; + + if (string.IsNullOrEmpty(filter)) + return true; + + return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase); + } + + private void GenerateButton(ListData data, ListContainerButton button) + { + if (data is not ShopVendorListingData cast) + return; + + _buttons.Add(button); + button.AddChild(new ShopVendorItem(cast.ItemId, cast.Text, cast.Cost)); + + button.ToolTip = cast.Text; + button.Disabled = cast.Cost > _balance; + button.StyleBoxOverride = button.Disabled ? _styleBroke : _style; + } + + public void Populate(List<ShopListing> listings) + { + var longestEntry = string.Empty; + var listData = new List<ShopVendorListingData>(); + for (var i = 0; i < listings.Count; i++) + { + var listing = listings[i]; + var proto = _proto.Index(listing.Id); + var text = proto.Name; + if (proto.TryGetComponent<StackComponent>(out var stack, _factory) && stack.Count > 1) + { + text += " "; + text += Loc.GetString("shop-vendor-stack-suffix", ("count", stack.Count)); + } + listData.Add(new ShopVendorListingData(i, listing.Id, text, listing.Cost)); + } + + _buttons.Clear(); + VendingContents.PopulateList(listData); + SetSizeAfterUpdate(longestEntry.Length, listings.Count); + } + + private void SetSizeAfterUpdate(int longestEntryLength, int contentCount) + { + SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400), + Math.Clamp(contentCount * 50, 150, 350)); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + UpdateBalance(); + } +} + +public record ShopVendorListingData(int Index, EntProtoId ItemId, string Text, uint Cost) : ListData; diff --git a/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs b/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs index e8e77217ea5..296e71d3a95 100644 --- a/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs +++ b/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent; namespace Content.Client.Disposal.UI @@ -21,20 +22,16 @@ protected override void Open() { base.Open(); - _window = new DisposalRouterWindow(); - - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<DisposalRouterWindow>(); _window.Confirm.OnPressed += _ => ButtonPressed(UiAction.Ok, _window.TagInput.Text); _window.TagInput.OnTextEntered += args => ButtonPressed(UiAction.Ok, args.Text); - } private void ButtonPressed(UiAction action, string tag) { SendMessage(new UiActionMessage(action, tag)); - _window?.Close(); + Close(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -48,18 +45,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.UpdateState(cast); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } - - } - } diff --git a/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs b/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs index 3aeed8dc802..7fc0eb85401 100644 --- a/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs +++ b/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent; namespace Content.Client.Disposal.UI @@ -21,20 +22,17 @@ protected override void Open() { base.Open(); - _window = new DisposalTaggerWindow(); - - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<DisposalTaggerWindow>(); _window.Confirm.OnPressed += _ => ButtonPressed(UiAction.Ok, _window.TagInput.Text); _window.TagInput.OnTextEntered += args => ButtonPressed(UiAction.Ok, args.Text); - } private void ButtonPressed(UiAction action, string tag) { + // TODO: This looks copy-pasted with the other mailing stuff... SendMessage(new UiActionMessage(action, tag)); - _window?.Close(); + Close(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -48,18 +46,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.UpdateState(cast); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } - - } - } diff --git a/Content.Client/Doors/Electronics/DoorElectronicsBoundUserInterface.cs b/Content.Client/Doors/Electronics/DoorElectronicsBoundUserInterface.cs index cd7ea717ce3..9b7e23c03aa 100644 --- a/Content.Client/Doors/Electronics/DoorElectronicsBoundUserInterface.cs +++ b/Content.Client/Doors/Electronics/DoorElectronicsBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Access; using Content.Shared.Doors.Electronics; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.Doors.Electronics; @@ -18,6 +19,23 @@ public DoorElectronicsBoundUserInterface(EntityUid owner, Enum uiKey) : base(own protected override void Open() { base.Open(); + _window = this.CreateWindow<DoorElectronicsConfigurationMenu>(); + _window.OnAccessChanged += UpdateConfiguration; + Reset(); + } + + public override void OnProtoReload(PrototypesReloadedEventArgs args) + { + base.OnProtoReload(args); + + if (!args.WasModified<AccessLevelPrototype>()) + return; + + Reset(); + } + + private void Reset() + { List<ProtoId<AccessLevelPrototype>> accessLevels = new(); foreach (var accessLevel in _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>()) @@ -29,10 +47,7 @@ protected override void Open() } accessLevels.Sort(); - - _window = new DoorElectronicsConfigurationMenu(this, accessLevels, _prototypeManager); - _window.OnClose += Close; - _window.OpenCentered(); + _window?.Reset(_prototypeManager, accessLevels); } protected override void UpdateState(BoundUserInterfaceState state) @@ -44,14 +59,6 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.UpdateState(castState); } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - - _window?.Dispose(); - } - public void UpdateConfiguration(List<ProtoId<AccessLevelPrototype>> newAccessList) { SendMessage(new DoorElectronicsUpdateConfigurationMessage(newAccessList)); diff --git a/Content.Client/Doors/Electronics/DoorElectronicsConfigurationMenu.xaml.cs b/Content.Client/Doors/Electronics/DoorElectronicsConfigurationMenu.xaml.cs index c01f13a462e..2112a562971 100644 --- a/Content.Client/Doors/Electronics/DoorElectronicsConfigurationMenu.xaml.cs +++ b/Content.Client/Doors/Electronics/DoorElectronicsConfigurationMenu.xaml.cs @@ -15,22 +15,23 @@ namespace Content.Client.Doors.Electronics; [GenerateTypedNameReferences] public sealed partial class DoorElectronicsConfigurationMenu : FancyWindow { - private readonly DoorElectronicsBoundUserInterface _owner; - private AccessLevelControl _buttonsList = new(); + private readonly AccessLevelControl _buttonsList = new(); - public DoorElectronicsConfigurationMenu(DoorElectronicsBoundUserInterface ui, List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager) + public event Action<List<ProtoId<AccessLevelPrototype>>>? OnAccessChanged; + + public DoorElectronicsConfigurationMenu() { RobustXamlLoader.Load(this); - - _owner = ui; - - _buttonsList.Populate(accessLevels, prototypeManager); AccessLevelControlContainer.AddChild(_buttonsList); + } + + public void Reset(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels) + { + _buttonsList.Populate(accessLevels, protoManager); - foreach (var (id, button) in _buttonsList.ButtonsList) + foreach (var button in _buttonsList.ButtonsList.Values) { - button.OnPressed += _ => _owner.UpdateConfiguration( - _buttonsList.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList()); + button.OnPressed += _ => OnAccessChanged?.Invoke(_buttonsList.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList()); } } diff --git a/Content.Client/Drowsiness/DrowsinessOverlay.cs b/Content.Client/Drowsiness/DrowsinessOverlay.cs new file mode 100644 index 00000000000..a316f31ae64 --- /dev/null +++ b/Content.Client/Drowsiness/DrowsinessOverlay.cs @@ -0,0 +1,80 @@ +using Content.Shared.Drowsiness; +using Content.Shared.StatusEffect; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Drowsiness; + +public sealed class DrowsinessOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IEntitySystemManager _sysMan = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + private readonly ShaderInstance _drowsinessShader; + + public float CurrentPower = 0.0f; + + private const float PowerDivisor = 250.0f; + private const float Intensity = 0.2f; // for adjusting the visual scale + private float _visualScale = 0; // between 0 and 1 + + public DrowsinessOverlay() + { + IoCManager.InjectDependencies(this); + _drowsinessShader = _prototypeManager.Index<ShaderPrototype>("Drowsiness").InstanceUnique(); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + var playerEntity = _playerManager.LocalEntity; + + if (playerEntity == null) + return; + + if (!_entityManager.HasComponent<DrowsinessComponent>(playerEntity) + || !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status)) + return; + + var statusSys = _sysMan.GetEntitySystem<StatusEffectsSystem>(); + if (!statusSys.TryGetTime(playerEntity.Value, SharedDrowsinessSystem.DrowsinessKey, out var time, status)) + return; + + var curTime = _timing.CurTime; + var timeLeft = (float)(time.Value.Item2 - curTime).TotalSeconds; + + CurrentPower += 8f * (0.5f * timeLeft - CurrentPower) * args.DeltaSeconds / (timeLeft + 1); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp)) + return false; + + if (args.Viewport.Eye != eyeComp.Eye) + return false; + + _visualScale = Math.Clamp(CurrentPower / PowerDivisor, 0.0f, 1.0f); + return _visualScale > 0; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + var handle = args.WorldHandle; + _drowsinessShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _drowsinessShader.SetParameter("Strength", _visualScale * Intensity); + handle.UseShader(_drowsinessShader); + handle.DrawRect(args.WorldBounds, Color.White); + handle.UseShader(null); + } +} diff --git a/Content.Client/Drowsiness/DrowsinessSystem.cs b/Content.Client/Drowsiness/DrowsinessSystem.cs new file mode 100644 index 00000000000..bc8862b19d2 --- /dev/null +++ b/Content.Client/Drowsiness/DrowsinessSystem.cs @@ -0,0 +1,53 @@ +using Content.Shared.Drowsiness; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.Drowsiness; + +public sealed class DrowsinessSystem : SharedDrowsinessSystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private DrowsinessOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<DrowsinessComponent, ComponentInit>(OnDrowsinessInit); + SubscribeLocalEvent<DrowsinessComponent, ComponentShutdown>(OnDrowsinessShutdown); + + SubscribeLocalEvent<DrowsinessComponent, LocalPlayerAttachedEvent>(OnPlayerAttached); + SubscribeLocalEvent<DrowsinessComponent, LocalPlayerDetachedEvent>(OnPlayerDetached); + + _overlay = new(); + } + + private void OnPlayerAttached(EntityUid uid, DrowsinessComponent component, LocalPlayerAttachedEvent args) + { + _overlayMan.AddOverlay(_overlay); + } + + private void OnPlayerDetached(EntityUid uid, DrowsinessComponent component, LocalPlayerDetachedEvent args) + { + _overlay.CurrentPower = 0; + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnDrowsinessInit(EntityUid uid, DrowsinessComponent component, ComponentInit args) + { + if (_player.LocalEntity == uid) + _overlayMan.AddOverlay(_overlay); + } + + private void OnDrowsinessShutdown(EntityUid uid, DrowsinessComponent component, ComponentShutdown args) + { + if (_player.LocalEntity == uid) + { + _overlay.CurrentPower = 0; + _overlayMan.RemoveOverlay(_overlay); + } + } +} diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index bf5f021be3d..05d3454f48f 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -102,15 +102,14 @@ public override void Init() _prototypeManager.RegisterIgnore("seed"); // Seeds prototypes are server-only. _prototypeManager.RegisterIgnore("objective"); _prototypeManager.RegisterIgnore("holiday"); - _prototypeManager.RegisterIgnore("aiFaction"); _prototypeManager.RegisterIgnore("htnCompound"); _prototypeManager.RegisterIgnore("htnPrimitive"); _prototypeManager.RegisterIgnore("gameMap"); _prototypeManager.RegisterIgnore("gameMapPool"); - _prototypeManager.RegisterIgnore("npcFaction"); _prototypeManager.RegisterIgnore("lobbyBackground"); _prototypeManager.RegisterIgnore("gamePreset"); _prototypeManager.RegisterIgnore("noiseChannel"); + _prototypeManager.RegisterIgnore("playerConnectionWhitelist"); _prototypeManager.RegisterIgnore("spaceBiome"); _prototypeManager.RegisterIgnore("worldgenConfig"); _prototypeManager.RegisterIgnore("gameRule"); diff --git a/Content.Client/Fax/UI/FaxBoundUi.cs b/Content.Client/Fax/UI/FaxBoundUi.cs index a95066a3b58..ca2e834b4fe 100644 --- a/Content.Client/Fax/UI/FaxBoundUi.cs +++ b/Content.Client/Fax/UI/FaxBoundUi.cs @@ -25,10 +25,7 @@ protected override void Open() { base.Open(); - _window = new FaxWindow(); - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<FaxWindow>(); _window.FileButtonPressed += OnFileButtonPressed; _window.CopyButtonPressed += OnCopyButtonPressed; _window.SendButtonPressed += OnSendButtonPressed; @@ -104,11 +101,4 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.UpdateState(cast); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - _window?.Dispose(); - } } diff --git a/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs b/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs index ba49f11ea0f..08596b04e6e 100644 --- a/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs +++ b/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs @@ -1,6 +1,7 @@ using Robust.Client.GameObjects; using Robust.Shared.Timing; using Content.Shared.Forensics; +using Robust.Client.UserInterface; namespace Content.Client.Forensics { @@ -21,11 +22,9 @@ public ForensicScannerBoundUserInterface(EntityUid owner, Enum uiKey) : base(own protected override void Open() { base.Open(); - _window = new ForensicScannerMenu(); - _window.OnClose += Close; + _window = this.CreateWindow<ForensicScannerMenu>(); _window.Print.OnPressed += _ => Print(); _window.Clear.OnPressed += _ => Clear(); - _window.OpenCentered(); } private void Print() @@ -62,6 +61,7 @@ protected override void UpdateState(BoundUserInterfaceState state) _printCooldown = cast.PrintCooldown; + // TODO: Fix this if (cast.PrintReadyAt > _gameTiming.CurTime) Timer.Spawn(cast.PrintReadyAt - _gameTiming.CurTime, () => { @@ -71,14 +71,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.UpdateState(cast); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _window?.Dispose(); - } } } diff --git a/Content.Client/GPS/Components/HandheldGPSComponent.cs b/Content.Client/GPS/Components/HandheldGPSComponent.cs deleted file mode 100644 index 0f5271fd80c..00000000000 --- a/Content.Client/GPS/Components/HandheldGPSComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Shared.GPS; - -namespace Content.Client.GPS.Components -{ - [RegisterComponent] - public sealed partial class HandheldGPSComponent : SharedHandheldGPSComponent - { - } -} diff --git a/Content.Client/GPS/Systems/HandheldGpsSystem.cs b/Content.Client/GPS/Systems/HandheldGpsSystem.cs index cc2b888da37..3f38a3b6952 100644 --- a/Content.Client/GPS/Systems/HandheldGpsSystem.cs +++ b/Content.Client/GPS/Systems/HandheldGpsSystem.cs @@ -1,4 +1,4 @@ -using Content.Client.GPS.Components; +using Content.Shared.GPS.Components; using Content.Client.GPS.UI; using Content.Client.Items; diff --git a/Content.Client/GPS/UI/HandheldGpsStatusControl.cs b/Content.Client/GPS/UI/HandheldGpsStatusControl.cs index de6a1031bad..57645e386e7 100644 --- a/Content.Client/GPS/UI/HandheldGpsStatusControl.cs +++ b/Content.Client/GPS/UI/HandheldGpsStatusControl.cs @@ -1,6 +1,7 @@ -using Content.Client.GPS.Components; +using Content.Shared.GPS.Components; using Content.Client.Message; using Content.Client.Stylesheets; +using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; @@ -13,11 +14,13 @@ public sealed class HandheldGpsStatusControl : Control private readonly RichTextLabel _label; private float _updateDif; private readonly IEntityManager _entMan; + private readonly SharedTransformSystem _transform; public HandheldGpsStatusControl(Entity<HandheldGPSComponent> parent) { _parent = parent; _entMan = IoCManager.Resolve<IEntityManager>(); + _transform = _entMan.System<TransformSystem>(); _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; AddChild(_label); UpdateGpsDetails(); @@ -27,6 +30,13 @@ protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); + // don't display the label if the gps component is being removed + if (_parent.Comp.LifeStage > ComponentLifeStage.Running) + { + _label.Visible = false; + return; + } + _updateDif += args.DeltaSeconds; if (_updateDif < _parent.Comp.UpdateRate) return; @@ -41,9 +51,9 @@ private void UpdateGpsDetails() var posText = "Error"; if (_entMan.TryGetComponent(_parent, out TransformComponent? transComp)) { - var pos = transComp.MapPosition; - var x = (int) pos.X; - var y = (int) pos.Y; + var pos = _transform.GetMapCoordinates(_parent.Owner, xform: transComp); + var x = (int)pos.X; + var y = (int)pos.Y; posText = $"({x}, {y})"; } _label.SetMarkup(Loc.GetString("handheld-gps-coordinates-title", ("coordinates", posText))); diff --git a/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs b/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs index fdb3cdbc010..457b70ca7ca 100644 --- a/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs +++ b/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Gateway; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Gateway.UI; @@ -17,24 +18,13 @@ protected override void Open() { base.Open(); - _window = new GatewayWindow(EntMan.GetNetEntity(Owner)); + _window = this.CreateWindow<GatewayWindow>(); + _window.SetEntity(EntMan.GetNetEntity(Owner)); _window.OpenPortal += destination => { SendMessage(new GatewayOpenPortalMessage(destination)); }; - _window.OnClose += Close; - _window?.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - _window?.Dispose(); - _window = null; - } } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Gateway/UI/GatewayWindow.xaml.cs b/Content.Client/Gateway/UI/GatewayWindow.xaml.cs index b3e1333a7a9..0cc03a36149 100644 --- a/Content.Client/Gateway/UI/GatewayWindow.xaml.cs +++ b/Content.Client/Gateway/UI/GatewayWindow.xaml.cs @@ -22,7 +22,7 @@ public sealed partial class GatewayWindow : FancyWindow, public event Action<NetEntity>? OpenPortal; private List<GatewayDestinationData> _destinations = new(); - public readonly NetEntity Owner; + public NetEntity Owner; private NetEntity? _current; private TimeSpan _nextReady; @@ -46,16 +46,20 @@ public sealed partial class GatewayWindow : FancyWindow, /// </summary> private bool _isCooldownPending = true; - public GatewayWindow(NetEntity netEntity) + public GatewayWindow() { RobustXamlLoader.Load(this); var dependencies = IoCManager.Instance!; _timing = dependencies.Resolve<IGameTiming>(); - Owner = netEntity; NextUnlockBar.ForegroundStyleBoxOverride = new StyleBoxFlat(Color.FromHex("#C74EBD")); } + public void SetEntity(NetEntity entity) + { + + } + public void UpdateState(GatewayBoundUserInterfaceState state) { _destinations = state.Destinations; diff --git a/Content.Client/Gravity/GravitySystem.cs b/Content.Client/Gravity/GravitySystem.cs index 3e87f76ba2b..dd51436a1f5 100644 --- a/Content.Client/Gravity/GravitySystem.cs +++ b/Content.Client/Gravity/GravitySystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Gravity; +using Content.Shared.Power; using Robust.Client.GameObjects; namespace Content.Client.Gravity; @@ -21,7 +22,7 @@ private void OnAppearanceChange(EntityUid uid, SharedGravityGeneratorComponent c if (args.Sprite == null) return; - if (_appearanceSystem.TryGetData<GravityGeneratorStatus>(uid, GravityGeneratorVisuals.State, out var state, args.Component)) + if (_appearanceSystem.TryGetData<PowerChargeStatus>(uid, PowerChargeVisuals.State, out var state, args.Component)) { if (comp.SpriteMap.TryGetValue(state, out var spriteState)) { @@ -30,7 +31,7 @@ private void OnAppearanceChange(EntityUid uid, SharedGravityGeneratorComponent c } } - if (_appearanceSystem.TryGetData<float>(uid, GravityGeneratorVisuals.Charge, out var charge, args.Component)) + if (_appearanceSystem.TryGetData<float>(uid, PowerChargeVisuals.Charge, out var charge, args.Component)) { var layer = args.Sprite.LayerMapGet(GravityGeneratorVisualLayers.Core); switch (charge) diff --git a/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs b/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs deleted file mode 100644 index d72da3e8120..00000000000 --- a/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Content.Shared.Gravity; -using JetBrains.Annotations; -using Robust.Client.GameObjects; - -namespace Content.Client.Gravity.UI -{ - [UsedImplicitly] - public sealed class GravityGeneratorBoundUserInterface : BoundUserInterface - { - [ViewVariables] - private GravityGeneratorWindow? _window; - - public GravityGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } - - protected override void Open() - { - base.Open(); - - _window = new GravityGeneratorWindow(this); - - /* - _window.Switch.OnPressed += _ => - { - SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(!IsOn)); - }; - */ - - _window.OpenCentered(); - _window.OnClose += Close; - } - - protected override void UpdateState(BoundUserInterfaceState state) - { - base.UpdateState(state); - - var castState = (SharedGravityGeneratorComponent.GeneratorState) state; - _window?.UpdateState(castState); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - - _window?.Dispose(); - } - - public void SetPowerSwitch(bool on) - { - SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(on)); - } - } -} diff --git a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs b/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs deleted file mode 100644 index 75f8eb479b5..00000000000 --- a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Content.Shared.Gravity; -using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow; - -namespace Content.Client.Gravity.UI -{ - [GenerateTypedNameReferences] - public sealed partial class GravityGeneratorWindow : FancyWindow - { - private readonly ButtonGroup _buttonGroup = new(); - - private readonly GravityGeneratorBoundUserInterface _owner; - - public GravityGeneratorWindow(GravityGeneratorBoundUserInterface owner) - { - RobustXamlLoader.Load(this); - IoCManager.InjectDependencies(this); - - _owner = owner; - - OnButton.Group = _buttonGroup; - OffButton.Group = _buttonGroup; - - OnButton.OnPressed += _ => _owner.SetPowerSwitch(true); - OffButton.OnPressed += _ => _owner.SetPowerSwitch(false); - - EntityView.SetEntity(owner.Owner); - } - - public void UpdateState(SharedGravityGeneratorComponent.GeneratorState state) - { - if (state.On) - OnButton.Pressed = true; - else - OffButton.Pressed = true; - - PowerLabel.Text = Loc.GetString( - "gravity-generator-window-power-label", - ("draw", state.PowerDraw), - ("max", state.PowerDrawMax)); - - PowerLabel.SetOnlyStyleClass(MathHelper.CloseTo(state.PowerDraw, state.PowerDrawMax) ? "Good" : "Caution"); - - ChargeBar.Value = state.Charge; - ChargeText.Text = (state.Charge / 255f).ToString("P0"); - StatusLabel.Text = Loc.GetString(state.PowerStatus switch - { - GravityGeneratorPowerStatus.Off => "gravity-generator-window-status-off", - GravityGeneratorPowerStatus.Discharging => "gravity-generator-window-status-discharging", - GravityGeneratorPowerStatus.Charging => "gravity-generator-window-status-charging", - GravityGeneratorPowerStatus.FullyCharged => "gravity-generator-window-status-fully-charged", - _ => throw new ArgumentOutOfRangeException() - }); - - StatusLabel.SetOnlyStyleClass(state.PowerStatus switch - { - GravityGeneratorPowerStatus.Off => "Danger", - GravityGeneratorPowerStatus.Discharging => "Caution", - GravityGeneratorPowerStatus.Charging => "Caution", - GravityGeneratorPowerStatus.FullyCharged => "Good", - _ => throw new ArgumentOutOfRangeException() - }); - - EtaLabel.Text = state.EtaSeconds >= 0 - ? Loc.GetString("gravity-generator-window-eta-value", ("left", TimeSpan.FromSeconds(state.EtaSeconds))) - : Loc.GetString("gravity-generator-window-eta-none"); - - EtaLabel.SetOnlyStyleClass(state.EtaSeconds >= 0 ? "Caution" : "Disabled"); - } - } -} diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs index a8872604a4c..53977eb636b 100644 --- a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs +++ b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; +using Robust.Client.UserInterface; namespace Content.Client.Humanoid; @@ -20,8 +21,7 @@ protected override void Open() { base.Open(); - _window = new(); - _window.OnClose += Close; + _window = this.CreateWindow<HumanoidMarkingModifierWindow>(); _window.OnMarkingAdded += SendMarkingSet; _window.OnMarkingRemoved += SendMarkingSet; _window.OnMarkingColorChange += SendMarkingSetNoResend; diff --git a/Content.Client/Instruments/UI/BandMenu.xaml.cs b/Content.Client/Instruments/UI/BandMenu.xaml.cs index 5fb293a194d..26cd1369e55 100644 --- a/Content.Client/Instruments/UI/BandMenu.xaml.cs +++ b/Content.Client/Instruments/UI/BandMenu.xaml.cs @@ -11,7 +11,9 @@ public sealed partial class BandMenu : DefaultWindow { private readonly InstrumentBoundUserInterface _owner; - public BandMenu(InstrumentBoundUserInterface owner) : base() + public EntityUid? Master; + + public BandMenu(InstrumentBoundUserInterface owner) { RobustXamlLoader.Load(this); @@ -40,7 +42,7 @@ public void Populate((NetEntity, string)[] nearby, IEntityManager entManager) { var uid = entManager.GetEntity(nent); var item = BandList.AddItem(name, null, true, uid); - item.Selected = _owner.Instrument?.Master == uid; + item.Selected = Master == uid; } } } diff --git a/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs b/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs index 2814d415365..c175e67842f 100644 --- a/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs +++ b/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs @@ -51,7 +51,7 @@ private void OnClearPressed(BaseButton.ButtonEventArgs obj) } } - public void Populate() + public void Populate(InstrumentComponent? instrument) { ChannelList.Clear(); @@ -60,7 +60,8 @@ public void Populate() var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name", ("number", i)), null, true, i); - item.Selected = !_owner.Instrument?.FilteredChannels[i] ?? false; + + item.Selected = !instrument?.FilteredChannels[i] ?? false; } } } diff --git a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs index 0f5729f55b1..4816ce8c365 100644 --- a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs +++ b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs @@ -24,8 +24,6 @@ public sealed class InstrumentBoundUserInterface : BoundUserInterface [ViewVariables] private BandMenu? _bandMenu; [ViewVariables] private ChannelsMenu? _channelsMenu; - [ViewVariables] public InstrumentComponent? Instrument { get; private set; } - public InstrumentBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { IoCManager.InjectDependencies(this); @@ -43,14 +41,20 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) protected override void Open() { - if (!EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument)) - return; + _instrumentMenu = this.CreateWindow<InstrumentMenu>(); + _instrumentMenu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; - Instrument = instrument; - _instrumentMenu = new InstrumentMenu(this); - _instrumentMenu.OnClose += Close; + _instrumentMenu.OnOpenBand += OpenBandMenu; + _instrumentMenu.OnOpenChannels += OpenChannelsMenu; + _instrumentMenu.OnCloseChannels += CloseChannelsMenu; + _instrumentMenu.OnCloseBands += CloseBandMenu; - _instrumentMenu.OpenCentered(); + _instrumentMenu.SetMIDI(MidiManager.IsAvailable); + + if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument)) + { + _instrumentMenu.SetInstrument((Owner, instrument)); + } } protected override void Dispose(bool disposing) @@ -58,7 +62,12 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); if (!disposing) return; - _instrumentMenu?.Dispose(); + + if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument)) + { + _instrumentMenu?.RemoveInstrument(instrument); + } + _bandMenu?.Dispose(); _channelsMenu?.Dispose(); } @@ -72,6 +81,11 @@ public void OpenBandMenu() { _bandMenu ??= new BandMenu(this); + if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument)) + { + _bandMenu.Master = instrument.Master; + } + // Refresh cache... RefreshBands(); @@ -87,7 +101,9 @@ public void CloseBandMenu() public void OpenChannelsMenu() { _channelsMenu ??= new ChannelsMenu(this); - _channelsMenu.Populate(); + EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument); + + _channelsMenu.Populate(instrument); _channelsMenu.OpenCenteredRight(); } diff --git a/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs b/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs index da443e3fb5b..9b14e01fb57 100644 --- a/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs +++ b/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs @@ -1,7 +1,10 @@ using System.IO; using System.Numerics; using System.Threading.Tasks; +using Content.Client.Interactable; +using Content.Shared.ActionBlocker; using Robust.Client.AutoGenerated; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; @@ -16,33 +19,23 @@ namespace Content.Client.Instruments.UI [GenerateTypedNameReferences] public sealed partial class InstrumentMenu : DefaultWindow { - private readonly InstrumentBoundUserInterface _owner; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IFileDialogManager _dialogs = default!; + [Dependency] private readonly IPlayerManager _player = default!; private bool _isMidiFileDialogueWindowOpen; - public InstrumentMenu(InstrumentBoundUserInterface owner) - { - RobustXamlLoader.Load(this); - - _owner = owner; + public event Action? OnOpenBand; + public event Action? OnOpenChannels; + public event Action? OnCloseBands; + public event Action? OnCloseChannels; - if (_owner.Instrument != null) - { - _owner.Instrument.OnMidiPlaybackEnded += InstrumentOnMidiPlaybackEnded; - Title = _owner.Entities.GetComponent<MetaDataComponent>(_owner.Owner).EntityName; - LoopButton.Disabled = !_owner.Instrument.IsMidiOpen; - LoopButton.Pressed = _owner.Instrument.LoopMidi; - ChannelsButton.Disabled = !_owner.Instrument.IsRendererAlive; - StopButton.Disabled = !_owner.Instrument.IsMidiOpen; - PlaybackSlider.MouseFilter = _owner.Instrument.IsMidiOpen ? MouseFilterMode.Pass : MouseFilterMode.Ignore; - } + public EntityUid Entity; - if (!_owner.MidiManager.IsAvailable) - { - UnavailableOverlay.Visible = true; - // We return early as to not give the buttons behavior. - return; - } + public InstrumentMenu() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); InputButton.OnToggled += MidiInputButtonOnOnToggled; BandButton.OnPressed += BandButtonOnPressed; @@ -57,12 +50,34 @@ public InstrumentMenu(InstrumentBoundUserInterface owner) MinSize = SetSize = new Vector2(400, 150); } + public void SetInstrument(Entity<InstrumentComponent> entity) + { + Entity = entity; + var component = entity.Comp; + component.OnMidiPlaybackEnded += InstrumentOnMidiPlaybackEnded; + LoopButton.Disabled = !component.IsMidiOpen; + LoopButton.Pressed = component.LoopMidi; + ChannelsButton.Disabled = !component.IsRendererAlive; + StopButton.Disabled = !component.IsMidiOpen; + PlaybackSlider.MouseFilter = component.IsMidiOpen ? MouseFilterMode.Pass : MouseFilterMode.Ignore; + } + + public void RemoveInstrument(InstrumentComponent component) + { + component.OnMidiPlaybackEnded -= InstrumentOnMidiPlaybackEnded; + } + + public void SetMIDI(bool available) + { + UnavailableOverlay.Visible = !available; + } + private void BandButtonOnPressed(ButtonEventArgs obj) { if (!PlayCheck()) return; - _owner.OpenBandMenu(); + OnOpenBand?.Invoke(); } private void BandButtonOnToggled(ButtonToggledEventArgs obj) @@ -70,12 +85,15 @@ private void BandButtonOnToggled(ButtonToggledEventArgs obj) if (obj.Pressed) return; - _owner.Instruments.SetMaster(_owner.Owner, null); + if (_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument)) + { + _entManager.System<InstrumentSystem>().SetMaster(Entity, instrument.Master); + } } private void ChannelsButtonOnPressed(ButtonEventArgs obj) { - _owner.OpenChannelsMenu(); + OnOpenChannels?.Invoke(); } private void InstrumentOnMidiPlaybackEnded() @@ -85,8 +103,10 @@ private void InstrumentOnMidiPlaybackEnded() public void MidiPlaybackSetButtonsDisabled(bool disabled) { - if(disabled) - _owner.CloseChannelsMenu(); + if (disabled) + { + OnCloseChannels?.Invoke(); + } LoopButton.Disabled = disabled; StopButton.Disabled = disabled; @@ -100,7 +120,7 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj) if (_isMidiFileDialogueWindowOpen) return; - _owner.CloseBandMenu(); + OnCloseBands?.Invoke(); var filters = new FileDialogFilters(new FileDialogFilters.Group("mid", "midi")); @@ -108,7 +128,7 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj) // or focus the previously-opened window. _isMidiFileDialogueWindowOpen = true; - await using var file = await _owner.FileDialogManager.OpenFile(filters); + await using var file = await _dialogs.OpenFile(filters); _isMidiFileDialogueWindowOpen = false; @@ -129,9 +149,18 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj) await file.CopyToAsync(memStream); - if (_owner.Instrument is not {} instrument - || !_owner.Instruments.OpenMidi(_owner.Owner, memStream.GetBuffer().AsSpan(0, (int) memStream.Length), instrument)) + if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument)) + { return; + } + + if (!_entManager.System<InstrumentSystem>() + .OpenMidi(Entity, + memStream.GetBuffer().AsSpan(0, (int) memStream.Length), + instrument)) + { + return; + } MidiPlaybackSetButtonsDisabled(false); if (InputButton.Pressed) @@ -140,7 +169,7 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj) private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj) { - _owner.CloseBandMenu(); + OnCloseBands?.Invoke(); if (obj.Pressed) { @@ -148,109 +177,99 @@ private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj) return; MidiStopButtonOnPressed(null); - if(_owner.Instrument is {} instrument) - _owner.Instruments.OpenInput(_owner.Owner, instrument); + + if (_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument)) + _entManager.System<InstrumentSystem>().OpenInput(Entity, instrument); } - else if (_owner.Instrument is { } instrument) + else { - _owner.Instruments.CloseInput(_owner.Owner, false, instrument); - _owner.CloseChannelsMenu(); + _entManager.System<InstrumentSystem>().CloseInput(Entity, false); + OnCloseChannels?.Invoke(); } } private bool PlayCheck() { // TODO all of these checks should also be done server-side. - - var instrumentEnt = _owner.Owner; - var instrument = _owner.Instrument; - - if (instrument == null) + if (!_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument)) return false; - var localEntity = _owner.PlayerManager.LocalEntity; + var localEntity = _player.LocalEntity; // If we don't have a player or controlled entity, we return. if (localEntity == null) return false; // By default, allow an instrument to play itself and skip all other checks - if (localEntity == instrumentEnt) + if (localEntity == Entity) return true; - var container = _owner.Entities.System<SharedContainerSystem>(); + var container = _entManager.System<SharedContainerSystem>(); // If we're a handheld instrument, we might be in a container. Get it just in case. - container.TryGetContainingContainer(instrumentEnt, out var conMan); + container.TryGetContainingContainer((Entity, null, null), out var conMan); // If the instrument is handheld and we're not holding it, we return. - if ((instrument.Handheld && (conMan == null || conMan.Owner != localEntity))) + if (instrument.Handheld && (conMan == null || conMan.Owner != localEntity)) return false; - if (!_owner.ActionBlocker.CanInteract(localEntity.Value, instrumentEnt)) + if (!_entManager.System<ActionBlockerSystem>().CanInteract(localEntity.Value, Entity)) return false; // We check that we're in range unobstructed just in case. - return _owner.Interactions.InRangeUnobstructed(localEntity.Value, instrumentEnt); + return _entManager.System<InteractionSystem>().InRangeUnobstructed(localEntity.Value, Entity); } private void MidiStopButtonOnPressed(ButtonEventArgs? obj) { MidiPlaybackSetButtonsDisabled(true); - if (_owner.Instrument is not {} instrument) - return; - - _owner.Instruments.CloseMidi(_owner.Owner, false, instrument); - _owner.CloseChannelsMenu(); + _entManager.System<InstrumentSystem>().CloseMidi(Entity, false); + OnCloseChannels?.Invoke(); } private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj) { - if (_owner.Instrument == null) - return; + var instrument = _entManager.System<InstrumentSystem>(); + + if (_entManager.TryGetComponent(Entity, out InstrumentComponent? instrumentComp)) + { + instrumentComp.LoopMidi = obj.Pressed; + } - _owner.Instrument.LoopMidi = obj.Pressed; - _owner.Instruments.UpdateRenderer(_owner.Owner, _owner.Instrument); + instrument.UpdateRenderer(Entity); } private void PlaybackSliderSeek(Range _) { // Do not seek while still grabbing. - if (PlaybackSlider.Grabbed || _owner.Instrument is not {} instrument) + if (PlaybackSlider.Grabbed) return; - _owner.Instruments.SetPlayerTick(_owner.Owner, (int)Math.Ceiling(PlaybackSlider.Value), instrument); + _entManager.System<InstrumentSystem>().SetPlayerTick(Entity, (int)Math.Ceiling(PlaybackSlider.Value)); } private void PlaybackSliderKeyUp(GUIBoundKeyEventArgs args) { - if (args.Function != EngineKeyFunctions.UIClick || _owner.Instrument is not {} instrument) + if (args.Function != EngineKeyFunctions.UIClick) return; - _owner.Instruments.SetPlayerTick(_owner.Owner, (int)Math.Ceiling(PlaybackSlider.Value), instrument); - } - - public override void Close() - { - base.Close(); - _owner.CloseBandMenu(); - _owner.CloseChannelsMenu(); + _entManager.System<InstrumentSystem>().SetPlayerTick(Entity, (int)Math.Ceiling(PlaybackSlider.Value)); } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - if (_owner.Instrument == null) + if (!_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument)) return; - var hasMaster = _owner.Instrument.Master != null; + var hasMaster = instrument.Master != null; BandButton.ToggleMode = hasMaster; BandButton.Pressed = hasMaster; - BandButton.Disabled = _owner.Instrument.IsMidiOpen || _owner.Instrument.IsInputOpen; - ChannelsButton.Disabled = !_owner.Instrument.IsRendererAlive; + BandButton.Disabled = instrument.IsMidiOpen || instrument.IsInputOpen; + ChannelsButton.Disabled = !instrument.IsRendererAlive; - if (!_owner.Instrument.IsMidiOpen) + if (!instrument.IsMidiOpen) { PlaybackSlider.MaxValue = 1; PlaybackSlider.SetValueWithoutEvent(0); @@ -260,8 +279,8 @@ protected override void FrameUpdate(FrameEventArgs args) if (PlaybackSlider.Grabbed) return; - PlaybackSlider.MaxValue = _owner.Instrument.PlayerTotalTick; - PlaybackSlider.SetValueWithoutEvent(_owner.Instrument.PlayerTick); + PlaybackSlider.MaxValue = instrument.PlayerTotalTick; + PlaybackSlider.SetValueWithoutEvent(instrument.PlayerTick); } } } diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index 4ccede7f5d4..58883d0ac83 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -17,6 +17,7 @@ using Content.Shared.Strip.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.Player; @@ -31,11 +32,13 @@ namespace Content.Client.Inventory [UsedImplicitly] public sealed class StrippableBoundUserInterface : BoundUserInterface { + [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IUserInterfaceManager _ui = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; + private readonly ExamineSystem _examine; private readonly InventorySystem _inv; private readonly SharedCuffableSystem _cuffable; + private readonly StrippableSystem _strippable; [ViewVariables] private const int ButtonSeparation = 4; @@ -44,7 +47,7 @@ public sealed class StrippableBoundUserInterface : BoundUserInterface public const string HiddenPocketEntityId = "StrippingHiddenEntity"; [ViewVariables] - private readonly StrippingMenu? _strippingMenu; + private StrippingMenu? _strippingMenu; [ViewVariables] private readonly EntityUid _virtualHiddenEntity; @@ -54,29 +57,32 @@ public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, u _examine = EntMan.System<ExamineSystem>(); _inv = EntMan.System<InventorySystem>(); _cuffable = EntMan.System<SharedCuffableSystem>(); + _strippable = EntMan.System<StrippableSystem>(); - var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner, EntMan))); - _strippingMenu = new StrippingMenu(title, this); - _strippingMenu.OnClose += Close; _virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace); } protected override void Open() { base.Open(); + + _strippingMenu = this.CreateWindow<StrippingMenu>(); + _strippingMenu.OnDirty += UpdateMenu; + _strippingMenu.Title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner, EntMan))); + _strippingMenu?.OpenCenteredLeft(); } protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - EntMan.DeleteEntity(_virtualHiddenEntity); - if (!disposing) return; - _strippingMenu?.Dispose(); + if (_strippingMenu != null) + _strippingMenu.OnDirty -= UpdateMenu; + + EntMan.DeleteEntity(_virtualHiddenEntity); + base.Dispose(disposing); } public void DirtyMenu() @@ -200,8 +206,11 @@ private void AddInventoryButton(EntityUid invUid, string slotId, InventoryCompon var entity = container.ContainedEntity; // If this is a full pocket, obscure the real entity - if (entity != null && slotDef.StripHidden - && !(EntMan.TryGetComponent<ThievingComponent>(_playerManager.LocalEntity, out var thiefcomponent) && thiefcomponent.IgnoreStripHidden)) + // this does not work for modified clients because they are still sent the real entity + if (entity != null + && _strippable.IsStripHidden(slotDef, _player.LocalEntity) + && !(EntMan.TryGetComponent<ThievingComponent>(PlayerManager.LocalEntity, out var thiefComponent) + && thiefComponent.IgnoreStripHidden)) entity = _virtualHiddenEntity; var button = new SlotButton(new SlotData(slotDef, container)); diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 745509a56a0..6d181c52b8c 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -2,6 +2,7 @@ using Content.Client.Changelog; using Content.Client.Chat.Managers; using Content.Client.Clickable; +using Content.Client.DeltaV.NanoChat; using Content.Client.DiscordAuth; using Content.Client.JoinQueue; using Content.Client.Eui; @@ -23,6 +24,8 @@ using Content.Shared.Chat; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.RateLimiting; +using Robust.Client.GameObjects; + namespace Content.Client.IoC { @@ -53,10 +56,11 @@ public static void Register() collection.Register<DocumentParsingManager>(); collection.Register<ContentReplayPlaybackManager>(); collection.Register<ISharedPlaytimeManager, JobRequirementsManager>(); - IoCManager.Register<JoinQueueManager>(); - IoCManager.Register<DiscordAuthManager>(); + collection.Register<JoinQueueManager>(); + collection.Register<DiscordAuthManager>(); collection.Register<PlayerRateLimitManager>(); collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>(); + collection.Register<NanoChatSystem>(); } } } diff --git a/Content.Client/Items/Systems/ItemSystem.cs b/Content.Client/Items/Systems/ItemSystem.cs index 5e60d06d0ce..2b5a41c6ce7 100644 --- a/Content.Client/Items/Systems/ItemSystem.cs +++ b/Content.Client/Items/Systems/ItemSystem.cs @@ -43,7 +43,7 @@ private void OnEquipped(EntityUid uid, SpriteComponent component, GotEquippedEve public override void VisualsChanged(EntityUid uid) { // if the item is in a container, it might be equipped to hands or inventory slots --> update visuals. - if (Container.TryGetContainingContainer(uid, out var container)) + if (Container.TryGetContainingContainer((uid, null, null), out var container)) RaiseLocalEvent(container.Owner, new VisualsChangedEvent(GetNetEntity(uid), container.ID)); } diff --git a/Content.Client/Items/Systems/ItemToggleSystem.cs b/Content.Client/Items/Systems/ItemToggleSystem.cs deleted file mode 100644 index 46d6f1b464d..00000000000 --- a/Content.Client/Items/Systems/ItemToggleSystem.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Shared.Item.ItemToggle; - -namespace Content.Shared.Item; - -/// <inheritdoc/> -public sealed class ItemToggleSystem : SharedItemToggleSystem -{ - -} diff --git a/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs b/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs index f97d8a73302..7884268c428 100644 --- a/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs +++ b/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs @@ -12,42 +12,34 @@ namespace Content.Client.Kitchen.UI [GenerateTypedNameReferences] public sealed partial class GrinderMenu : FancyWindow { - private readonly IEntityManager _entityManager; - private readonly IPrototypeManager _prototypeManager; - private readonly ReagentGrinderBoundUserInterface _owner; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; private readonly Dictionary<int, EntityUid> _chamberVisualContents = new(); - public GrinderMenu(ReagentGrinderBoundUserInterface owner, IEntityManager entityManager, IPrototypeManager prototypeManager) + public event Action? OnToggleAuto; + public event Action? OnGrind; + public event Action? OnJuice; + public event Action? OnEjectAll; + public event Action? OnEjectBeaker; + public event Action<EntityUid>? OnEjectChamber; + + public GrinderMenu() { RobustXamlLoader.Load(this); - _entityManager = entityManager; - _prototypeManager = prototypeManager; - _owner = owner; - AutoModeButton.OnPressed += owner.ToggleAutoMode; - GrindButton.OnPressed += owner.StartGrinding; - JuiceButton.OnPressed += owner.StartJuicing; - ChamberContentBox.EjectButton.OnPressed += owner.EjectAll; - BeakerContentBox.EjectButton.OnPressed += owner.EjectBeaker; + IoCManager.InjectDependencies(this); + AutoModeButton.OnPressed += _ => OnToggleAuto?.Invoke(); + GrindButton.OnPressed += _ => OnGrind?.Invoke(); + JuiceButton.OnPressed += _ => OnJuice?.Invoke(); + ChamberContentBox.EjectButton.OnPressed += _ => OnEjectAll?.Invoke(); + BeakerContentBox.EjectButton.OnPressed += _ => OnEjectBeaker?.Invoke(); ChamberContentBox.BoxContents.OnItemSelected += OnChamberBoxContentsItemSelected; BeakerContentBox.BoxContents.SelectMode = ItemList.ItemListSelectMode.None; } private void OnChamberBoxContentsItemSelected(ItemList.ItemListSelectedEventArgs args) { - _owner.EjectChamberContent(_chamberVisualContents[args.ItemIndex]); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - _chamberVisualContents.Clear(); - GrindButton.OnPressed -= _owner.StartGrinding; - JuiceButton.OnPressed -= _owner.StartJuicing; - ChamberContentBox.EjectButton.OnPressed -= _owner.EjectAll; - BeakerContentBox.EjectButton.OnPressed -= _owner.EjectBeaker; - ChamberContentBox.BoxContents.OnItemSelected -= OnChamberBoxContentsItemSelected; + OnEjectChamber?.Invoke(_chamberVisualContents[args.ItemIndex]); } public void UpdateState(ReagentGrinderInterfaceState state) diff --git a/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs b/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs index 7e7dd2d6935..643ac47054b 100644 --- a/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs +++ b/Content.Client/Kitchen/UI/MicrowaveBoundUserInterface.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; @@ -19,28 +20,15 @@ public sealed class MicrowaveBoundUserInterface : BoundUserInterface [ViewVariables] private readonly Dictionary<int, ReagentQuantity> _reagents = new(); - [Dependency] private readonly IGameTiming _gameTiming = default!; - - public MicrowaveUpdateUserInterfaceState currentState = default!; - - private IEntityManager _entManager; public MicrowaveBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { - _entManager = IoCManager.Resolve<IEntityManager>(); - } - - public TimeSpan GetCurrentTime() - { - return _gameTiming.CurTime; } protected override void Open() { base.Open(); - _menu = new MicrowaveMenu(this); - _menu.OpenCentered(); - _menu.OnClose += Close; + _menu = this.CreateWindow<MicrowaveMenu>(); _menu.StartButton.OnPressed += _ => SendPredictedMessage(new MicrowaveStartCookMessage()); _menu.EjectButton.OnPressed += _ => SendPredictedMessage(new MicrowaveEjectMessage()); _menu.IngredientsList.OnItemSelected += args => @@ -74,38 +62,23 @@ protected override void Open() }; } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (!disposing) - { - return; - } - - _solids.Clear(); - _menu?.Dispose(); - } - protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); - if (state is not MicrowaveUpdateUserInterfaceState cState) + if (state is not MicrowaveUpdateUserInterfaceState cState || _menu == null) { return; } + _menu.IsBusy = cState.IsMicrowaveBusy; + _menu.CurrentCooktimeEnd = cState.CurrentCookTimeEnd; - _menu?.ToggleBusyDisableOverlayPanel(cState.IsMicrowaveBusy || cState.ContainedSolids.Length == 0); - currentState = cState; - + _menu.ToggleBusyDisableOverlayPanel(cState.IsMicrowaveBusy || cState.ContainedSolids.Length == 0); // TODO move this to a component state and ensure the net ids. - RefreshContentsDisplay(_entManager.GetEntityArray(cState.ContainedSolids)); - - if (_menu == null) return; + RefreshContentsDisplay(EntMan.GetEntityArray(cState.ContainedSolids)); //Set the cook time info label - var cookTime = cState.ActiveButtonIndex == 0 + var cookTime = cState.ActiveButtonIndex == 0 ? Loc.GetString("microwave-menu-instant-button") : cState.CurrentCookTime.ToString(); diff --git a/Content.Client/Kitchen/UI/MicrowaveMenu.xaml.cs b/Content.Client/Kitchen/UI/MicrowaveMenu.xaml.cs index b292e9f1465..13029e38469 100644 --- a/Content.Client/Kitchen/UI/MicrowaveMenu.xaml.cs +++ b/Content.Client/Kitchen/UI/MicrowaveMenu.xaml.cs @@ -9,22 +9,21 @@ namespace Content.Client.Kitchen.UI [GenerateTypedNameReferences] public sealed partial class MicrowaveMenu : FancyWindow { - public sealed class MicrowaveCookTimeButton : Button - { - public uint CookTime; - } + [Dependency] private readonly IGameTiming _timing = default!; public event Action<BaseButton.ButtonEventArgs, int>? OnCookTimeSelected; public ButtonGroup CookTimeButtonGroup { get; } - private readonly MicrowaveBoundUserInterface _owner; - public MicrowaveMenu(MicrowaveBoundUserInterface owner) + public bool IsBusy; + public TimeSpan CurrentCooktimeEnd; + + public MicrowaveMenu() { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); CookTimeButtonGroup = new ButtonGroup(); InstantCookButton.Group = CookTimeButtonGroup; - _owner = owner; InstantCookButton.OnPressed += args => { OnCookTimeSelected?.Invoke(args, 0); @@ -65,14 +64,20 @@ public void ToggleBusyDisableOverlayPanel(bool shouldDisable) protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - if(!_owner.currentState.IsMicrowaveBusy) + + if (!IsBusy) return; - if(_owner.currentState.CurrentCookTimeEnd > _owner.GetCurrentTime()) + if (CurrentCooktimeEnd > _timing.CurTime) { CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label", - ("time",_owner.currentState.CurrentCookTimeEnd.Subtract(_owner.GetCurrentTime()).Seconds)); + ("time", CurrentCooktimeEnd.Subtract(_timing.CurTime).Seconds)); } } + + public sealed class MicrowaveCookTimeButton : Button + { + public uint CookTime; + } } } diff --git a/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs b/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs index e6f108b3050..bc4cc75b4d1 100644 --- a/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs +++ b/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Containers.ItemSlots; using Content.Shared.Kitchen; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Prototypes; @@ -8,8 +9,6 @@ namespace Content.Client.Kitchen.UI { public sealed class ReagentGrinderBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [ViewVariables] private GrinderMenu? _menu; @@ -21,20 +20,13 @@ protected override void Open() { base.Open(); - _menu = new GrinderMenu(this, EntMan, _prototypeManager); - _menu.OpenCentered(); - _menu.OnClose += Close; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - { - return; - } - - _menu?.Dispose(); + _menu = this.CreateWindow<GrinderMenu>(); + _menu.OnToggleAuto += ToggleAutoMode; + _menu.OnGrind += StartGrinding; + _menu.OnJuice += StartJuicing; + _menu.OnEjectAll += EjectAll; + _menu.OnEjectBeaker += EjectBeaker; + _menu.OnEjectChamber += EjectChamberContent; } protected override void UpdateState(BoundUserInterfaceState state) @@ -52,27 +44,27 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) _menu?.HandleMessage(message); } - public void ToggleAutoMode(BaseButton.ButtonEventArgs args) + public void ToggleAutoMode() { SendMessage(new ReagentGrinderToggleAutoModeMessage()); } - public void StartGrinding(BaseButton.ButtonEventArgs? _ = null) + public void StartGrinding() { SendMessage(new ReagentGrinderStartMessage(GrinderProgram.Grind)); } - public void StartJuicing(BaseButton.ButtonEventArgs? _ = null) + public void StartJuicing() { SendMessage(new ReagentGrinderStartMessage(GrinderProgram.Juice)); } - public void EjectAll(BaseButton.ButtonEventArgs? _ = null) + public void EjectAll() { SendMessage(new ReagentGrinderEjectChamberAllMessage()); } - public void EjectBeaker(BaseButton.ButtonEventArgs? _ = null) + public void EjectBeaker() { SendMessage(new ItemSlotButtonPressedEvent(SharedReagentGrinder.BeakerSlotId)); } diff --git a/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs b/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs index 555f1ff09e6..6b656123412 100644 --- a/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs +++ b/Content.Client/Labels/UI/HandLabelerBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Labels; using Content.Shared.Labels.Components; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Labels.UI { @@ -23,13 +24,8 @@ protected override void Open() { base.Open(); - _window = new HandLabelerWindow(); - if (State != null) - UpdateState(State); + _window = this.CreateWindow<HandLabelerWindow>(); - _window.OpenCentered(); - - _window.OnClose += Close; _window.OnLabelChanged += OnLabelChanged; Reload(); } @@ -51,13 +47,5 @@ public void Reload() _window.SetCurrentLabel(component.AssignedLabel); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } - } diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index e474f0c9429..31c09ef1a37 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -251,7 +251,7 @@ private void RebuildUI() VerticalAlignment = VAlignment.Center }; - var jobIcon = _prototypeManager.Index<StatusIconPrototype>(prototype.Icon); + var jobIcon = _prototypeManager.Index(prototype.Icon); icon.Texture = _sprites.Frame0(jobIcon.Icon); jobSelector.AddChild(icon); diff --git a/Content.Client/Lathe/UI/LatheBoundUserInterface.cs b/Content.Client/Lathe/UI/LatheBoundUserInterface.cs index 6e6d1b91761..66f09aa41da 100644 --- a/Content.Client/Lathe/UI/LatheBoundUserInterface.cs +++ b/Content.Client/Lathe/UI/LatheBoundUserInterface.cs @@ -1,6 +1,8 @@ +using Content.Shared.DeltaV.Salvage; // DeltaV using Content.Shared.Lathe; using Content.Shared.Research.Components; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Lathe.UI { @@ -17,9 +19,9 @@ protected override void Open() { base.Open(); - _menu = new LatheMenu(this); - _menu.OnClose += Close; - + _menu = this.CreateWindow<LatheMenu>(); + _menu.SetEntity(Owner); + _menu.OpenCenteredRight(); _menu.OnServerListButtonPressed += _ => { @@ -31,7 +33,7 @@ protected override void Open() SendMessage(new LatheQueueRecipeMessage(recipe, amount)); }; - _menu.OpenCenteredRight(); + _menu.OnClaimMiningPoints += () => SendMessage(new LatheClaimMiningPointsMessage()); // DeltaV } protected override void UpdateState(BoundUserInterfaceState state) @@ -50,13 +52,5 @@ protected override void UpdateState(BoundUserInterfaceState state) break; } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Dispose(); - } } } diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml index 6f484d8c7be..d84449ce43e 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml +++ b/Content.Client/Lathe/UI/LatheMenu.xaml @@ -100,12 +100,9 @@ Margin="5 0 0 0" Text="{Loc 'lathe-menu-fabricating-message'}"> </Label> - <TextureRect - Name="Icon" - HorizontalExpand="True" - SizeFlagsStretchRatio="2" - Margin="100 0 0 0"> - </TextureRect> + <BoxContainer Name="FabricatingDisplayContainer" + HorizontalAlignment="Left" + Margin="100 0 0 0"/> <Label Name="NameLabel" RectClipContent="True" @@ -114,12 +111,15 @@ </Label> </PanelContainer> </BoxContainer> - <ItemList - Name="QueueList" - VerticalExpand="True" - SizeFlagsStretchRatio="3" - SelectMode="None"> - </ItemList> + <ScrollContainer VerticalExpand="True" HScrollEnabled="False"> + <BoxContainer + Name="QueueList" + Orientation="Vertical" + HorizontalExpand="True" + VerticalExpand="True" + RectClipContent="True"> + </BoxContainer> + </ScrollContainer> </BoxContainer> <BoxContainer VerticalExpand="True" @@ -132,6 +132,12 @@ HorizontalExpand="True"> <ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/> </BoxContainer> + <!-- Begin DeltaV Additions: Mining points --> + <BoxContainer Orientation="Horizontal" Name="MiningPointsContainer" Visible="False"> + <Label Name="MiningPointsLabel" HorizontalExpand="True"/> + <Button Name="MiningPointsClaimButton" Text="{Loc 'lathe-menu-mining-points-claim-button'}"/> + </BoxContainer> + <!-- End DeltaV Additions: Mining points --> </BoxContainer> </BoxContainer> diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs index 9e15f8239e5..96658996642 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs +++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs @@ -1,18 +1,20 @@ using System.Linq; using System.Text; using Content.Client.Materials; +using Content.Shared.DeltaV.Salvage.Components; // DeltaV +using Content.Shared.DeltaV.Salvage.Systems; // DeltaV using Content.Shared.Lathe; using Content.Shared.Lathe.Prototypes; -using Content.Shared.Materials; using Content.Shared.Research.Prototypes; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; +using Robust.Client.Player; // DeltaV +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Client.ResourceManagement; -using Robust.Client.Graphics; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; // DeltaV namespace Content.Client.Lathe.UI; @@ -20,16 +22,17 @@ namespace Content.Client.Lathe.UI; public sealed partial class LatheMenu : DefaultWindow { [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _player = default!; // DeltaV [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IResourceCache _resources = default!; - private EntityUid _owner; private readonly SpriteSystem _spriteSystem; private readonly LatheSystem _lathe; private readonly MaterialStorageSystem _materialStorage; + private readonly MiningPointsSystem _miningPoints; // DeltaV public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed; public event Action<string, int>? RecipeQueueAction; + public event Action? OnClaimMiningPoints; // DeltaV public List<ProtoId<LatheRecipePrototype>> Recipes = new(); @@ -37,17 +40,19 @@ public sealed partial class LatheMenu : DefaultWindow public ProtoId<LatheCategoryPrototype>? CurrentCategory; - public LatheMenu(LatheBoundUserInterface owner) + public EntityUid Entity; + + private uint? _lastMiningPoints; // DeltaV: used to avoid Loc.GetString every frame + + public LatheMenu() { - _owner = owner.Owner; RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _spriteSystem = _entityManager.System<SpriteSystem>(); _lathe = _entityManager.System<LatheSystem>(); _materialStorage = _entityManager.System<MaterialStorageSystem>(); - - Title = _entityManager.GetComponent<MetaDataComponent>(owner.Owner).EntityName; + _miningPoints = _entityManager.System<MiningPointsSystem>(); // DeltaV SearchBar.OnTextChanged += _ => { @@ -61,8 +66,13 @@ public LatheMenu(LatheBoundUserInterface owner) FilterOption.OnItemSelected += OnItemSelected; ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a); + } - if (_entityManager.TryGetComponent<LatheComponent>(owner.Owner, out var latheComponent)) + public void SetEntity(EntityUid uid) + { + Entity = uid; + + if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComponent)) { if (!latheComponent.DynamicRecipes.Any()) { @@ -70,17 +80,57 @@ public LatheMenu(LatheBoundUserInterface owner) } } - MaterialsList.SetOwner(owner.Owner); + // Begin DeltaV Additions: Mining points UI + MiningPointsContainer.Visible = _entityManager.TryGetComponent<MiningPointsComponent>(Entity, out var points); + MiningPointsClaimButton.OnPressed += _ => OnClaimMiningPoints?.Invoke(); + if (points != null) + UpdateMiningPoints(points.Points); + // End DeltaV Additions + + MaterialsList.SetOwner(Entity); + } + + protected override void Opened() + { + base.Opened(); + + if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComp)) + { + AmountLineEdit.SetText(latheComp.DefaultProductionAmount.ToString()); + } } /// <summary> - /// Populates the list of all the recipes + /// DeltaV: Updates the UI elements for mining points. /// </summary> - public void PopulateRecipes() + private void UpdateMiningPoints(uint points) { - if (!_entityManager.TryGetComponent<LatheComponent>(_owner, out var component)) + MiningPointsClaimButton.Disabled = points == 0 || + _player.LocalSession?.AttachedEntity is not {} player || + _miningPoints.TryFindIdCard(player) == null; + if (points == _lastMiningPoints) return; + _lastMiningPoints = points; + MiningPointsLabel.Text = Loc.GetString("lathe-menu-mining-points", ("points", points)); + } + + /// <summary> + /// DeltaV: Update mining points UI whenever it changes. + /// </summary> + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (_entityManager.TryGetComponent<MiningPointsComponent>(Entity, out var points)) + UpdateMiningPoints(points.Points); + } + + /// <summary> + /// Populates the list of all the recipes + /// </summary> + public void PopulateRecipes() + { var recipesToShow = new List<LatheRecipePrototype>(); foreach (var recipe in Recipes) { @@ -92,7 +142,7 @@ public void PopulateRecipes() if (SearchBar.Text.Trim().Length != 0) { - if (proto.Name.ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant())) + if (_lathe.GetRecipeName(recipe).ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant())) recipesToShow.Add(proto); } else @@ -104,25 +154,15 @@ public void PopulateRecipes() if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0) quantity = 1; - var sortedRecipesToShow = recipesToShow.OrderBy(p => p.Name); + var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName); RecipeList.Children.Clear(); + _entityManager.TryGetComponent(Entity, out LatheComponent? lathe); + foreach (var prototype in sortedRecipesToShow) { - List<Texture> textures; - if (_prototypeManager.TryIndex(prototype.Result, out EntityPrototype? entityProto) && entityProto != null) - { - textures = SpriteComponent.GetPrototypeTextures(entityProto, _resources).Select(o => o.Default).ToList(); - } - else - { - textures = prototype.Icon == null - ? new List<Texture> { _spriteSystem.GetPrototypeIcon(prototype.Result).Default } - : new List<Texture> { _spriteSystem.Frame0(prototype.Icon) }; - } - - var canProduce = _lathe.CanProduce(_owner, prototype, quantity); + var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe); - var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, textures); + var control = new RecipeControl(_lathe, prototype, () => GenerateTooltipText(prototype), canProduce, GetRecipeDisplayControl(prototype)); control.OnButtonPressed += s => { if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0) @@ -136,19 +176,20 @@ public void PopulateRecipes() private string GenerateTooltipText(LatheRecipePrototype prototype) { StringBuilder sb = new(); + var multiplier = _entityManager.GetComponent<LatheComponent>(Entity).MaterialUseMultiplier; - foreach (var (id, amount) in prototype.RequiredMaterials) + foreach (var (id, amount) in prototype.Materials) { - if (!_prototypeManager.TryIndex<MaterialPrototype>(id, out var proto)) + if (!_prototypeManager.TryIndex(id, out var proto)) continue; - var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, _entityManager.GetComponent<LatheComponent>(_owner).MaterialUseMultiplier); + var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, multiplier); var sheetVolume = _materialStorage.GetSheetVolume(proto); var unit = Loc.GetString(proto.Unit); var sheets = adjustedAmount / (float) sheetVolume; - var availableAmount = _materialStorage.GetMaterialAmount(_owner, id); + var availableAmount = _materialStorage.GetMaterialAmount(Entity, id); var missingAmount = Math.Max(0, adjustedAmount - availableAmount); var missingSheets = missingAmount / (float) sheetVolume; @@ -168,8 +209,9 @@ private string GenerateTooltipText(LatheRecipePrototype prototype) sb.AppendLine(tooltipText); } - if (!string.IsNullOrWhiteSpace(prototype.Description)) - sb.AppendLine(Loc.GetString("lathe-menu-description-display", ("description", prototype.Description))); + var desc = _lathe.GetRecipeDescription(prototype); + if (!string.IsNullOrWhiteSpace(desc)) + sb.AppendLine(Loc.GetString("lathe-menu-description-display", ("description", desc))); // Remove last newline if (sb.Length > 0) @@ -219,14 +261,22 @@ public void UpdateCategories() /// <param name="queue"></param> public void PopulateQueueList(List<LatheRecipePrototype> queue) { - QueueList.Clear(); + QueueList.DisposeAllChildren(); + + QueueList.DisposeAllChildren(); + var idx = 1; foreach (var recipe in queue) { - var icon = recipe.Icon == null - ? _spriteSystem.GetPrototypeIcon(recipe.Result).Default - : _spriteSystem.Frame0(recipe.Icon); - QueueList.AddItem($"{idx}. {recipe.Name}", icon); + var queuedRecipeBox = new BoxContainer(); + queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal; + + queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe)); + + var queuedRecipeLabel = new Label(); + queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}"; + queuedRecipeBox.AddChild(queuedRecipeLabel); + QueueList.AddChild(queuedRecipeBox); idx++; } } @@ -236,10 +286,30 @@ public void SetQueueInfo(LatheRecipePrototype? recipe) FabricatingContainer.Visible = recipe != null; if (recipe == null) return; - Icon.Texture = recipe.Icon == null - ? _spriteSystem.GetPrototypeIcon(recipe.Result).Default - : _spriteSystem.Frame0(recipe.Icon); - NameLabel.Text = $"{recipe.Name}"; + + FabricatingDisplayContainer.Children.Clear(); + FabricatingDisplayContainer.AddChild(GetRecipeDisplayControl(recipe)); + + NameLabel.Text = _lathe.GetRecipeName(recipe); + } + + public Control GetRecipeDisplayControl(LatheRecipePrototype recipe) + { + if (recipe.Icon != null) + { + var textRect = new TextureRect(); + textRect.Texture = _spriteSystem.Frame0(recipe.Icon); + return textRect; + } + + if (recipe.Result is { } result) + { + var entProtoView = new EntityPrototypeView(); + entProtoView.SetPrototype(result); + return entProtoView; + } + + return new Control(); } private void OnItemSelected(OptionButton.ItemSelectedEventArgs obj) diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml b/Content.Client/Lathe/UI/RecipeControl.xaml index d1371a026a2..1105ab478ff 100644 --- a/Content.Client/Lathe/UI/RecipeControl.xaml +++ b/Content.Client/Lathe/UI/RecipeControl.xaml @@ -5,14 +5,12 @@ Margin="0" StyleClasses="ButtonSquare"> <BoxContainer Orientation="Horizontal"> - <LayeredTextureRect - Name="RecipeTextures" + <BoxContainer + Name="RecipeDisplayContainer" Margin="0 0 4 0" HorizontalAlignment="Center" VerticalAlignment="Center" - Stretch="KeepAspectCentered" MinSize="32 32" - CanShrink="true" /> <Label Name="RecipeName" HorizontalExpand="True" /> </BoxContainer> diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs index 47b6b5932c4..e9108e88315 100644 --- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs +++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs @@ -1,9 +1,8 @@ using Content.Shared.Research.Prototypes; using Robust.Client.AutoGenerated; -using Robust.Client.Graphics; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; namespace Content.Client.Lathe.UI; @@ -13,12 +12,12 @@ public sealed partial class RecipeControl : Control public Action<string>? OnButtonPressed; public Func<string> TooltipTextSupplier; - public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, List<Texture> textures) + public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Control displayControl) { RobustXamlLoader.Load(this); - RecipeName.Text = recipe.Name; - RecipeTextures.Textures = textures; + RecipeName.Text = latheSystem.GetRecipeName(recipe); + RecipeDisplayContainer.AddChild(displayControl); Button.Disabled = !canProduce; TooltipTextSupplier = tooltipTextSupplier; Button.TooltipSupplier = SupplyTooltip; diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 64db3eab12f..07c673a9f3e 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -771,7 +771,7 @@ public void RefreshJobs() TextureScale = new(2, 2), VerticalAlignment = VAlignment.Center }; - var jobIcon = _prototypeManager.Index<StatusIconPrototype>(job.Icon); + var jobIcon = _prototypeManager.Index<JobIconPrototype>(job.Icon); icon.Texture = jobIcon.Icon.Frame0(); selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon); @@ -905,7 +905,7 @@ private void UpdateRoleRequirements() TextureScale = new Vector2(2, 2), VerticalAlignment = VAlignment.Center }; - var jobIcon = _prototypeManager.Index<StatusIconPrototype>(job.Icon); + var jobIcon = _prototypeManager.Index(job.Icon); icon.Texture = jobIcon.Icon.Frame0(); selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon); diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs index 14709f8b1f0..619cac68391 100644 --- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs +++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.UserInterface.Controls; +using Prometheus; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml.cs b/Content.Client/Lobby/UI/LobbyGui.xaml.cs index 3ab2caad786..d19643f20a9 100644 --- a/Content.Client/Lobby/UI/LobbyGui.xaml.cs +++ b/Content.Client/Lobby/UI/LobbyGui.xaml.cs @@ -3,6 +3,7 @@ using Content.Client.UserInterface.Systems.EscapeMenu; using Robust.Client.AutoGenerated; using Robust.Client.Console; +using Robust.Client.State; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; diff --git a/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs index 09bdedfd94c..11abe8c2451 100644 --- a/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs +++ b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.MachineLinking; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.Timing; namespace Content.Client.MachineLinking.UI; @@ -19,19 +20,14 @@ protected override void Open() { base.Open(); - _window = new SignalTimerWindow(this); - - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<SignalTimerWindow>(); + _window.OnStartTimer += StartTimer; _window.OnCurrentTextChanged += OnTextChanged; _window.OnCurrentDelayMinutesChanged += OnDelayChanged; _window.OnCurrentDelaySecondsChanged += OnDelayChanged; } - public void OnStartTimer() + public void StartTimer() { SendMessage(new SignalTimerStartMessage()); } @@ -48,11 +44,6 @@ private void OnDelayChanged(string newDelay) SendMessage(new SignalTimerDelayChangedMessage(_window.GetDelay())); } - public TimeSpan GetCurrentTime() - { - return _gameTiming.CurTime; - } - /// <summary> /// Update the UI state based on server-sent info /// </summary> @@ -72,11 +63,4 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.SetTimerStarted(cast.TimerStarted); _window.SetHasAccess(cast.HasAccess); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } diff --git a/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs index b62595595e5..441ca9ea365 100644 --- a/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs +++ b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs @@ -3,48 +3,51 @@ using Robust.Client.UserInterface.XAML; using Robust.Shared.Timing; using Content.Client.TextScreen; +using Robust.Client.UserInterface.Controls; namespace Content.Client.MachineLinking.UI; [GenerateTypedNameReferences] public sealed partial class SignalTimerWindow : DefaultWindow { + [Dependency] private readonly IGameTiming _timing = default!; + private const int MaxTextLength = 5; public event Action<string>? OnCurrentTextChanged; public event Action<string>? OnCurrentDelayMinutesChanged; public event Action<string>? OnCurrentDelaySecondsChanged; - private readonly SignalTimerBoundUserInterface _owner; - private TimeSpan? _triggerTime; private bool _timerStarted; - public SignalTimerWindow(SignalTimerBoundUserInterface owner) + public event Action? OnStartTimer; + + public SignalTimerWindow() { RobustXamlLoader.Load(this); - - _owner = owner; + IoCManager.InjectDependencies(this); CurrentTextEdit.OnTextChanged += e => OnCurrentTextChange(e.Text); CurrentDelayEditMinutes.OnTextChanged += e => OnCurrentDelayMinutesChange(e.Text); CurrentDelayEditSeconds.OnTextChanged += e => OnCurrentDelaySecondsChange(e.Text); - StartTimer.OnPressed += _ => OnStartTimer(); + StartTimer.OnPressed += _ => StartTimerWeh(); } - public void OnStartTimer() + private void StartTimerWeh() { if (!_timerStarted) { _timerStarted = true; - _triggerTime = _owner.GetCurrentTime() + GetDelay(); + _triggerTime = _timing.CurTime + GetDelay(); } else { SetTimerStarted(false); } - _owner.OnStartTimer(); + + OnStartTimer?.Invoke(); } protected override void FrameUpdate(FrameEventArgs args) @@ -54,9 +57,9 @@ protected override void FrameUpdate(FrameEventArgs args) if (!_timerStarted || _triggerTime == null) return; - if (_owner.GetCurrentTime() < _triggerTime.Value) + if (_timing.CurTime < _triggerTime.Value) { - StartTimer.Text = TextScreenSystem.TimeToString(_triggerTime.Value - _owner.GetCurrentTime()); + StartTimer.Text = TextScreenSystem.TimeToString(_triggerTime.Value - _timing.CurTime); } else { diff --git a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs index f6979bf8d7b..0a87948ff62 100644 --- a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs +++ b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Humanoid.Markings; using Content.Shared.MagicMirror; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.MagicMirror; @@ -17,7 +18,7 @@ protected override void Open() { base.Open(); - _window = new(); + _window = this.CreateWindow<MagicMirrorWindow>(); _window.OnHairSelected += tuple => SelectHair(MagicMirrorCategory.Hair, tuple.id, tuple.slot); _window.OnHairColorChanged += args => ChangeColor(MagicMirrorCategory.Hair, args.marking, args.slot); @@ -29,9 +30,6 @@ protected override void Open() args => ChangeColor(MagicMirrorCategory.FacialHair, args.marking, args.slot); _window.OnFacialHairSlotAdded += delegate () { AddSlot(MagicMirrorCategory.FacialHair); }; _window.OnFacialHairSlotRemoved += args => RemoveSlot(MagicMirrorCategory.FacialHair, args); - - _window.OnClose += Close; - _window.OpenCentered(); } private void SelectHair(MagicMirrorCategory category, string marking, int slot) @@ -65,14 +63,5 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.UpdateState(data); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _window?.Dispose(); - } } diff --git a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs index 80eca82e324..22e5bc452a0 100644 --- a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs +++ b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs @@ -1,6 +1,7 @@ using JetBrains.Annotations; using Content.Shared.MassMedia.Systems; using Content.Shared.MassMedia.Components; +using Robust.Client.UserInterface; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -9,8 +10,6 @@ namespace Content.Client.MassMedia.Ui; [UsedImplicitly] public sealed class NewsWriterBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [ViewVariables] private NewsWriterMenu? _menu; @@ -21,10 +20,7 @@ public NewsWriterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, u protected override void Open() { - _menu = new NewsWriterMenu(_gameTiming); - - _menu.OpenCentered(); - _menu.OnClose += Close; + _menu = this.CreateWindow<NewsWriterMenu>(); _menu.ArticleEditorPanel.PublishButtonPressed += OnPublishButtonPressed; _menu.DeleteButtonPressed += OnDeleteButtonPressed; @@ -32,16 +28,6 @@ protected override void Open() SendMessage(new NewsWriterArticlesRequestMessage()); } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Close(); - _menu?.Dispose(); - } - protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); diff --git a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs index e2d57935e3a..c059ce785af 100644 --- a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs +++ b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs @@ -10,17 +10,17 @@ namespace Content.Client.MassMedia.Ui; [GenerateTypedNameReferences] public sealed partial class NewsWriterMenu : FancyWindow { - private readonly IGameTiming _gameTiming; + [Dependency] private readonly IGameTiming _gameTiming = default!; private TimeSpan? _nextPublish; public event Action<int>? DeleteButtonPressed; - public NewsWriterMenu(IGameTiming gameTiming) + public NewsWriterMenu() { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); - _gameTiming = gameTiming; ContentsContainer.RectClipContent = false; // Customize scrollbar width and margin. This is not possible in xaml diff --git a/Content.Client/Mech/Ui/MechBoundUserInterface.cs b/Content.Client/Mech/Ui/MechBoundUserInterface.cs index 4172bdc90f1..558678718f1 100644 --- a/Content.Client/Mech/Ui/MechBoundUserInterface.cs +++ b/Content.Client/Mech/Ui/MechBoundUserInterface.cs @@ -3,6 +3,7 @@ using Content.Shared.Mech.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Mech.Ui; @@ -20,9 +21,8 @@ protected override void Open() { base.Open(); - _menu = new(Owner); - - _menu.OnClose += Close; + _menu = this.CreateWindow<MechMenu>(); + _menu.SetEntity(Owner); _menu.OpenCenteredLeft(); _menu.OnRemoveButtonPressed += uid => @@ -60,16 +60,6 @@ public void UpdateEquipmentControls(MechBoundUiState state) } } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (!disposing) - return; - - _menu?.Close(); - } - public UIFragment? GetEquipmentUi(EntityUid? uid) { var component = EntMan.GetComponentOrNull<UIFragmentComponent>(uid); @@ -77,4 +67,3 @@ protected override void Dispose(bool disposing) return component?.Ui; } } - diff --git a/Content.Client/Mech/Ui/MechMenu.xaml.cs b/Content.Client/Mech/Ui/MechMenu.xaml.cs index fad76488086..6f39bc386ee 100644 --- a/Content.Client/Mech/Ui/MechMenu.xaml.cs +++ b/Content.Client/Mech/Ui/MechMenu.xaml.cs @@ -16,14 +16,15 @@ public sealed partial class MechMenu : FancyWindow public event Action<EntityUid>? OnRemoveButtonPressed; - public MechMenu(EntityUid mech) + public MechMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + } - _mech = mech; - - MechView.SetEntity(mech); + public void SetEntity(EntityUid uid) + { + MechView.SetEntity(uid); } public void UpdateMechStats() diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs index 39788809871..b1f239cd78e 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs @@ -1,4 +1,5 @@ using Content.Shared.Medical.CrewMonitoring; +using Robust.Client.UserInterface; namespace Content.Client.Medical.CrewMonitoring; @@ -14,7 +15,7 @@ public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owne protected override void Open() { EntityUid? gridUid = null; - string stationName = string.Empty; + var stationName = string.Empty; if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform)) { @@ -26,10 +27,8 @@ protected override void Open() } } - _menu = new CrewMonitoringWindow(stationName, gridUid); - - _menu.OpenCentered(); - _menu.OnClose += Close; + _menu = this.CreateWindow<CrewMonitoringWindow>(); + _menu.Set(stationName, gridUid); } protected override void UpdateState(BoundUserInterfaceState state) @@ -44,13 +43,4 @@ protected override void UpdateState(BoundUserInterfaceState state) break; } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Dispose(); - } } diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs index 863412e5532..0709921ae8b 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs @@ -23,22 +23,27 @@ namespace Content.Client.Medical.CrewMonitoring; [GenerateTypedNameReferences] public sealed partial class CrewMonitoringWindow : FancyWindow { - private readonly IEntityManager _entManager; - private readonly IPrototypeManager _prototypeManager; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; private readonly SpriteSystem _spriteSystem; private NetEntity? _trackedEntity; private bool _tryToScrollToListFocus; private Texture? _blipTexture; - public CrewMonitoringWindow(string stationName, EntityUid? mapUid) + public CrewMonitoringWindow() { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); - _entManager = IoCManager.Resolve<IEntityManager>(); - _prototypeManager = IoCManager.Resolve<IPrototypeManager>(); _spriteSystem = _entManager.System<SpriteSystem>(); + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + + } + + public void Set(string stationName, EntityUid? mapUid) + { _blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform)) @@ -49,8 +54,6 @@ public CrewMonitoringWindow(string stationName, EntityUid? mapUid) StationName.AddStyleClass("LabelBig"); StationName.Text = stationName; - - NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; NavMap.ForceNavMapUpdate(); } @@ -254,7 +257,7 @@ private void PopulateDepartmentList(IEnumerable<SuitSensorStatus> departmentSens mainContainer.AddChild(jobContainer); // Job icon - if (_prototypeManager.TryIndex<StatusIconPrototype>(sensor.JobIcon, out var proto)) + if (_prototypeManager.TryIndex<JobIconPrototype>(sensor.JobIcon, out var proto)) { var jobIcon = new TextureRect() { diff --git a/Content.Client/MouseRotator/MouseRotatorSystem.cs b/Content.Client/MouseRotator/MouseRotatorSystem.cs index 363c8248c96..a60990301f6 100644 --- a/Content.Client/MouseRotator/MouseRotatorSystem.cs +++ b/Content.Client/MouseRotator/MouseRotatorSystem.cs @@ -2,6 +2,7 @@ using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; +using Robust.Client.Replays.Loading; using Robust.Shared.Map; using Robust.Shared.Timing; @@ -37,7 +38,7 @@ public override void Update(float frameTime) if (mapPos.MapId == MapId.Nullspace) return; - var angle = (mapPos.Position - xform.MapPosition.Position).ToWorldAngle(); + var angle = (mapPos.Position - _transform.GetMapCoordinates(player.Value, xform: xform).Position).ToWorldAngle(); var curRot = _transform.GetWorldRotation(xform); diff --git a/Content.Client/Movement/Systems/JetpackSystem.cs b/Content.Client/Movement/Systems/JetpackSystem.cs index b7f5e48821f..6f9ef830408 100644 --- a/Content.Client/Movement/Systems/JetpackSystem.cs +++ b/Content.Client/Movement/Systems/JetpackSystem.cs @@ -63,15 +63,15 @@ public override void Update(float frameTime) private void CreateParticles(EntityUid uid) { + var uidXform = Transform(uid); // Don't show particles unless the user is moving. - if (Container.TryGetContainingContainer(uid, out var container) && + if (Container.TryGetContainingContainer((uid, uidXform, null), out var container) && TryComp<PhysicsComponent>(container.Owner, out var body) && body.LinearVelocity.LengthSquared() < 1f) { return; } - var uidXform = Transform(uid); var coordinates = uidXform.Coordinates; var gridUid = coordinates.GetGridUid(EntityManager); diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs index 80c98f143b9..f85220a9266 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Client.NetworkConfigurator.Systems; using Content.Shared.DeviceNetwork; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; namespace Content.Client.NetworkConfigurator; @@ -35,14 +36,12 @@ protected override void Open() switch (UiKey) { case NetworkConfiguratorUiKey.List: - _listMenu = new NetworkConfiguratorListMenu(this); - _listMenu.OnClose += Close; + _listMenu = this.CreateWindow<NetworkConfiguratorListMenu>(); _listMenu.ClearButton.OnPressed += _ => OnClearButtonPressed(); - _listMenu.OpenCenteredRight(); + _listMenu.OnRemoveAddress += OnRemoveButtonPressed; break; case NetworkConfiguratorUiKey.Configure: - _configurationMenu = new NetworkConfiguratorConfigurationMenu(); - _configurationMenu.OnClose += Close; + _configurationMenu = this.CreateWindow<NetworkConfiguratorConfigurationMenu>(); _configurationMenu.Set.OnPressed += _ => OnConfigButtonPressed(NetworkConfiguratorButtonKey.Set); _configurationMenu.Add.OnPressed += _ => OnConfigButtonPressed(NetworkConfiguratorButtonKey.Add); //_configurationMenu.Edit.OnPressed += _ => OnConfigButtonPressed(NetworkConfiguratorButtonKey.Edit); @@ -50,12 +49,24 @@ protected override void Open() _configurationMenu.Copy.OnPressed += _ => OnConfigButtonPressed(NetworkConfiguratorButtonKey.Copy); _configurationMenu.Show.OnPressed += OnShowPressed; _configurationMenu.Show.Pressed = _netConfig.ConfiguredListIsTracked(Owner); - _configurationMenu.OpenCentered(); + _configurationMenu.OnRemoveAddress += OnRemoveButtonPressed; break; case NetworkConfiguratorUiKey.Link: - _linkMenu = new NetworkConfiguratorLinkMenu(this); - _linkMenu.OnClose += Close; - _linkMenu.OpenCentered(); + _linkMenu = this.CreateWindow<NetworkConfiguratorLinkMenu>(); + _linkMenu.OnLinkDefaults += args => + { + SendMessage(new NetworkConfiguratorLinksSaveMessage(args)); + }; + + _linkMenu.OnToggleLink += (left, right) => + { + SendMessage(new NetworkConfiguratorToggleLinkMessage(left, right)); + }; + + _linkMenu.OnClearLinks += () => + { + SendMessage(new NetworkConfiguratorClearLinksMessage()); + }; break; } } @@ -83,16 +94,6 @@ protected override void UpdateState(BoundUserInterfaceState state) } } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - - _linkMenu?.Dispose(); - _listMenu?.Dispose(); - _configurationMenu?.Dispose(); - } - private void OnClearButtonPressed() { SendMessage(new NetworkConfiguratorClearDevicesMessage()); diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml.cs index 19d04cd3464..fcd2f759187 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml.cs +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml.cs @@ -9,17 +9,23 @@ namespace Content.Client.NetworkConfigurator; [GenerateTypedNameReferences] public sealed partial class NetworkConfiguratorConfigurationMenu : FancyWindow { + public event Action<string>? OnRemoveAddress; + public NetworkConfiguratorConfigurationMenu() { RobustXamlLoader.Load(this); Clear.StyleClasses.Add(StyleBase.ButtonOpenLeft); Clear.StyleClasses.Add(StyleNano.StyleClassButtonColorRed); + DeviceList.OnRemoveAddress += args => + { + OnRemoveAddress?.Invoke(args); + }; } public void UpdateState(DeviceListUserInterfaceState state) { - DeviceList.UpdateState(null, state.DeviceList); + DeviceList.UpdateState(state.DeviceList, false); Count.Text = Loc.GetString("network-configurator-ui-count-label", ("count", state.DeviceList.Count)); } diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorDeviceList.xaml.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorDeviceList.xaml.cs index 8cfa97dc6c2..e75c60058cb 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorDeviceList.xaml.cs +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorDeviceList.xaml.cs @@ -7,17 +7,19 @@ namespace Content.Client.NetworkConfigurator; [GenerateTypedNameReferences] public sealed partial class NetworkConfiguratorDeviceList : ScrollContainer { - public void UpdateState(NetworkConfiguratorBoundUserInterface? ui, HashSet<(string address, string name)> devices) + public event Action<string>? OnRemoveAddress; + + public void UpdateState(HashSet<(string address, string name)> devices, bool ui) { DeviceList.RemoveAllChildren(); foreach (var device in devices) { - DeviceList.AddChild(BuildDeviceListRow(ui, device)); + DeviceList.AddChild(BuildDeviceListRow(device, ui)); } } - private static BoxContainer BuildDeviceListRow(NetworkConfiguratorBoundUserInterface? ui, (string address, string name) savedDevice) + private BoxContainer BuildDeviceListRow((string address, string name) savedDevice, bool ui) { var row = new BoxContainer() { @@ -48,10 +50,10 @@ private static BoxContainer BuildDeviceListRow(NetworkConfiguratorBoundUserInter row.AddChild(name); row.AddChild(address); - if (ui != null) + if (ui) { row.AddChild(removeButton); - removeButton.OnPressed += _ => ui.OnRemoveButtonPressed(savedDevice.address); + removeButton.OnPressed += _ => OnRemoveAddress?.Invoke(savedDevice.address); } return row; diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkMenu.xaml.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkMenu.xaml.cs index c04b42f249b..8cdffd16af6 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkMenu.xaml.cs +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkMenu.xaml.cs @@ -18,20 +18,20 @@ public sealed partial class NetworkConfiguratorLinkMenu : FancyWindow private readonly LinksRender _links; - private readonly List<SourcePortPrototype> _sources = new(); private readonly List<SinkPortPrototype> _sinks = new(); - private readonly NetworkConfiguratorBoundUserInterface _userInterface; - private (ButtonPosition position, string id, int index)? _selectedButton; private List<(string left, string right)>? _defaults; - public NetworkConfiguratorLinkMenu(NetworkConfiguratorBoundUserInterface userInterface) + public event Action? OnClearLinks; + public event Action<string, string>? OnToggleLink; + public event Action<List<(string left, string right)>>? OnLinkDefaults; + + public NetworkConfiguratorLinkMenu() { - _userInterface = userInterface; RobustXamlLoader.Load(this); var footerStyleBox = new StyleBoxFlat() @@ -52,7 +52,7 @@ public NetworkConfiguratorLinkMenu(NetworkConfiguratorBoundUserInterface userInt ButtonOk.OnPressed += _ => Close(); ButtonLinkDefault.OnPressed += _ => LinkDefaults(); - ButtonClear.OnPressed += _ => _userInterface.SendMessage(new NetworkConfiguratorClearLinksMessage()); + ButtonClear.OnPressed += _ => OnClearLinks?.Invoke(); } public void UpdateState(DeviceLinkUserInterfaceState linkState) @@ -98,7 +98,7 @@ private void LinkDefaults() if (_defaults == default) return; - _userInterface.SendMessage(new NetworkConfiguratorLinksSaveMessage(_defaults)); + OnLinkDefaults?.Invoke(_defaults); } private Button CreateButton(ButtonPosition position, string name, string description, string id, int index) @@ -138,7 +138,7 @@ private void OnButtonPressed(BaseButton.ButtonEventArgs args, ButtonPosition pos var left = _selectedButton.Value.position == ButtonPosition.Left ? _selectedButton.Value.id : id; var right = _selectedButton.Value.position == ButtonPosition.Left ? id : _selectedButton.Value.id; - _userInterface.SendMessage(new NetworkConfiguratorToggleLinkMessage(left, right)); + OnToggleLink?.Invoke(left, right); args.Button.Pressed = false; diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorListMenu.xaml.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorListMenu.xaml.cs index fb4aec1974b..6294facaeed 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorListMenu.xaml.cs +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorListMenu.xaml.cs @@ -9,17 +9,20 @@ namespace Content.Client.NetworkConfigurator; [GenerateTypedNameReferences] public sealed partial class NetworkConfiguratorListMenu : FancyWindow { - private readonly NetworkConfiguratorBoundUserInterface _ui; - public NetworkConfiguratorListMenu(NetworkConfiguratorBoundUserInterface ui) + public event Action<string>? OnRemoveAddress; + + public NetworkConfiguratorListMenu() { RobustXamlLoader.Load(this); - - _ui = ui; + DeviceList.OnRemoveAddress += args => + { + OnRemoveAddress?.Invoke(args); + }; } public void UpdateState(NetworkConfiguratorUserInterfaceState state) { DeviceCountLabel.Text = Loc.GetString("network-configurator-ui-count-label", ("count", state.DeviceList.Count)); - DeviceList.UpdateState(_ui, state.DeviceList); + DeviceList.UpdateState(state.DeviceList, true); } } diff --git a/Content.Client/Ninja/Systems/ItemCreatorSystem.cs b/Content.Client/Ninja/Systems/ItemCreatorSystem.cs new file mode 100644 index 00000000000..9ab62cc12db --- /dev/null +++ b/Content.Client/Ninja/Systems/ItemCreatorSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Ninja.Systems; + +namespace Content.Client.Ninja.Systems; + +public sealed class ItemCreatorSystem : SharedItemCreatorSystem; diff --git a/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs b/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs index 7758c3d7e2b..5b07b1588fd 100644 --- a/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs +++ b/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs @@ -2,9 +2,4 @@ namespace Content.Client.Ninja.Systems; -/// <summary> -/// Does nothing special, only exists to provide a client implementation. -/// </summary> -public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem -{ -} +public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem; diff --git a/Content.Client/Ninja/Systems/NinjaSuitSystem.cs b/Content.Client/Ninja/Systems/NinjaSuitSystem.cs index fde1801b37d..852ea8af46e 100644 --- a/Content.Client/Ninja/Systems/NinjaSuitSystem.cs +++ b/Content.Client/Ninja/Systems/NinjaSuitSystem.cs @@ -1,24 +1,5 @@ -using Content.Shared.Clothing.EntitySystems; -using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; namespace Content.Client.Ninja.Systems; -/// <summary> -/// Disables cloak prediction since client has no knowledge of battery power. -/// Cloak will still be enabled after server tells it. -/// </summary> -public sealed class NinjaSuitSystem : SharedNinjaSuitSystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<NinjaSuitComponent, AttemptStealthEvent>(OnAttemptStealth); - } - - private void OnAttemptStealth(EntityUid uid, NinjaSuitComponent comp, AttemptStealthEvent args) - { - args.Cancel(); - } -} +public sealed class NinjaSuitSystem : SharedNinjaSuitSystem; diff --git a/Content.Client/Ninja/Systems/NinjaSystem.cs b/Content.Client/Ninja/Systems/NinjaSystem.cs index aa2fa2047f1..958dc6a5d9a 100644 --- a/Content.Client/Ninja/Systems/NinjaSystem.cs +++ b/Content.Client/Ninja/Systems/NinjaSystem.cs @@ -2,11 +2,4 @@ namespace Content.Client.Ninja.Systems; -/// <summary> -/// Currently does nothing special clientside. -/// All functionality is in shared and server. -/// Only exists to prevent crashing. -/// </summary> -public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem -{ -} +public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem; diff --git a/Content.Client/Ninja/Systems/SpiderChargeSystem.cs b/Content.Client/Ninja/Systems/SpiderChargeSystem.cs new file mode 100644 index 00000000000..b107fd3867d --- /dev/null +++ b/Content.Client/Ninja/Systems/SpiderChargeSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Ninja.Systems; + +namespace Content.Client.Ninja.Systems; + +public sealed class SpiderChargeSystem : SharedSpiderChargeSystem; diff --git a/Content.Client/Nuke/NukeBoundUserInterface.cs b/Content.Client/Nuke/NukeBoundUserInterface.cs index 59fbc5b319b..2e150423734 100644 --- a/Content.Client/Nuke/NukeBoundUserInterface.cs +++ b/Content.Client/Nuke/NukeBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Nuke; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Nuke { @@ -11,15 +12,13 @@ public sealed class NukeBoundUserInterface : BoundUserInterface [ViewVariables] private NukeMenu? _menu; - public NukeBoundUserInterface([NotNull] EntityUid owner, [NotNull] Enum uiKey) : base(owner, uiKey) + public NukeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } protected override void Open() { - _menu = new NukeMenu(); - _menu.OpenCentered(); - _menu.OnClose += Close; + _menu = this.CreateWindow<NukeMenu>(); _menu.OnKeypadButtonPressed += i => { @@ -62,15 +61,5 @@ protected override void UpdateState(BoundUserInterfaceState state) break; } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Close(); - _menu?.Dispose(); - } } } diff --git a/Content.Client/NukeOps/WarDeclaratorBoundUserInterface.cs b/Content.Client/NukeOps/WarDeclaratorBoundUserInterface.cs index ec055b3240c..ad4f1a75d47 100644 --- a/Content.Client/NukeOps/WarDeclaratorBoundUserInterface.cs +++ b/Content.Client/NukeOps/WarDeclaratorBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Chat; using Content.Shared.NukeOps; using JetBrains.Annotations; +using Robust.Client.UserInterface; using Robust.Shared.Configuration; using Robust.Shared.Timing; @@ -11,8 +12,6 @@ namespace Content.Client.NukeOps; public sealed class WarDeclaratorBoundUserInterface : BoundUserInterface { [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly ILocalizationManager _localizationManager = default!; [ViewVariables] private WarDeclaratorWindow? _window; @@ -23,13 +22,7 @@ protected override void Open() { base.Open(); - _window = new WarDeclaratorWindow(_gameTiming, _localizationManager); - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<WarDeclaratorWindow>(); _window.OnActivated += OnWarDeclaratorActivated; } @@ -42,13 +35,6 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.UpdateState(cast); } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - _window?.Dispose(); - } - private void OnWarDeclaratorActivated(string message) { var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength); diff --git a/Content.Client/NukeOps/WarDeclaratorWindow.xaml.cs b/Content.Client/NukeOps/WarDeclaratorWindow.xaml.cs index b4a3f1c7fa5..aeceae13275 100644 --- a/Content.Client/NukeOps/WarDeclaratorWindow.xaml.cs +++ b/Content.Client/NukeOps/WarDeclaratorWindow.xaml.cs @@ -11,7 +11,8 @@ namespace Content.Client.NukeOps; [GenerateTypedNameReferences] public sealed partial class WarDeclaratorWindow : FancyWindow { - private readonly IGameTiming _gameTiming; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ILocalizationManager _localizationManager = default!; public event Action<string>? OnActivated; @@ -19,15 +20,13 @@ public sealed partial class WarDeclaratorWindow : FancyWindow private TimeSpan _shuttleDisabledTime; private WarConditionStatus _status; - public WarDeclaratorWindow(IGameTiming gameTiming, ILocalizationManager localizationManager) + public WarDeclaratorWindow() { RobustXamlLoader.Load(this); - _gameTiming = gameTiming; - WarButton.OnPressed += (_) => OnActivated?.Invoke(Rope.Collapse(MessageEdit.TextRope)); - MessageEdit.Placeholder = new Rope.Leaf(localizationManager.GetString("war-declarator-message-placeholder")); + MessageEdit.Placeholder = new Rope.Leaf(_localizationManager.GetString("war-declarator-message-placeholder")); } protected override void FrameUpdate(FrameEventArgs args) diff --git a/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs b/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs new file mode 100644 index 00000000000..c708c6fe7d2 --- /dev/null +++ b/Content.Client/Nutrition/EntitySystems/ClientFoodSequenceSystem.cs @@ -0,0 +1,62 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Client.GameObjects; + +namespace Content.Client.Nutrition.EntitySystems; + +public sealed class ClientFoodSequenceSystem : SharedFoodSequenceSystem +{ + public override void Initialize() + { + SubscribeLocalEvent<FoodSequenceStartPointComponent, AfterAutoHandleStateEvent>(OnHandleState); + } + + private void OnHandleState(Entity<FoodSequenceStartPointComponent> start, ref AfterAutoHandleStateEvent args) + { + if (!TryComp<SpriteComponent>(start, out var sprite)) + return; + + UpdateFoodVisuals(start, sprite); + } + + private void UpdateFoodVisuals(Entity<FoodSequenceStartPointComponent> start, SpriteComponent? sprite = null) + { + if (!Resolve(start, ref sprite, false)) + return; + + //Remove old layers + foreach (var key in start.Comp.RevealedLayers) + { + sprite.RemoveLayer(key); + } + start.Comp.RevealedLayers.Clear(); + + //Add new layers + var counter = 0; + foreach (var state in start.Comp.FoodLayers) + { + if (state.Sprite is null) + continue; + + var keyCode = $"food-layer-{counter}"; + start.Comp.RevealedLayers.Add(keyCode); + + sprite.LayerMapTryGet(start.Comp.TargetLayerMap, out var index); + + if (start.Comp.InverseLayers) + index++; + + sprite.AddBlankLayer(index); + sprite.LayerMapSet(keyCode, index); + sprite.LayerSetSprite(index, state.Sprite); + sprite.LayerSetScale(index, state.Scale); + + //Offset the layer + var layerPos = start.Comp.StartPosition; + layerPos += (start.Comp.Offset * counter) + state.LocalOffset; + sprite.LayerSetOffset(index, layerPos); + + counter++; + } + } +} diff --git a/Content.Client/Nyanotrasen/Mail/MailSystem.cs b/Content.Client/Nyanotrasen/Mail/MailSystem.cs index de63d74099b..5827aaa473c 100644 --- a/Content.Client/Nyanotrasen/Mail/MailSystem.cs +++ b/Content.Client/Nyanotrasen/Mail/MailSystem.cs @@ -40,7 +40,7 @@ protected override void OnAppearanceChange(EntityUid uid, MailComponent componen if (string.IsNullOrEmpty(job)) job = "JobIconUnknown"; - if (!_prototypeManager.TryIndex<StatusIconPrototype>(job, out var icon)) + if (!_prototypeManager.TryIndex<JobIconPrototype>(job, out var icon)) { args.Sprite.LayerSetTexture(MailVisualLayers.JobStamp, _spriteSystem.Frame0(_prototypeManager.Index("JobIconUnknown"))); return; diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs index c3a8e664705..c03e50dda00 100644 --- a/Content.Client/Options/UI/OptionsMenu.xaml.cs +++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs @@ -1,3 +1,4 @@ +using Content.Client.Options.UI.Tabs; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; diff --git a/Content.Client/Outline/TargetOutlineSystem.cs b/Content.Client/Outline/TargetOutlineSystem.cs index 2a6867f51f5..df57578b1f5 100644 --- a/Content.Client/Outline/TargetOutlineSystem.cs +++ b/Content.Client/Outline/TargetOutlineSystem.cs @@ -22,6 +22,7 @@ public sealed class TargetOutlineSystem : EntitySystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; private bool _enabled = false; @@ -137,7 +138,7 @@ private void HighlightTargets() // check the entity whitelist if (valid && Whitelist != null) - valid = Whitelist.IsValid(entity); + valid = _whitelistSystem.IsWhitelistPass(Whitelist, entity); // and check the cancellable event if (valid && ValidationEvent != null) diff --git a/Content.Client/Overlays/EntityHealthBarOverlay.cs b/Content.Client/Overlays/EntityHealthBarOverlay.cs index c96225c0c65..193635bda7b 100644 --- a/Content.Client/Overlays/EntityHealthBarOverlay.cs +++ b/Content.Client/Overlays/EntityHealthBarOverlay.cs @@ -1,14 +1,16 @@ +using System.Numerics; +using Content.Client.StatusIcon; +using Content.Client.UserInterface.Systems; using Content.Shared.Damage; using Content.Shared.FixedPoint; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Enums; -using System.Numerics; -using Content.Shared.StatusIcon.Components; -using Content.Client.UserInterface.Systems; using Robust.Shared.Prototypes; using static Robust.Shared.Maths.Color; @@ -20,19 +22,27 @@ namespace Content.Client.Overlays; public sealed class EntityHealthBarOverlay : Overlay { private readonly IEntityManager _entManager; + private readonly IPrototypeManager _prototype; + private readonly SharedTransformSystem _transform; private readonly MobStateSystem _mobStateSystem; private readonly MobThresholdSystem _mobThresholdSystem; + private readonly StatusIconSystem _statusIconSystem; private readonly ProgressColorSystem _progressColor; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; public HashSet<string> DamageContainers = new(); + public ProtoId<HealthIconPrototype>? StatusIcon; - public EntityHealthBarOverlay(IEntityManager entManager) + public EntityHealthBarOverlay(IEntityManager entManager, IPrototypeManager prototype) { _entManager = entManager; + _prototype = prototype; _transform = _entManager.System<SharedTransformSystem>(); _mobStateSystem = _entManager.System<MobStateSystem>(); _mobThresholdSystem = _entManager.System<MobThresholdSystem>(); + _statusIconSystem = _entManager.System<StatusIconSystem>(); _progressColor = _entManager.System<ProgressColorSystem>(); } @@ -45,6 +55,7 @@ protected override void Draw(in OverlayDrawArgs args) const float scale = 1f; var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale)); var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation); + _prototype.TryIndex(StatusIcon, out var statusIcon); var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, SpriteComponent>(); while (query.MoveNext(out var uid, @@ -53,31 +64,27 @@ protected override void Draw(in OverlayDrawArgs args) out var damageableComponent, out var spriteComponent)) { - if (_entManager.TryGetComponent<MetaDataComponent>(uid, out var metaDataComponent) && - metaDataComponent.Flags.HasFlag(MetaDataFlags.InContainer)) - { + if (statusIcon != null && !_statusIconSystem.IsVisible((uid, _entManager.GetComponent<MetaDataComponent>(uid)), statusIcon)) continue; - } + // We want the stealth user to still be able to see his health bar himself if (!xformQuery.TryGetComponent(uid, out var xform) || xform.MapID != args.MapId) - { continue; - } if (damageableComponent.DamageContainerID == null || !DamageContainers.Contains(damageableComponent.DamageContainerID)) - { continue; - } // we use the status icon component bounds if specified otherwise use sprite var bounds = _entManager.GetComponentOrNull<StatusIconComponent>(uid)?.Bounds ?? spriteComponent.Bounds; var worldPos = _transform.GetWorldPosition(xform, xformQuery); if (!bounds.Translated(worldPos).Intersects(args.WorldAABB)) - { continue; - } + + // we are all progressing towards death every day + if (CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent) is not { } deathProgress) + continue; var worldPosition = _transform.GetWorldPosition(xform); var worldMatrix = Matrix3Helpers.CreateTranslation(worldPosition); @@ -91,10 +98,6 @@ protected override void Draw(in OverlayDrawArgs args) var widthOfMob = bounds.Width * EyeManager.PixelsPerMeter; var position = new Vector2(-widthOfMob / EyeManager.PixelsPerMeter / 2, yOffset / EyeManager.PixelsPerMeter); - - // we are all progressing towards death every day - (float ratio, bool inCrit) deathProgress = CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent); - var color = GetProgressColor(deathProgress.ratio, deathProgress.inCrit); // Hardcoded width of the progress bar because it doesn't match the texture. @@ -122,10 +125,13 @@ protected override void Draw(in OverlayDrawArgs args) /// <summary> /// Returns a ratio between 0 and 1, and whether the entity is in crit. /// </summary> - private (float, bool) CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds) + private (float ratio, bool inCrit)? CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds) { if (_mobStateSystem.IsAlive(uid, component)) { + if (dmg.HealthBarThreshold != null && dmg.TotalDamage < dmg.HealthBarThreshold) + return null; + if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholds) && !_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out threshold, thresholds)) return (1, false); diff --git a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs index 8f23cd510cb..c353b172729 100644 --- a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs +++ b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs @@ -19,10 +19,10 @@ public override void Initialize() private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent component, ref GetStatusIconsEvent ev) { - if (!IsActive || ev.InContainer) + if (!IsActive) return; - if (_prototype.TryIndex<StatusIconPrototype>(component.StatusIcon.Id, out var iconPrototype)) + if (_prototype.TryIndex(component.StatusIcon, out var iconPrototype)) ev.StatusIcons.Add(iconPrototype); } } diff --git a/Content.Client/Overlays/ShowHealthBarsSystem.cs b/Content.Client/Overlays/ShowHealthBarsSystem.cs index 170f552cf3f..1eb712a8988 100644 --- a/Content.Client/Overlays/ShowHealthBarsSystem.cs +++ b/Content.Client/Overlays/ShowHealthBarsSystem.cs @@ -2,6 +2,8 @@ using Content.Shared.Overlays; using Robust.Client.Graphics; using System.Linq; +using Robust.Client.Player; +using Robust.Shared.Prototypes; namespace Content.Client.Overlays; @@ -11,6 +13,7 @@ namespace Content.Client.Overlays; public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComponent> { [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; private EntityHealthBarOverlay _overlay = default!; @@ -18,16 +21,21 @@ public override void Initialize() { base.Initialize(); - _overlay = new(EntityManager); + _overlay = new(EntityManager, _prototype); } protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthBarsComponent> component) { base.UpdateInternal(component); - foreach (var damageContainerId in component.Components.SelectMany(x => x.DamageContainers)) + foreach (var comp in component.Components) { - _overlay.DamageContainers.Add(damageContainerId); + foreach (var damageContainerId in comp.DamageContainers) + { + _overlay.DamageContainers.Add(damageContainerId); + } + + _overlay.StatusIcon = comp.HealthStatusIcon; } if (!_overlayMan.HasOverlay<EntityHealthBarOverlay>()) diff --git a/Content.Client/Overlays/ShowHealthIconsSystem.cs b/Content.Client/Overlays/ShowHealthIconsSystem.cs index a546cf4d828..8c22c78f17c 100644 --- a/Content.Client/Overlays/ShowHealthIconsSystem.cs +++ b/Content.Client/Overlays/ShowHealthIconsSystem.cs @@ -24,7 +24,6 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<DamageableComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent); - } protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthIconsComponent> component) @@ -46,7 +45,7 @@ protected override void DeactivateInternal() private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args) { - if (!IsActive || args.InContainer) + if (!IsActive) return; var healthIcons = DecideHealthIcons(entity); @@ -54,17 +53,17 @@ private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetSt args.StatusIcons.AddRange(healthIcons); } - private IReadOnlyList<StatusIconPrototype> DecideHealthIcons(Entity<DamageableComponent> entity) + private IReadOnlyList<HealthIconPrototype> DecideHealthIcons(Entity<DamageableComponent> entity) { var damageableComponent = entity.Comp; if (damageableComponent.DamageContainerID == null || !DamageContainers.Contains(damageableComponent.DamageContainerID)) { - return Array.Empty<StatusIconPrototype>(); + return Array.Empty<HealthIconPrototype>(); } - var result = new List<StatusIconPrototype>(); + var result = new List<HealthIconPrototype>(); // Here you could check health status, diseases, mind status, etc. and pick a good icon, or multiple depending on whatever. if (damageableComponent?.DamageContainerID == "Biological") diff --git a/Content.Client/Overlays/ShowHungerIconsSystem.cs b/Content.Client/Overlays/ShowHungerIconsSystem.cs index b1c0f3a1a0c..6b0d575a810 100644 --- a/Content.Client/Overlays/ShowHungerIconsSystem.cs +++ b/Content.Client/Overlays/ShowHungerIconsSystem.cs @@ -18,7 +18,7 @@ public override void Initialize() private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent component, ref GetStatusIconsEvent ev) { - if (!IsActive || ev.InContainer) + if (!IsActive) return; if (_hunger.TryGetStatusIconPrototype(component, out var iconPrototype)) diff --git a/Content.Client/Overlays/ShowJobIconsSystem.cs b/Content.Client/Overlays/ShowJobIconsSystem.cs index e24b99f3e87..e5ba9b813f0 100644 --- a/Content.Client/Overlays/ShowJobIconsSystem.cs +++ b/Content.Client/Overlays/ShowJobIconsSystem.cs @@ -13,7 +13,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<JobIconPrototype>] private const string JobIconForNoId = "JobIconNoId"; public override void Initialize() @@ -25,7 +25,7 @@ public override void Initialize() private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent ev) { - if (!IsActive || ev.InContainer) + if (!IsActive) return; var iconId = JobIconForNoId; @@ -52,7 +52,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref Get } } - if (_prototype.TryIndex<StatusIconPrototype>(iconId, out var iconPrototype)) + if (_prototype.TryIndex<JobIconPrototype>(iconId, out var iconPrototype)) ev.StatusIcons.Add(iconPrototype); else Log.Error($"Invalid job icon prototype: {iconPrototype}"); diff --git a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs index 8bf39b875f6..cdb9c54fdfa 100644 --- a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs +++ b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs @@ -19,10 +19,10 @@ public override void Initialize() private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev) { - if (!IsActive || ev.InContainer) + if (!IsActive) return; - if (_prototype.TryIndex<StatusIconPrototype>(component.MindShieldStatusIcon.Id, out var iconPrototype)) + if (_prototype.TryIndex(component.MindShieldStatusIcon, out var iconPrototype)) ev.StatusIcons.Add(iconPrototype); } } diff --git a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs index 660ef198e14..9d4599b8233 100644 --- a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs +++ b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs @@ -19,11 +19,10 @@ public override void Initialize() private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent component, ref GetStatusIconsEvent ev) { - if (!IsActive || ev.InContainer) + if (!IsActive) return; - if (_prototype.TryIndex<StatusIconPrototype>(component.SyndStatusIcon, out var iconPrototype)) + if (_prototype.TryIndex<FactionIconPrototype>(component.SyndStatusIcon, out var iconPrototype)) ev.StatusIcons.Add(iconPrototype); } } - diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs index b08aa4340b2..44be1f7a67f 100644 --- a/Content.Client/Overlays/ShowThirstIconsSystem.cs +++ b/Content.Client/Overlays/ShowThirstIconsSystem.cs @@ -18,7 +18,7 @@ public override void Initialize() private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref GetStatusIconsEvent ev) { - if (!IsActive || ev.InContainer) + if (!IsActive) return; if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype)) diff --git a/Content.Client/PDA/PdaBoundUserInterface.cs b/Content.Client/PDA/PdaBoundUserInterface.cs index 07352b512b0..37ce9c4280f 100644 --- a/Content.Client/PDA/PdaBoundUserInterface.cs +++ b/Content.Client/PDA/PdaBoundUserInterface.cs @@ -21,9 +21,16 @@ public PdaBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) protected override void Open() { base.Open(); - _menu = new PdaMenu(); + + if (_menu == null) + CreateMenu(); + } + + private void CreateMenu() + { + _menu = this.CreateWindow<PdaMenu>(); _menu.OpenCenteredLeft(); - _menu.OnClose += Close; + _menu.FlashLightToggleButton.OnToggled += _ => { SendMessage(new PdaToggleFlashlightMessage()); @@ -88,7 +95,6 @@ protected override void UpdateState(BoundUserInterfaceState state) _menu?.UpdateState(updateState); } - protected override void AttachCartridgeUI(Control cartridgeUIFragment, string? title) { _menu?.ProgramView.AddChild(cartridgeUIFragment); @@ -110,15 +116,6 @@ protected override void UpdateAvailablePrograms(List<(EntityUid, CartridgeCompon _menu?.UpdateAvailablePrograms(programs); } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Dispose(); - } - private PdaBorderColorComponent? GetBorderColorComponent() { return EntMan.GetComponentOrNull<PdaBorderColorComponent>(Owner); diff --git a/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs b/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs index a0688523f1e..170a296ac2e 100644 --- a/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs +++ b/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.PDA; using Content.Shared.PDA.Ringer; using JetBrains.Annotations; +using Robust.Client.UserInterface; using Robust.Shared.Timing; namespace Content.Client.PDA.Ringer @@ -18,9 +19,8 @@ public RingerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey protected override void Open() { base.Open(); - _menu = new RingtoneMenu(); + _menu = this.CreateWindow<RingtoneMenu>(); _menu.OpenToLeft(); - _menu.OnClose += Close; _menu.TestRingerButton.OnPressed += _ => { diff --git a/Content.Client/Paper/UI/PaperBoundUserInterface.cs b/Content.Client/Paper/UI/PaperBoundUserInterface.cs index 4b0ac868f01..f3ad1e347e7 100644 --- a/Content.Client/Paper/UI/PaperBoundUserInterface.cs +++ b/Content.Client/Paper/UI/PaperBoundUserInterface.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Utility; using static Content.Shared.Paper.SharedPaperComponent; @@ -19,16 +20,13 @@ protected override void Open() { base.Open(); - _window = new PaperWindow(); - _window.OnClose += Close; - _window.OnSaved += Input_OnTextEntered; + _window = this.CreateWindow<PaperWindow>(); + _window.OnSaved += InputOnTextEntered; if (EntMan.TryGetComponent<PaperVisualsComponent>(Owner, out var visuals)) { _window.InitVisuals(Owner, visuals); } - - _window.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -37,7 +35,7 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.Populate((PaperBoundUserInterfaceState) state); } - private void Input_OnTextEntered(string text) + private void InputOnTextEntered(string text) { SendMessage(new PaperInputTextMessage(text)); @@ -47,11 +45,4 @@ private void Input_OnTextEntered(string text) _window.Input.CursorPosition = new TextEdit.CursorPos(0, TextEdit.LineBreakBias.Top); } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _window?.Dispose(); - } } diff --git a/Content.Client/Paper/UI/PaperWindow.xaml.cs b/Content.Client/Paper/UI/PaperWindow.xaml.cs index 7a5fd652643..f7cace642ce 100644 --- a/Content.Client/Paper/UI/PaperWindow.xaml.cs +++ b/Content.Client/Paper/UI/PaperWindow.xaml.cs @@ -17,6 +17,7 @@ namespace Content.Client.Paper.UI public sealed partial class PaperWindow : BaseWindow { [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IResourceCache _resCache = default!; private static Color DefaultTextColor = new(25, 25, 25); @@ -85,11 +86,10 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals) // Randomize the placement of any stamps based on the entity UID // so that there's some variety in different papers. StampDisplay.PlacementSeed = (int)entity; - var resCache = IoCManager.Resolve<IResourceCache>(); // Initialize the background: PaperBackground.ModulateSelfOverride = visuals.BackgroundModulate; - var backgroundImage = visuals.BackgroundImagePath != null? resCache.GetResource<TextureResource>(visuals.BackgroundImagePath) : null; + var backgroundImage = visuals.BackgroundImagePath != null? _resCache.GetResource<TextureResource>(visuals.BackgroundImagePath) : null; if (backgroundImage != null) { var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch; @@ -127,7 +127,7 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals) PaperContent.ModulateSelfOverride = visuals.ContentImageModulate; WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor; - var contentImage = visuals.ContentImagePath != null ? resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null; + var contentImage = visuals.ContentImagePath != null ? _resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null; if (contentImage != null) { // Setup the paper content texture, but keep a reference to it, as we can't set diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs index cde5ba9ef79..ff1eae36f55 100644 --- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs +++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Singularity.Components; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.ParticleAccelerator.UI { @@ -16,9 +17,10 @@ protected override void Open() { base.Open(); - _menu = new ParticleAcceleratorControlMenu(this); - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<ParticleAcceleratorControlMenu>(); + _menu.OnOverallState += SendEnableMessage; + _menu.OnPowerState += SendPowerStateMessage; + _menu.OnScanPartsRequested += SendScanPartsMessage; } public void SendEnableMessage(bool enable) @@ -40,13 +42,5 @@ protected override void UpdateState(BoundUserInterfaceState state) { _menu?.DataUpdate((ParticleAcceleratorUIState) state); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - _menu?.Dispose(); - _menu = null; - } } } diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs index c69e0271372..05a296edf39 100644 --- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs +++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs @@ -18,9 +18,10 @@ namespace Content.Client.ParticleAccelerator.UI { public sealed class ParticleAcceleratorControlMenu : BaseWindow { - private readonly ShaderInstance _greyScaleShader; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IResourceCache _cache = default!; - private readonly ParticleAcceleratorBoundUserInterface _owner; + private readonly ShaderInstance _greyScaleShader; private readonly Label _drawLabel; private readonly FastNoiseLite _drawNoiseGenerator; @@ -50,19 +51,22 @@ public sealed class ParticleAcceleratorControlMenu : BaseWindow private bool _shouldContinueAnimating; private int _maxStrength = 3; - public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owner) + public event Action<bool>? OnOverallState; + public event Action<ParticleAcceleratorPowerState>? OnPowerState; + public event Action? OnScanPartsRequested; + + public ParticleAcceleratorControlMenu() { + IoCManager.InjectDependencies(this); SetSize = new Vector2(400, 320); - _greyScaleShader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("Greyscale").Instance(); + _greyScaleShader = _protoManager.Index<ShaderPrototype>("Greyscale").Instance(); - _owner = owner; _drawNoiseGenerator = new(); _drawNoiseGenerator.SetFractalType(FastNoiseLite.FractalType.FBm); _drawNoiseGenerator.SetFrequency(0.5f); - var resourceCache = IoCManager.Resolve<IResourceCache>(); - var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); - var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); + var font = _cache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); + var panelTex = _cache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); MouseFilter = MouseFilterMode.Stop; @@ -112,7 +116,8 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne Text = Loc.GetString("particle-accelerator-control-menu-off-button"), StyleClasses = { StyleBase.ButtonOpenRight }, }; - _offButton.OnPressed += args => owner.SendEnableMessage(false); + + _offButton.OnPressed += args => OnOverallState?.Invoke(false); _onButton = new Button { @@ -120,7 +125,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne Text = Loc.GetString("particle-accelerator-control-menu-on-button"), StyleClasses = { StyleBase.ButtonOpenLeft }, }; - _onButton.OnPressed += args => owner.SendEnableMessage(true); + _onButton.OnPressed += args => OnOverallState?.Invoke(true); var closeButton = new TextureButton { @@ -316,7 +321,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne } }); - _scanButton.OnPressed += args => _owner.SendScanPartsMessage(); + _scanButton.OnPressed += args => OnScanPartsRequested?.Invoke(); _alarmControl.AnimationCompleted += s => { @@ -332,7 +337,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne PASegmentControl Segment(string name) { - return new(this, resourceCache, name); + return new(this, _cache, name); } UpdateUI(false, false, false, false); @@ -368,7 +373,7 @@ private void PowerStateChanged(ValueChangedEventArgs e) } _stateSpinBox.SetButtonDisabled(true); - _owner.SendPowerStateMessage(newState); + OnPowerState?.Invoke(newState); } protected override DragMode GetDragModeFor(Vector2 relativeMousePos) diff --git a/Content.Client/Pinpointer/UI/NavMapBeaconBoundUserInterface.cs b/Content.Client/Pinpointer/UI/NavMapBeaconBoundUserInterface.cs index 3ebcf7cbced..0df6787170a 100644 --- a/Content.Client/Pinpointer/UI/NavMapBeaconBoundUserInterface.cs +++ b/Content.Client/Pinpointer/UI/NavMapBeaconBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Pinpointer; using JetBrains.Annotations; +using Robust.Client.UserInterface; namespace Content.Client.Pinpointer.UI; @@ -16,19 +17,16 @@ public NavMapBeaconBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, protected override void Open() { base.Open(); - _window = new NavMapBeaconWindow(Owner); - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<NavMapBeaconWindow>(); + + if (EntMan.TryGetComponent(Owner, out NavMapBeaconComponent? beacon)) + { + _window.SetEntity(Owner, beacon); + } _window.OnApplyButtonPressed += (label, enabled, color) => { SendMessage(new NavMapBeaconConfigureBuiMessage(label, enabled, color)); }; } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _window?.Dispose(); - } } diff --git a/Content.Client/Pinpointer/UI/NavMapBeaconWindow.xaml.cs b/Content.Client/Pinpointer/UI/NavMapBeaconWindow.xaml.cs index 968fe188f75..b77f1af0472 100644 --- a/Content.Client/Pinpointer/UI/NavMapBeaconWindow.xaml.cs +++ b/Content.Client/Pinpointer/UI/NavMapBeaconWindow.xaml.cs @@ -10,38 +10,37 @@ namespace Content.Client.Pinpointer.UI; [GenerateTypedNameReferences] public sealed partial class NavMapBeaconWindow : FancyWindow { - [Dependency] private readonly IEntityManager _entityManager = default!; - private string? _defaultLabel; private bool _defaultEnabled; private Color _defaultColor; public event Action<string?, bool, Color>? OnApplyButtonPressed; - public NavMapBeaconWindow(EntityUid beaconEntity) + public NavMapBeaconWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - if (!_entityManager.TryGetComponent<NavMapBeaconComponent>(beaconEntity, out var navMap)) - return; - _defaultLabel = navMap.Text; - _defaultEnabled = navMap.Enabled; - _defaultColor = navMap.Color; - UpdateVisibleButton(navMap.Enabled); VisibleButton.OnPressed += args => UpdateVisibleButton(args.Button.Pressed); - - LabelLineEdit.Text = navMap.Text ?? string.Empty; LabelLineEdit.OnTextChanged += OnTextChanged; - - ColorSelector.Color = navMap.Color; ColorSelector.OnColorChanged += _ => TryEnableApplyButton(); TryEnableApplyButton(); ApplyButton.OnPressed += OnApplyPressed; } + public void SetEntity(EntityUid uid, NavMapBeaconComponent navMap) + { + _defaultLabel = navMap.Text; + _defaultEnabled = navMap.Enabled; + _defaultColor = navMap.Color; + + UpdateVisibleButton(navMap.Enabled); + LabelLineEdit.Text = navMap.Text ?? string.Empty; + ColorSelector.Color = navMap.Color; + } + private void UpdateVisibleButton(bool value) { VisibleButton.Pressed = value; diff --git a/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml new file mode 100644 index 00000000000..e1c55131cd6 --- /dev/null +++ b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml @@ -0,0 +1,19 @@ +<Control xmlns="https://spacestation14.io" HorizontalExpand="True"> + <BoxContainer Name="MainContainer" + Orientation="Horizontal" + HorizontalExpand="True"> + <PanelContainer Name="ColorPanel" + VerticalExpand="True" + SetWidth="7" + Margin="0 1 0 0" /> + <Button Name="MainButton" + HorizontalExpand="True" + VerticalExpand="True" + StyleClasses="ButtonSquare" + Margin="-1 0 0 0"> + <BoxContainer Orientation="Horizontal" HorizontalExpand="True"> + <Label Name="BeaconNameLabel" /> + </BoxContainer> + </Button> + </BoxContainer> +</Control> diff --git a/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs new file mode 100644 index 00000000000..a4d4055c7df --- /dev/null +++ b/Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs @@ -0,0 +1,50 @@ +using Content.Shared.Pinpointer; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; + +namespace Content.Client.Pinpointer.UI; + +[GenerateTypedNameReferences] +public sealed partial class StationMapBeaconControl : Control, IComparable<StationMapBeaconControl> +{ + [Dependency] private readonly IEntityManager _entMan = default!; + + public readonly EntityCoordinates BeaconPosition; + public Action<EntityCoordinates>? OnPressed; + public string? Label => BeaconNameLabel.Text; + private StyleBoxFlat _styleBox; + public Color Color => _styleBox.BackgroundColor; + + public StationMapBeaconControl(EntityUid mapUid, SharedNavMapSystem.NavMapBeacon beacon) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + BeaconPosition = new EntityCoordinates(mapUid, beacon.Position); + + _styleBox = new StyleBoxFlat { BackgroundColor = beacon.Color }; + ColorPanel.PanelOverride = _styleBox; + BeaconNameLabel.Text = beacon.Text; + + MainButton.OnPressed += args => OnPressed?.Invoke(BeaconPosition); + } + + public int CompareTo(StationMapBeaconControl? other) + { + if (other == null) + return 1; + + // Group by color + var colorCompare = Color.ToArgb().CompareTo(other.Color.ToArgb()); + if (colorCompare != 0) + { + return colorCompare; + } + + // If same color, sort by text + return string.Compare(Label, other.Label); + } +} diff --git a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs index 1483e75e732..d662dc91c66 100644 --- a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs +++ b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs @@ -1,4 +1,5 @@ -using Robust.Client.GameObjects; +using Content.Shared.Pinpointer; +using Robust.Client.UserInterface; namespace Content.Client.Pinpointer.UI; @@ -14,22 +15,22 @@ public StationMapBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, u protected override void Open() { base.Open(); - _window?.Close(); EntityUid? gridUid = null; if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform)) - { gridUid = xform.GridUid; - } - _window = new StationMapWindow(gridUid, Owner); - _window.OpenCentered(); - _window.OnClose += Close; - } + _window = this.CreateWindow<StationMapWindow>(); + _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _window?.Dispose(); + string stationName = string.Empty; + + if(EntMan.TryGetComponent<MetaDataComponent>(gridUid, out var gridMetaData)) + stationName = gridMetaData.EntityName; + + if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation) + _window.Set(stationName, gridUid, Owner); + else + _window.Set(stationName, gridUid, null); } } diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml b/Content.Client/Pinpointer/UI/StationMapWindow.xaml index 00424a3566a..c79fc8f9e7b 100644 --- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml +++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml @@ -3,11 +3,28 @@ xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI" Title="{Loc 'station-map-window-title'}" Resizable="False" - SetSize="668 713" - MinSize="668 713"> + SetSize="868 748" + MinSize="868 748"> <BoxContainer Orientation="Vertical"> - <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 8 0 10" VerticalAlignment="Top"> + <!-- Station name --> + <controls:StripeBack> + <PanelContainer> + <Label Name="StationName" Text="Unknown station" StyleClasses="LabelBig" Align="Center"/> + </PanelContainer> + </controls:StripeBack> + + <BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalAlignment="Top"> <ui:NavMapControl Name="NavMapScreen"/> + + <BoxContainer Orientation="Vertical" SetWidth="200"> + <!-- Search bar --> + <LineEdit Name="FilterBar" PlaceHolder="{Loc 'station-map-filter-placeholder'}" Margin="0 0 10 10" HorizontalExpand="True"/> + + <ScrollContainer HorizontalExpand="True" VerticalExpand="True"> + <!-- Beacon Buttons (filled by code) --> + <BoxContainer Name="BeaconButtons" Orientation="Vertical" HorizontalExpand="True" /> + </ScrollContainer> + </BoxContainer> </BoxContainer> <!-- Footer --> diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs index 1b01fe4e304..52ef2ab7da4 100644 --- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs +++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs @@ -3,25 +3,75 @@ using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map; +using Content.Shared.Pinpointer; namespace Content.Client.Pinpointer.UI; [GenerateTypedNameReferences] public sealed partial class StationMapWindow : FancyWindow { - public StationMapWindow(EntityUid? mapUid, EntityUid? trackedEntity) + [Dependency] private readonly IEntityManager _entMan = default!; + + private readonly List<StationMapBeaconControl> _buttons = new(); + + public StationMapWindow() { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + FilterBar.OnTextChanged += (bar) => OnFilterChanged(bar.Text); + } + + public void Set(string stationName, EntityUid? mapUid, EntityUid? trackedEntity) + { NavMapScreen.MapUid = mapUid; if (trackedEntity != null) NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Cyan)); - if (IoCManager.Resolve<IEntityManager>().TryGetComponent<MetaDataComponent>(mapUid, out var metadata)) + if (!string.IsNullOrEmpty(stationName)) { - Title = metadata.EntityName; + StationName.Text = stationName; } NavMapScreen.ForceNavMapUpdate(); + UpdateBeaconList(mapUid); + } + + public void OnFilterChanged(string newFilter) + { + foreach (var button in _buttons) + { + button.Visible = string.IsNullOrEmpty(newFilter) || ( + !string.IsNullOrEmpty(button.Label) && + button.Label.Contains(newFilter, StringComparison.OrdinalIgnoreCase) + ); + }; + } + + public void UpdateBeaconList(EntityUid? mapUid) + { + BeaconButtons.Children.Clear(); + _buttons.Clear(); + + if (!mapUid.HasValue) + return; + + if (!_entMan.TryGetComponent<NavMapComponent>(mapUid, out var navMap)) + return; + + foreach (var beacon in navMap.Beacons.Values) + { + var button = new StationMapBeaconControl(mapUid.Value, beacon); + + button.OnPressed += NavMapScreen.CenterToCoordinates; + + _buttons.Add(button); + } + + _buttons.Sort(); + + foreach (var button in _buttons) + BeaconButtons.AddChild(button); } -} +} \ No newline at end of file diff --git a/Content.Client/Pinpointer/UI/UntrackedMapBoundUserInterface.cs b/Content.Client/Pinpointer/UI/UntrackedMapBoundUserInterface.cs deleted file mode 100644 index 57965b030a2..00000000000 --- a/Content.Client/Pinpointer/UI/UntrackedMapBoundUserInterface.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Robust.Client.GameObjects; - -namespace Content.Client.Pinpointer.UI; - -public sealed class UntrackedStationMapBoundUserInterface : BoundUserInterface -{ - [ViewVariables] - private StationMapWindow? _window; - - public UntrackedStationMapBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } - - protected override void Open() - { - base.Open(); - _window?.Close(); - EntityUid? gridUid = null; - - if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform)) - { - gridUid = xform.GridUid; - } - - _window = new StationMapWindow(gridUid, null); - _window.OpenCentered(); - _window.OnClose += Close; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _window?.Dispose(); - } -} diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 286358b85e4..9929267aee8 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -47,6 +47,8 @@ private void ClientOnRunLevelChanged(object? sender, RunLevelChangedEventArgs e) { // Reset on disconnect, just in case. _roles.Clear(); + _jobWhitelists.Clear(); + _roleBans.Clear(); } } @@ -54,9 +56,6 @@ private void RxRoleBans(MsgRoleBans message) { _sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries."); - if (_roleBans.Equals(message.Bans)) - return; - _roleBans.Clear(); _roleBans.AddRange(message.Bans); Updated?.Invoke(); diff --git a/Content.Client/Power/APC/ApcBoundUserInterface.cs b/Content.Client/Power/APC/ApcBoundUserInterface.cs index fbcbf011569..759a5949ba6 100644 --- a/Content.Client/Power/APC/ApcBoundUserInterface.cs +++ b/Content.Client/Power/APC/ApcBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.APC; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Power.APC { @@ -19,9 +20,8 @@ protected override void Open() { base.Open(); - _menu = new ApcMenu(this); - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<ApcMenu>(); + _menu.OnBreaker += BreakerPressed; } protected override void UpdateState(BoundUserInterfaceState state) @@ -36,15 +36,5 @@ public void BreakerPressed() { SendMessage(new ApcToggleMainBreakerMessage()); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _menu?.Dispose(); - } - } } } diff --git a/Content.Client/Power/APC/UI/ApcMenu.xaml.cs b/Content.Client/Power/APC/UI/ApcMenu.xaml.cs index dbf68ea07b0..2f61ea63a86 100644 --- a/Content.Client/Power/APC/UI/ApcMenu.xaml.cs +++ b/Content.Client/Power/APC/UI/ApcMenu.xaml.cs @@ -17,13 +17,19 @@ namespace Content.Client.Power.APC.UI [GenerateTypedNameReferences] public sealed partial class ApcMenu : FancyWindow { - public ApcMenu(ApcBoundUserInterface owner) + public event Action? OnBreaker; + + public ApcMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - EntityView.SetEntity(owner.Owner); - BreakerButton.OnPressed += _ => owner.BreakerPressed(); + BreakerButton.OnPressed += _ => OnBreaker?.Invoke(); + } + + public void SetEntity(EntityUid entity) + { + EntityView.SetEntity(entity); } public void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Power/Components/ApcPowerReceiverComponent.cs b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs new file mode 100644 index 00000000000..fbebcb7cf83 --- /dev/null +++ b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs @@ -0,0 +1,8 @@ +using Content.Shared.Power.Components; + +namespace Content.Client.Power.Components; + +[RegisterComponent] +public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent +{ +} diff --git a/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs new file mode 100644 index 00000000000..ebf6c18c953 --- /dev/null +++ b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Client.Power.Components; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; +using Content.Shared.Examine; +using Robust.Shared.GameStates; + +namespace Content.Client.Power.EntitySystems; + +public sealed class PowerReceiverSystem : SharedPowerReceiverSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<ApcPowerReceiverComponent, ExaminedEvent>(OnExamined); + SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentHandleState>(OnHandleState); + } + + private void OnExamined(Entity<ApcPowerReceiverComponent> ent, ref ExaminedEvent args) + { + args.PushMarkup(GetExamineText(ent.Comp.Powered)); + } + + private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args) + { + if (args.Current is not ApcPowerReceiverComponentState state) + return; + + component.Powered = state.Powered; + } + + public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component) + { + if (component != null) + return true; + + if (!TryComp(entity, out ApcPowerReceiverComponent? receiver)) + return false; + + component = receiver; + return true; + } +} diff --git a/Content.Client/Power/Generator/GeneratorWindow.xaml.cs b/Content.Client/Power/Generator/GeneratorWindow.xaml.cs index bd5b75de1da..161482e0905 100644 --- a/Content.Client/Power/Generator/GeneratorWindow.xaml.cs +++ b/Content.Client/Power/Generator/GeneratorWindow.xaml.cs @@ -3,41 +3,46 @@ using Content.Shared.Power.Generator; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Network; namespace Content.Client.Power.Generator; [GenerateTypedNameReferences] public sealed partial class GeneratorWindow : FancyWindow { - private readonly EntityUid _entity; - [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly ILocalizationManager _loc = default!; - private readonly SharedPowerSwitchableSystem _switchable; - private readonly FuelGeneratorComponent? _component; - private PortableGeneratorComponentBuiState? _lastState; + private EntityUid _entity; + + public float? MaximumPower; + + public event Action<int>? OnPower; + public event Action<bool>? OnState; + public event Action? OnSwitchOutput; + public event Action? OnEjectFuel; - public GeneratorWindow(PortableGeneratorBoundUserInterface bui, EntityUid entity) + public GeneratorWindow() { - _entity = entity; RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - _entityManager.TryGetComponent(entity, out _component); - _switchable = _entityManager.System<SharedPowerSwitchableSystem>(); - - EntityView.SetEntity(entity); TargetPower.IsValid += IsValid; TargetPower.ValueChanged += (args) => { - bui.SetTargetPower(args.Value); + OnPower?.Invoke(args.Value); }; - StartButton.OnPressed += _ => bui.Start(); - StopButton.OnPressed += _ => bui.Stop(); - OutputSwitchButton.OnPressed += _ => bui.SwitchOutput(); - FuelEject.OnPressed += _ => bui.EjectFuel(); + StartButton.OnPressed += _ => OnState?.Invoke(true); + StopButton.OnPressed += _ => OnState?.Invoke(false); + OutputSwitchButton.OnPressed += _ => OnSwitchOutput?.Invoke(); + FuelEject.OnPressed += _ => OnEjectFuel?.Invoke(); + } + + public void SetEntity(EntityUid entity) + { + _entity = entity; + EntityView.SetEntity(entity); } private bool IsValid(int arg) @@ -45,7 +50,7 @@ private bool IsValid(int arg) if (arg < 0) return false; - if (arg > (_lastState?.MaximumPower / 1000.0f ?? 0)) + if (arg > (MaximumPower / 1000.0f ?? 0)) return false; return true; @@ -53,16 +58,17 @@ private bool IsValid(int arg) public void Update(PortableGeneratorComponentBuiState state) { - if (_component == null) + MaximumPower = state.MaximumPower; + + if (!_entityManager.TryGetComponent(_entity, out FuelGeneratorComponent? component)) return; - _lastState = state; if (!TargetPower.LineEditControl.HasKeyboardFocus()) TargetPower.OverrideValue((int)(state.TargetPower / 1000.0f)); - var efficiency = SharedGeneratorSystem.CalcFuelEfficiency(state.TargetPower, state.OptimalPower, _component); + var efficiency = SharedGeneratorSystem.CalcFuelEfficiency(state.TargetPower, state.OptimalPower, component); Efficiency.Text = efficiency.ToString("P1"); - var burnRate = _component.OptimalBurnRate / efficiency; + var burnRate = component.OptimalBurnRate / efficiency; var left = state.RemainingFuel / burnRate; Eta.Text = Loc.GetString( @@ -102,14 +108,15 @@ public void Update(PortableGeneratorComponentBuiState state) } var canSwitch = _entityManager.TryGetComponent(_entity, out PowerSwitchableComponent? switchable); + var switcher = _entityManager.System<SharedPowerSwitchableSystem>(); OutputSwitchLabel.Visible = canSwitch; OutputSwitchButton.Visible = canSwitch; if (switchable != null) { - var voltage = _switchable.VoltageString(_switchable.GetVoltage(_entity, switchable)); + var voltage = switcher.VoltageString(switcher.GetVoltage(_entity, switchable)); OutputSwitchLabel.Text = Loc.GetString("portable-generator-ui-current-output", ("voltage", voltage)); - var nextVoltage = _switchable.VoltageString(_switchable.GetNextVoltage(_entity, switchable)); + var nextVoltage = switcher.VoltageString(switcher.GetNextVoltage(_entity, switchable)); OutputSwitchButton.Text = Loc.GetString("power-switchable-switch-voltage", ("voltage", nextVoltage)); OutputSwitchButton.Disabled = state.On; } diff --git a/Content.Client/Power/Generator/PortableGeneratorBoundUserInterface.cs b/Content.Client/Power/Generator/PortableGeneratorBoundUserInterface.cs index 30679d71fd6..550e1041b62 100644 --- a/Content.Client/Power/Generator/PortableGeneratorBoundUserInterface.cs +++ b/Content.Client/Power/Generator/PortableGeneratorBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Power.Generator; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Power.Generator; @@ -16,10 +17,25 @@ public PortableGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(o protected override void Open() { base.Open(); - _window = new GeneratorWindow(this, Owner); + _window = this.CreateWindow<GeneratorWindow>(); + _window.SetEntity(Owner); + _window.OnState += args => + { + if (args) + { + Start(); + } + else + { + Stop(); + } + }; + + _window.OnPower += SetTargetPower; + _window.OnEjectFuel += EjectFuel; + _window.OnSwitchOutput += SwitchOutput; _window.OpenCenteredLeft(); - _window.OnClose += Close; } protected override void UpdateState(BoundUserInterfaceState state) @@ -30,11 +46,6 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.Update(msg); } - protected override void Dispose(bool disposing) - { - _window?.Dispose(); - } - public void SetTargetPower(int target) { SendMessage(new PortableGeneratorSetTargetPowerMessage(target)); diff --git a/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs b/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs new file mode 100644 index 00000000000..7a36b8ddf59 --- /dev/null +++ b/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs @@ -0,0 +1,38 @@ +using Content.Shared.Power; +using Robust.Client.UserInterface; + +namespace Content.Client.Power.PowerCharge; + +public sealed class PowerChargeBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private PowerChargeWindow? _window; + + public PowerChargeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + public void SetPowerSwitch(bool on) + { + SendMessage(new SwitchChargingMachineMessage(on)); + } + + protected override void Open() + { + base.Open(); + if (!EntMan.TryGetComponent(Owner, out PowerChargeComponent? component)) + return; + + _window = this.CreateWindow<PowerChargeWindow>(); + _window.UpdateWindow(this, Loc.GetString(component.WindowTitle)); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + if (state is not PowerChargeState chargeState) + return; + + _window?.UpdateState(chargeState); + } +} diff --git a/Content.Client/Power/PowerCharge/PowerChargeComponent.cs b/Content.Client/Power/PowerCharge/PowerChargeComponent.cs new file mode 100644 index 00000000000..ab5baa4e2f5 --- /dev/null +++ b/Content.Client/Power/PowerCharge/PowerChargeComponent.cs @@ -0,0 +1,10 @@ +using Content.Shared.Power; + +namespace Content.Client.Power.PowerCharge; + +/// <inheritdoc cref="Content.Shared.Power.SharedPowerChargeComponent" /> +[RegisterComponent] +public sealed partial class PowerChargeComponent : SharedPowerChargeComponent +{ + +} diff --git a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml similarity index 60% rename from Content.Client/Gravity/UI/GravityGeneratorWindow.xaml rename to Content.Client/Power/PowerCharge/PowerChargeWindow.xaml index 853f437a2bf..4e61255326e 100644 --- a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml +++ b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml @@ -1,27 +1,26 @@ <controls:FancyWindow xmlns="https://spacestation14.io" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" - Title="{Loc 'gravity-generator-window-title'}" MinSize="270 130" SetSize="360 180"> <BoxContainer Margin="4 0" Orientation="Horizontal"> <BoxContainer Orientation="Vertical" HorizontalExpand="True"> <GridContainer Margin="2 0 0 0" Columns="2"> <!-- Power --> - <Label Text="{Loc 'gravity-generator-window-power'}" HorizontalExpand="True" StyleClasses="StatusFieldTitle" /> + <Label Text="{Loc 'power-charge-window-power'}" HorizontalExpand="True" StyleClasses="StatusFieldTitle" /> <BoxContainer Orientation="Horizontal" MinWidth="120"> - <Button Name="OnButton" Text="{Loc 'gravity-generator-window-power-on'}" StyleClasses="OpenRight" /> - <Button Name="OffButton" Text="{Loc 'gravity-generator-window-power-off'}" StyleClasses="OpenLeft" /> + <Button Name="OnButton" Text="{Loc 'power-charge-window-power-on'}" StyleClasses="OpenRight" /> + <Button Name="OffButton" Text="{Loc 'power-charge-window-power-off'}" StyleClasses="OpenLeft" /> </BoxContainer> <Control /> <!-- Empty control to act as a spacer in the grid. --> <Label Name="PowerLabel" Text="0 / 0 W" /> <!-- Status --> - <Label Text="{Loc 'gravity-generator-window-status'}" StyleClasses="StatusFieldTitle" /> - <Label Name="StatusLabel" Text="{Loc 'gravity-generator-window-status-fully-charged'}" /> + <Label Text="{Loc 'power-charge-window-status'}" StyleClasses="StatusFieldTitle" /> + <Label Name="StatusLabel" Text="{Loc 'power-charge-window-status-fully-charged'}" /> <!-- ETA --> - <Label Text="{Loc 'gravity-generator-window-eta'}" StyleClasses="StatusFieldTitle" /> + <Label Text="{Loc 'power-charge-window-eta'}" StyleClasses="StatusFieldTitle" /> <Label Name="EtaLabel" Text="N/A" /> <!-- Charge --> - <Label Text="{Loc 'gravity-generator-window-charge'}" StyleClasses="StatusFieldTitle" /> + <Label Text="{Loc 'power-charge-window-charge'}" StyleClasses="StatusFieldTitle" /> <ProgressBar Name="ChargeBar" MaxValue="255"> <Label Name="ChargeText" Margin="4 0" Text="0 %" /> </ProgressBar> @@ -31,5 +30,4 @@ <SpriteView Name="EntityView" SetSize="96 96" OverrideDirection="South" /> </PanelContainer> </BoxContainer> - </controls:FancyWindow> diff --git a/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs new file mode 100644 index 00000000000..6739e24c208 --- /dev/null +++ b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs @@ -0,0 +1,72 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.Power; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Power.PowerCharge; + +[GenerateTypedNameReferences] +public sealed partial class PowerChargeWindow : FancyWindow +{ + private readonly ButtonGroup _buttonGroup = new(); + + public PowerChargeWindow() + { + RobustXamlLoader.Load(this); + + OnButton.Group = _buttonGroup; + OffButton.Group = _buttonGroup; + } + + public void UpdateWindow(PowerChargeBoundUserInterface bui, string title) + { + Title = title; + + OnButton.OnPressed += _ => bui.SetPowerSwitch(true); + OffButton.OnPressed += _ => bui.SetPowerSwitch(false); + + EntityView.SetEntity(bui.Owner); + } + + public void UpdateState(PowerChargeState state) + { + if (state.On) + OnButton.Pressed = true; + else + OffButton.Pressed = true; + + PowerLabel.Text = Loc.GetString( + "power-charge-window-power-label", + ("draw", state.PowerDraw), + ("max", state.PowerDrawMax)); + + PowerLabel.SetOnlyStyleClass(MathHelper.CloseTo(state.PowerDraw, state.PowerDrawMax) ? "Good" : "Caution"); + + ChargeBar.Value = state.Charge; + ChargeText.Text = (state.Charge / 255f).ToString("P0"); + StatusLabel.Text = Loc.GetString(state.PowerStatus switch + { + PowerChargePowerStatus.Off => "power-charge-window-status-off", + PowerChargePowerStatus.Discharging => "power-charge-window-status-discharging", + PowerChargePowerStatus.Charging => "power-charge-window-status-charging", + PowerChargePowerStatus.FullyCharged => "power-charge-window-status-fully-charged", + _ => throw new ArgumentOutOfRangeException() + }); + + StatusLabel.SetOnlyStyleClass(state.PowerStatus switch + { + PowerChargePowerStatus.Off => "Danger", + PowerChargePowerStatus.Discharging => "Caution", + PowerChargePowerStatus.Charging => "Caution", + PowerChargePowerStatus.FullyCharged => "Good", + _ => throw new ArgumentOutOfRangeException() + }); + + EtaLabel.Text = state.EtaSeconds >= 0 + ? Loc.GetString("power-charge-window-eta-value", ("left", TimeSpan.FromSeconds(state.EtaSeconds))) + : Loc.GetString("power-charge-window-eta-none"); + + EtaLabel.SetOnlyStyleClass(state.EtaSeconds >= 0 ? "Caution" : "Disabled"); + } +} diff --git a/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs b/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs index dc1dcd03ef1..cbc343c06c6 100644 --- a/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs +++ b/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs @@ -1,4 +1,5 @@ using Content.Shared.Power; +using Robust.Client.UserInterface; namespace Content.Client.Power; @@ -11,9 +12,9 @@ public PowerMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : b protected override void Open() { - _menu = new PowerMonitoringWindow(this, Owner); - _menu.OpenCentered(); - _menu.OnClose += Close; + _menu = this.CreateWindow<PowerMonitoringWindow>(); + _menu.SetEntity(Owner); + _menu.SendPowerMonitoringConsoleMessageAction += SendPowerMonitoringConsoleMessage; } protected override void UpdateState(BoundUserInterfaceState state) @@ -22,9 +23,6 @@ protected override void UpdateState(BoundUserInterfaceState state) var castState = (PowerMonitoringConsoleBoundInterfaceState) state; - if (castState == null) - return; - EntMan.TryGetComponent<TransformComponent>(Owner, out var xform); _menu?.ShowEntites (castState.TotalSources, @@ -40,13 +38,4 @@ public void SendPowerMonitoringConsoleMessage(NetEntity? netEntity, PowerMonitor { SendMessage(new PowerMonitoringConsoleMessage(netEntity, group)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Dispose(); - } } diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs index 25a586a75de..0fb49df5344 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs @@ -32,7 +32,7 @@ private void UpdateWindowConsoleEntry if (windowEntry == null) return; - // Update sources and loads + // Update sources and loads UpdateEntrySourcesOrLoads(masterContainer, windowEntry.SourcesContainer, focusSources, _sourceIcon); UpdateEntrySourcesOrLoads(masterContainer, windowEntry.LoadsContainer, focusLoads, _loadIconPath); @@ -133,7 +133,7 @@ private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContaine subEntry.Button.OnButtonUp += args => { ButtonAction(subEntry, masterContainer); }; } - if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner, out var console)) + if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(Entity, out var console)) return; // Update all children @@ -378,7 +378,7 @@ public PowerMonitoringWindowEntry(PowerMonitoringConsoleEntry entry) : base(entr AddChild(MainContainer); - // Grid container to hold the list of sources when selected + // Grid container to hold the list of sources when selected SourcesContainer = new BoxContainer() { Orientation = LayoutOrientation.Vertical, diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs index 81fe1f4d047..e3043252486 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs @@ -15,13 +15,12 @@ namespace Content.Client.Power; [GenerateTypedNameReferences] public sealed partial class PowerMonitoringWindow : FancyWindow { - private readonly IEntityManager _entManager; + [Dependency] private IEntityManager _entManager = default!; private readonly SpriteSystem _spriteSystem; - private readonly IGameTiming _gameTiming; + [Dependency] private IGameTiming _gameTiming = default!; private const float BlinkFrequency = 1f; - private EntityUid? _owner; private NetEntity? _focusEntity; public event Action<NetEntity?, PowerMonitoringConsoleGroup>? SendPowerMonitoringConsoleMessageAction; @@ -34,31 +33,56 @@ public sealed partial class PowerMonitoringWindow : FancyWindow { PowerMonitoringConsoleGroup.APC, (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), Color.LimeGreen) }, }; - public PowerMonitoringWindow(PowerMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner) + public EntityUid Entity; + + public PowerMonitoringWindow() { RobustXamlLoader.Load(this); - _entManager = IoCManager.Resolve<IEntityManager>(); - _gameTiming = IoCManager.Resolve<IGameTiming>(); + IoCManager.InjectDependencies(this); _spriteSystem = _entManager.System<SpriteSystem>(); - _owner = owner; + + // Set trackable entity selected action + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + + // Update nav map + NavMap.ForceNavMapUpdate(); + + // Set UI tab titles + MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-label-sources")); + MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-label-smes")); + MasterTabContainer.SetTabTitle(2, Loc.GetString("power-monitoring-window-label-substation")); + MasterTabContainer.SetTabTitle(3, Loc.GetString("power-monitoring-window-label-apc")); + + // Track when the MasterTabContainer changes its tab + MasterTabContainer.OnTabChanged += OnTabChanged; + + // Set UI toggles + ShowHVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.HighVoltage); + ShowMVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.MediumVoltage); + ShowLVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.Apc); + } + + public void SetEntity(EntityUid uid) + { + Entity = uid; // Pass owner to nav map - NavMap.Owner = _owner; + NavMap.Owner = uid; // Set nav map grid uid var stationName = Loc.GetString("power-monitoring-window-unknown-location"); - if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform)) + if (_entManager.TryGetComponent<TransformComponent>(uid, out var xform)) { NavMap.MapUid = xform.GridUid; - // Assign station name + // Assign station name if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData)) stationName = stationMetaData.EntityName; var msg = new FormattedMessage(); - msg.AddMarkup(Loc.GetString("power-monitoring-window-station-name", ("stationName", stationName))); + msg.AddMarkupOrThrow(Loc.GetString("power-monitoring-window-station-name", ("stationName", stationName))); StationName.SetMessage(msg); } @@ -68,29 +92,6 @@ public PowerMonitoringWindow(PowerMonitoringConsoleBoundUserInterface userInterf StationName.SetMessage(stationName); NavMap.Visible = false; } - - // Set trackable entity selected action - NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; - - // Update nav map - NavMap.ForceNavMapUpdate(); - - // Set UI tab titles - MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-label-sources")); - MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-label-smes")); - MasterTabContainer.SetTabTitle(2, Loc.GetString("power-monitoring-window-label-substation")); - MasterTabContainer.SetTabTitle(3, Loc.GetString("power-monitoring-window-label-apc")); - - // Track when the MasterTabContainer changes its tab - MasterTabContainer.OnTabChanged += OnTabChanged; - - // Set UI toggles - ShowHVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.HighVoltage); - ShowMVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.MediumVoltage); - ShowLVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.Apc); - - // Set power monitoring message action - SendPowerMonitoringConsoleMessageAction += userInterface.SendPowerMonitoringConsoleMessage; } private void OnTabChanged(int tab) @@ -113,10 +114,7 @@ public void ShowEntites PowerMonitoringConsoleEntry[] focusLoads, EntityCoordinates? monitorCoords) { - if (_owner == null) - return; - - if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner.Value, out var console)) + if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(Entity, out var console)) return; // Update power status text @@ -161,13 +159,13 @@ public void ShowEntites } // Show monitor location - var mon = _entManager.GetNetEntity(_owner); + var mon = _entManager.GetNetEntity(Entity); - if (monitorCoords != null && mon != null) + if (monitorCoords != null && mon.IsValid()) { var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); var blip = new NavMapBlip(monitorCoords.Value, texture, Color.Cyan, true, false); - NavMap.TrackedEntities[mon.Value] = blip; + NavMap.TrackedEntities[mon] = blip; } // If the entry group doesn't match the current tab, the data is out dated, do not use it @@ -239,7 +237,7 @@ private void SetTrackedEntityFromNavMap(NetEntity? netEntity) if (netEntity == null) return; - if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner, out var console)) + if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(Entity, out var console)) return; if (!console.PowerMonitoringDeviceMetaData.TryGetValue(netEntity.Value, out var metaData)) @@ -266,7 +264,7 @@ protected override void FrameUpdate(FrameEventArgs args) { AutoScrollToFocus(); - // Warning sign pulse + // Warning sign pulse var lit = _gameTiming.RealTime.TotalSeconds % BlinkFrequency > BlinkFrequency / 2f; SystemWarningPanel.Modulate = lit ? Color.White : new Color(178, 178, 178); } diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs index 3eb0397a690..f0d27d6b1fb 100644 --- a/Content.Client/RCD/RCDMenu.xaml.cs +++ b/Content.Client/RCD/RCDMenu.xaml.cs @@ -20,31 +20,37 @@ public sealed partial class RCDMenu : RadialMenu [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - private readonly SpriteSystem _spriteSystem; - private readonly SharedPopupSystem _popup; + private SharedPopupSystem _popup; + private SpriteSystem _sprites; public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction; private EntityUid _owner; - public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) + public RCDMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - _spriteSystem = _entManager.System<SpriteSystem>(); _popup = _entManager.System<SharedPopupSystem>(); + _sprites = _entManager.System<SpriteSystem>(); - _owner = owner; + OnChildAdded += AddRCDMenuButtonOnClickActions; + } + + public void SetEntity(EntityUid uid) + { + _owner = uid; + Refresh(); + } + public void Refresh() + { // Find the main radial container var main = FindControl<RadialContainer>("Main"); - if (main == null) - return; - // Populate secondary radial containers - if (!_entManager.TryGetComponent<RCDComponent>(owner, out var rcd)) + if (!_entManager.TryGetComponent<RCDComponent>(_owner, out var rcd)) return; foreach (var protoId in rcd.AvailablePrototypes) @@ -56,14 +62,10 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) continue; var parent = FindControl<RadialContainer>(proto.Category); - - if (parent == null) - continue; - var tooltip = Loc.GetString(proto.SetName); if ((proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject) && - proto.Prototype != null && _protoManager.TryIndex(proto.Prototype, out var entProto)) + proto.Prototype != null && _protoManager.TryIndex(proto.Prototype, out var entProto, logError: false)) { tooltip = Loc.GetString(entProto.Name); } @@ -84,7 +86,7 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) { VerticalAlignment = VAlignment.Center, HorizontalAlignment = HAlignment.Center, - Texture = _spriteSystem.Frame0(proto.Sprite), + Texture = _sprites.Frame0(proto.Sprite), TextureScale = new Vector2(2f, 2f), }; @@ -112,11 +114,9 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) // Set up menu actions foreach (var child in Children) + { AddRCDMenuButtonOnClickActions(child); - - OnChildAdded += AddRCDMenuButtonOnClickActions; - - SendRCDSystemMessageAction += bui.SendRCDSystemMessage; + } } private static string OopsConcat(string a, string b) @@ -153,7 +153,7 @@ private void AddRCDMenuButtonOnClickActions(Control control) var name = Loc.GetString(proto.SetName); if (proto.Prototype != null && - _protoManager.TryIndex(proto.Prototype, out var entProto)) + _protoManager.TryIndex(proto.Prototype, out var entProto, logError: false)) name = entProto.Name; msg = Loc.GetString("rcd-component-change-build-mode", ("name", name)); diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index a37dbcecf8c..1dd03626ae6 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.Input; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.RCD; @@ -24,8 +25,9 @@ protected override void Open() { base.Open(); - _menu = new(Owner, this); - _menu.OnClose += Close; + _menu = this.CreateWindow<RCDMenu>(); + _menu.SetEntity(Owner); + _menu.SendRCDSystemMessageAction += SendRCDSystemMessage; // Open the menu, centered on the mouse var vpSize = _displayManager.ScreenSize; @@ -34,16 +36,8 @@ protected override void Open() public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId) { - // A predicted message cannot be used here as the RCD UI is closed immediately + // A predicted message cannot be used here as the RCD UI is closed immediately // after this message is sent, which will stop the server from receiving it SendMessage(new RCDSystemMessage(protoId)); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - - _menu?.Dispose(); - } } diff --git a/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs b/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs index 9012767ef3f..8d5607af2d0 100644 --- a/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs +++ b/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs @@ -1,9 +1,11 @@ using System.Numerics; using Content.Shared.Radiation.Components; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Enums; using Robust.Shared.Graphics; using Robust.Shared.Map; +using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -14,6 +16,7 @@ public sealed class RadiationPulseOverlay : Overlay [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + private TransformSystem? _transform; private const float MaxDist = 15.0f; @@ -72,6 +75,8 @@ protected override void Draw(in OverlayDrawArgs args) //Queries all pulses on the map and either adds or removes them from the list of rendered pulses based on whether they should be drawn (in range? on the same z-level/map? pulse entity still exists?) private void RadiationQuery(IEye? currentEye) { + _transform ??= _entityManager.System<TransformSystem>(); + if (currentEye == null) { _pulses.Clear(); @@ -91,7 +96,7 @@ private void RadiationQuery(IEye? currentEye) ( _baseShader.Duplicate(), new RadiationShaderInstance( - _entityManager.GetComponent<TransformComponent>(pulseEntity).MapPosition, + _transform.GetMapCoordinates(pulseEntity), pulse.VisualRange, pulse.StartTime, pulse.VisualDuration @@ -109,7 +114,7 @@ private void RadiationQuery(IEye? currentEye) _entityManager.TryGetComponent(pulseEntity, out RadiationPulseComponent? pulse)) { var shaderInstance = _pulses[pulseEntity]; - shaderInstance.instance.CurrentMapCoords = _entityManager.GetComponent<TransformComponent>(pulseEntity).MapPosition; + shaderInstance.instance.CurrentMapCoords = _transform.GetMapCoordinates(pulseEntity); shaderInstance.instance.Range = pulse.VisualRange; } else { _pulses[pulseEntity].shd.Dispose(); diff --git a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs new file mode 100644 index 00000000000..29d6c635ebc --- /dev/null +++ b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs @@ -0,0 +1,23 @@ +using Content.Client.Radio.Ui; +using Content.Shared.Radio; +using Content.Shared.Radio.Components; +using Robust.Client.GameObjects; + +namespace Content.Client.Radio.EntitySystems; + +public sealed class RadioDeviceSystem : EntitySystem +{ + [Dependency] private readonly UserInterfaceSystem _ui = default!; + + /// <inheritdoc/> + public override void Initialize() + { + SubscribeLocalEvent<IntercomComponent, AfterAutoHandleStateEvent>(OnAfterHandleState); + } + + private void OnAfterHandleState(Entity<IntercomComponent> ent, ref AfterAutoHandleStateEvent args) + { + if (_ui.TryGetOpenUi<IntercomBoundUserInterface>(ent.Owner, IntercomUiKey.Key, out var bui)) + bui.Update(ent); + } +} diff --git a/Content.Client/Radio/Ui/IntercomBoundUserInterface.cs b/Content.Client/Radio/Ui/IntercomBoundUserInterface.cs index abbb1d58ec4..cb95d574399 100644 --- a/Content.Client/Radio/Ui/IntercomBoundUserInterface.cs +++ b/Content.Client/Radio/Ui/IntercomBoundUserInterface.cs @@ -1,6 +1,9 @@ using Content.Shared.Radio; +using Content.Shared.Radio.Components; +using Content.Shared.Radio.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Radio.Ui; @@ -19,7 +22,12 @@ protected override void Open() { base.Open(); - _menu = new(); + _menu = this.CreateWindow<IntercomMenu>(); + + if (EntMan.TryGetComponent(Owner, out IntercomComponent? intercom)) + { + _menu.Update((Owner, intercom)); + } _menu.OnMicPressed += enabled => { @@ -33,26 +41,10 @@ protected override void Open() { SendMessage(new SelectIntercomChannelMessage(channel)); }; - - _menu.OnClose += Close; - _menu.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Close(); } - protected override void UpdateState(BoundUserInterfaceState state) + public void Update(Entity<IntercomComponent> ent) { - base.UpdateState(state); - - if (state is not IntercomBoundUIState msg) - return; - - _menu?.Update(msg); + _menu?.Update(ent); } } diff --git a/Content.Client/Radio/Ui/IntercomMenu.xaml.cs b/Content.Client/Radio/Ui/IntercomMenu.xaml.cs index 8b4b38753c1..de659a07039 100644 --- a/Content.Client/Radio/Ui/IntercomMenu.xaml.cs +++ b/Content.Client/Radio/Ui/IntercomMenu.xaml.cs @@ -1,8 +1,10 @@ +using System.Threading.Channels; using Content.Client.UserInterface.Controls; -using Content.Shared.Radio; +using Content.Shared.Radio.Components; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client.Radio.Ui; @@ -26,30 +28,43 @@ public IntercomMenu() SpeakerButton.OnPressed += args => OnSpeakerPressed?.Invoke(args.Button.Pressed); } - public void Update(IntercomBoundUIState state) + public void Update(Entity<IntercomComponent> entity) { - MicButton.Pressed = state.MicEnabled; - SpeakerButton.Pressed = state.SpeakerEnabled; + MicButton.Pressed = entity.Comp.MicrophoneEnabled; + SpeakerButton.Pressed = entity.Comp.SpeakerEnabled; + + MicButton.Disabled = entity.Comp.SupportedChannels.Count == 0; + SpeakerButton.Disabled = entity.Comp.SupportedChannels.Count == 0; + ChannelOptions.Disabled = entity.Comp.SupportedChannels.Count == 0; ChannelOptions.Clear(); _channels.Clear(); - for (var i = 0; i < state.AvailableChannels.Count; i++) + for (var i = 0; i < entity.Comp.SupportedChannels.Count; i++) { - var channel = state.AvailableChannels[i]; - if (!_prototype.TryIndex<RadioChannelPrototype>(channel, out var prototype)) + var channel = entity.Comp.SupportedChannels[i]; + if (!_prototype.TryIndex(channel, out var prototype)) continue; _channels.Add(channel); ChannelOptions.AddItem(Loc.GetString(prototype.Name), i); - if (channel == state.SelectedChannel) + if (channel == entity.Comp.CurrentChannel) ChannelOptions.Select(i); } + + if (entity.Comp.SupportedChannels.Count == 0) + { + ChannelOptions.AddItem(Loc.GetString("intercom-options-none"), 0); + ChannelOptions.Select(0); + } + ChannelOptions.OnItemSelected += args => { + if (!_channels.TryGetValue(args.Id, out var proto)) + return; + ChannelOptions.SelectId(args.Id); - OnChannelSelected?.Invoke(_channels[args.Id]); + OnChannelSelected?.Invoke(proto); }; } } - diff --git a/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs b/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs index da008ca04c9..9641adb5b2d 100644 --- a/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs +++ b/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Research; using Content.Shared.Research.Components; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Research.UI { @@ -17,10 +18,7 @@ protected override void Open() { base.Open(); - _menu = new(); - - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<DiskConsoleMenu>(); _menu.OnServerButtonPressed += () => { @@ -32,14 +30,6 @@ protected override void Open() }; } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Close(); - } - protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); diff --git a/Content.Client/Research/UI/ResearchClientBoundUserInterface.cs b/Content.Client/Research/UI/ResearchClientBoundUserInterface.cs index 07dd35a0056..288445e4dea 100644 --- a/Content.Client/Research/UI/ResearchClientBoundUserInterface.cs +++ b/Content.Client/Research/UI/ResearchClientBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Research.Components; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Research.UI { @@ -16,10 +17,9 @@ public ResearchClientBoundUserInterface(EntityUid owner, Enum uiKey) : base(owne protected override void Open() { base.Open(); - - _menu = new ResearchClientServerSelectionMenu(this); - _menu.OnClose += Close; - _menu.OpenCentered(); + _menu = this.CreateWindow<ResearchClientServerSelectionMenu>(); + _menu.OnServerSelected += SelectServer; + _menu.OnServerDeselected += DeselectServer; } public void SelectServer(int serverId) @@ -38,12 +38,5 @@ protected override void UpdateState(BoundUserInterfaceState state) if (state is not ResearchClientBoundInterfaceState rState) return; _menu?.Populate(rState.ServerCount, rState.ServerNames, rState.ServerIds, rState.SelectedServerId); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) return; - _menu?.Dispose(); - } } } diff --git a/Content.Client/Research/UI/ResearchClientServerSelectionMenu.xaml.cs b/Content.Client/Research/UI/ResearchClientServerSelectionMenu.xaml.cs index ceaa965e59f..d10f8b39f48 100644 --- a/Content.Client/Research/UI/ResearchClientServerSelectionMenu.xaml.cs +++ b/Content.Client/Research/UI/ResearchClientServerSelectionMenu.xaml.cs @@ -13,27 +13,26 @@ public sealed partial class ResearchClientServerSelectionMenu : DefaultWindow private int[] _serverIds = Array.Empty<int>(); private int _selectedServerId = -1; - private ResearchClientBoundUserInterface Owner { get; } + public event Action<int>? OnServerSelected; + public event Action? OnServerDeselected; - public ResearchClientServerSelectionMenu(ResearchClientBoundUserInterface owner) + public ResearchClientServerSelectionMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - Owner = owner; - Servers.OnItemSelected += OnItemSelected; Servers.OnItemDeselected += OnItemDeselected; } public void OnItemSelected(ItemList.ItemListSelectedEventArgs itemListSelectedEventArgs) { - Owner.SelectServer(_serverIds[itemListSelectedEventArgs.ItemIndex]); + OnServerSelected?.Invoke(_serverIds[itemListSelectedEventArgs.ItemIndex]); } public void OnItemDeselected(ItemList.ItemListDeselectedEventArgs itemListDeselectedEventArgs) { - Owner.DeselectServer(); + OnServerDeselected?.Invoke(); } public void Populate(int serverCount, string[] serverNames, int[] serverIds, int selectedServerId) diff --git a/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs b/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs index f29e66baaeb..2895ada61fb 100644 --- a/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs +++ b/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs @@ -1,6 +1,8 @@ using Content.Shared.Research.Components; +using Content.Shared.Research.Prototypes; using JetBrains.Annotations; -using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; namespace Content.Client.Research.UI; @@ -20,7 +22,8 @@ protected override void Open() var owner = Owner; - _consoleMenu = new ResearchConsoleMenu(owner); + _consoleMenu = this.CreateWindow<ResearchConsoleMenu>(); + _consoleMenu.SetEntity(owner); _consoleMenu.OnTechnologyCardPressed += id => { @@ -31,10 +34,20 @@ protected override void Open() { SendMessage(new ConsoleServerSelectionMessage()); }; + } + + public override void OnProtoReload(PrototypesReloadedEventArgs args) + { + base.OnProtoReload(args); + + if (!args.WasModified<TechnologyPrototype>()) + return; - _consoleMenu.OnClose += Close; + if (State is not ResearchConsoleBoundInterfaceState rState) + return; - _consoleMenu.OpenCentered(); + _consoleMenu?.UpdatePanels(rState); + _consoleMenu?.UpdateInformationPanel(rState); } protected override void UpdateState(BoundUserInterfaceState state) @@ -46,12 +59,4 @@ protected override void UpdateState(BoundUserInterfaceState state) _consoleMenu?.UpdatePanels(castState); _consoleMenu?.UpdateInformationPanel(castState); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _consoleMenu?.Dispose(); - } } diff --git a/Content.Client/Research/UI/ResearchConsoleMenu.xaml.cs b/Content.Client/Research/UI/ResearchConsoleMenu.xaml.cs index b364b833124..d1021f82e56 100644 --- a/Content.Client/Research/UI/ResearchConsoleMenu.xaml.cs +++ b/Content.Client/Research/UI/ResearchConsoleMenu.xaml.cs @@ -25,14 +25,13 @@ public sealed partial class ResearchConsoleMenu : FancyWindow [Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPlayerManager _player = default!; - private readonly TechnologyDatabaseComponent? _technologyDatabase; private readonly ResearchSystem _research; private readonly SpriteSystem _sprite; private readonly AccessReaderSystem _accessReader; - public readonly EntityUid Entity; + public EntityUid Entity; - public ResearchConsoleMenu(EntityUid entity) + public ResearchConsoleMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); @@ -40,21 +39,23 @@ public ResearchConsoleMenu(EntityUid entity) _research = _entity.System<ResearchSystem>(); _sprite = _entity.System<SpriteSystem>(); _accessReader = _entity.System<AccessReaderSystem>(); - Entity = entity; ServerButton.OnPressed += _ => OnServerButtonPressed?.Invoke(); + } - _entity.TryGetComponent(entity, out _technologyDatabase); + public void SetEntity(EntityUid entity) + { + Entity = entity; } - public void UpdatePanels(ResearchConsoleBoundInterfaceState state) + public void UpdatePanels(ResearchConsoleBoundInterfaceState state) { TechnologyCardsContainer.Children.Clear(); var availableTech = _research.GetAvailableTechnologies(Entity); SyncTechnologyList(AvailableCardsContainer, availableTech); - if (_technologyDatabase == null) + if (!_entity.TryGetComponent(Entity, out TechnologyDatabaseComponent? database)) return; // i can't figure out the spacing so here you go @@ -66,7 +67,7 @@ public void UpdatePanels(ResearchConsoleBoundInterfaceState state) var hasAccess = _player.LocalEntity is not { } local || !_entity.TryGetComponent<AccessReaderComponent>(Entity, out var access) || _accessReader.IsAllowed(local, Entity, access); - foreach (var techId in _technologyDatabase.CurrentTechnologyCards) + foreach (var techId in database.CurrentTechnologyCards) { var tech = _prototype.Index<TechnologyPrototype>(techId); var cardControl = new TechnologyCardControl(tech, _prototype, _sprite, _research.GetTechnologyDescription(tech, includeTier: false), state.Points, hasAccess); @@ -74,7 +75,7 @@ public void UpdatePanels(ResearchConsoleBoundInterfaceState state) TechnologyCardsContainer.AddChild(cardControl); } - var unlockedTech = _technologyDatabase.UnlockedTechnologies.Select(x => _prototype.Index<TechnologyPrototype>(x)); + var unlockedTech = database.UnlockedTechnologies.Select(x => _prototype.Index<TechnologyPrototype>(x)); SyncTechnologyList(UnlockedCardsContainer, unlockedTech); } @@ -85,14 +86,14 @@ public void UpdateInformationPanel(ResearchConsoleBoundInterfaceState state) ("points", state.Points))); ResearchAmountLabel.SetMessage(amountMsg); - if (_technologyDatabase == null) + if (!_entity.TryGetComponent(Entity, out TechnologyDatabaseComponent? database)) return; var disciplineText = Loc.GetString("research-discipline-none"); var disciplineColor = Color.Gray; - if (_technologyDatabase.MainDiscipline != null) + if (database.MainDiscipline != null) { - var discipline = _prototype.Index<TechDisciplinePrototype>(_technologyDatabase.MainDiscipline); + var discipline = _prototype.Index<TechDisciplinePrototype>(database.MainDiscipline); disciplineText = Loc.GetString(discipline.Name); disciplineColor = discipline.Color; } @@ -103,10 +104,10 @@ public void UpdateInformationPanel(ResearchConsoleBoundInterfaceState state) MainDisciplineLabel.SetMessage(msg); TierDisplayContainer.Children.Clear(); - foreach (var disciplineId in _technologyDatabase.SupportedDisciplines) + foreach (var disciplineId in database.SupportedDisciplines) { var discipline = _prototype.Index<TechDisciplinePrototype>(disciplineId); - var tier = _research.GetHighestDisciplineTier(_technologyDatabase, discipline); + var tier = _research.GetHighestDisciplineTier(database, discipline); // don't show tiers with no available tech if (tier == 0) diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs index 682c73f93e7..8e7e687fa8f 100644 --- a/Content.Client/Revolutionary/RevolutionarySystem.cs +++ b/Content.Client/Revolutionary/RevolutionarySystem.cs @@ -1,44 +1,37 @@ -using Content.Shared.Antag; using Content.Shared.Revolutionary.Components; -using Content.Shared.Ghost; +using Content.Shared.Revolutionary; using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; namespace Content.Client.Revolutionary; /// <summary> /// Used for the client to get status icons from other revs. /// </summary> -public sealed class RevolutionarySystem : EntitySystem +public sealed class RevolutionarySystem : SharedRevolutionarySystem { + [Dependency] private readonly IPrototypeManager _prototype = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<RevolutionaryComponent, CanDisplayStatusIconsEvent>(OnCanShowRevIcon); - SubscribeLocalEvent<HeadRevolutionaryComponent, CanDisplayStatusIconsEvent>(OnCanShowRevIcon); + SubscribeLocalEvent<RevolutionaryComponent, GetStatusIconsEvent>(GetRevIcon); + SubscribeLocalEvent<HeadRevolutionaryComponent, GetStatusIconsEvent>(GetHeadRevIcon); } - /// <summary> - /// Determine whether a client should display the rev icon. - /// </summary> - private void OnCanShowRevIcon<T>(EntityUid uid, T comp, ref CanDisplayStatusIconsEvent args) where T : IAntagStatusIconComponent + private void GetRevIcon(Entity<RevolutionaryComponent> ent, ref GetStatusIconsEvent args) { - args.Cancelled = !CanDisplayIcon(args.User, comp.IconVisibleToGhost); + if (HasComp<HeadRevolutionaryComponent>(ent)) + return; + + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); } - /// <summary> - /// The criteria that determine whether a client should see Rev/Head rev icons. - /// </summary> - private bool CanDisplayIcon(EntityUid? uid, bool visibleToGhost) + private void GetHeadRevIcon(Entity<HeadRevolutionaryComponent> ent, ref GetStatusIconsEvent args) { - if (HasComp<HeadRevolutionaryComponent>(uid) || HasComp<RevolutionaryComponent>(uid)) - return true; - - if (visibleToGhost && HasComp<GhostComponent>(uid)) - return true; - - return HasComp<ShowRevIconsComponent>(uid); + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); } - } diff --git a/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs index 6185979eee6..9a5159880f9 100644 --- a/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs +++ b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Robotics; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Robotics.UI; @@ -16,7 +17,9 @@ protected override void Open() { base.Open(); - _window = new RoboticsConsoleWindow(Owner); + _window = this.CreateWindow<RoboticsConsoleWindow>(); + _window.SetEntity(Owner); + _window.OnDisablePressed += address => { SendMessage(new RoboticsConsoleDisableMessage(address)); @@ -25,9 +28,6 @@ protected override void Open() { SendMessage(new RoboticsConsoleDestroyMessage(address)); }; - _window.OnClose += Close; - - _window.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -37,14 +37,6 @@ protected override void UpdateState(BoundUserInterfaceState state) if (state is not RoboticsConsoleState cast) return; - _window?.UpdateState(cast); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - _window?.Dispose(); + _window.UpdateState(cast); } } diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs index 367114f2aa6..87d7e62c392 100644 --- a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs +++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs @@ -23,11 +23,12 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow public Action<string>? OnDisablePressed; public Action<string>? OnDestroyPressed; - private Entity<RoboticsConsoleComponent, LockComponent?> _console; private string? _selected; private Dictionary<string, CyborgControlData> _cyborgs = new(); - public RoboticsConsoleWindow(EntityUid console) + public EntityUid Entity; + + public RoboticsConsoleWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); @@ -35,9 +36,6 @@ public RoboticsConsoleWindow(EntityUid console) _lock = _entMan.System<LockSystem>(); _sprite = _entMan.System<SpriteSystem>(); - _console = (console, _entMan.GetComponent<RoboticsConsoleComponent>(console), null); - _entMan.TryGetComponent(_console, out _console.Comp2); - Cyborgs.OnItemSelected += args => { if (Cyborgs[args.ItemIndex].Metadata is not string address) @@ -66,6 +64,11 @@ public RoboticsConsoleWindow(EntityUid console) DestroyButton.StyleClasses.Add(StyleBase.ButtonCaution); } + public void SetEntity(EntityUid uid) + { + Entity = uid; + } + public void UpdateState(RoboticsConsoleState state) { _cyborgs = state.Cyborgs; @@ -81,7 +84,7 @@ public void UpdateState(RoboticsConsoleState state) PopulateData(); - var locked = _lock.IsLocked((_console, _console.Comp2)); + var locked = _lock.IsLocked(Entity); DangerZone.Visible = !locked; LockedMessage.Visible = locked; } @@ -134,14 +137,20 @@ private void PopulateData() BorgInfo.SetMessage(text); // how the turntables - DisableButton.Disabled = !data.HasBrain; - DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy; + DisableButton.Disabled = !(data.HasBrain && data.CanDisable); } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy; + if (_entMan.TryGetComponent(Entity, out RoboticsConsoleComponent? console)) + { + DestroyButton.Disabled = _timing.CurTime < console.NextDestroy; + } + else + { + DestroyButton.Disabled = true; + } } } diff --git a/Content.Client/Roles/RoleCodewordSystem.cs b/Content.Client/Roles/RoleCodewordSystem.cs new file mode 100644 index 00000000000..8cc2e93099f --- /dev/null +++ b/Content.Client/Roles/RoleCodewordSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Roles.RoleCodeword; + +namespace Content.Client.Roles; + +public sealed class RoleCodewordSystem : SharedRoleCodewordSystem +{ + +} diff --git a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs index 587450a2f66..e7311953170 100644 --- a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs +++ b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs @@ -30,13 +30,12 @@ private void OnGetStatusIcon(EntityUid uid, SSDIndicatorComponent component, ref { if (component.IsSSD && _cfg.GetCVar(CCVars.ICShowSSDIndicator) && - !args.InContainer && !_mobState.IsDead(uid) && !HasComp<ActiveNPCComponent>(uid) && TryComp<MindContainerComponent>(uid, out var mindContainer) && mindContainer.ShowExamineInfo) { - args.StatusIcons.Add(_prototype.Index<StatusIconPrototype>(component.Icon)); + args.StatusIcons.Add(_prototype.Index(component.Icon)); } } } diff --git a/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs b/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs index 8f1723d1f22..663bde15b0d 100644 --- a/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs +++ b/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs @@ -30,17 +30,9 @@ public SalvageExpeditionConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : protected override void Open() { base.Open(); - _window = new OfferingWindow(); + _window = this.CreateWindow<OfferingWindow>(); _window.Title = Loc.GetString("salvage-expedition-window-title"); - _window.OnClose += Close; - _window?.OpenCenteredLeft(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _window?.Dispose(); - _window = null; + _window.OpenCenteredLeft(); } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs index 086369aa264..b8b4fb8a746 100644 --- a/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs +++ b/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs @@ -3,6 +3,7 @@ using Content.Shared.Shuttles.Events; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Shuttles.BUI; @@ -20,8 +21,7 @@ protected override void Open() { base.Open(); - _window = new IFFConsoleWindow(); - _window.OnClose += Close; + _window = this.CreateWindow<IFFConsoleWindow>(); _window.ShowIFF += SendIFFMessage; _window.ShowVessel += SendVesselMessage; _window.OpenCenteredLeft(); diff --git a/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs index 4bd44a47a8e..f75759b042f 100644 --- a/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs +++ b/Content.Client/Shuttles/BUI/RadarConsoleBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.Shuttles.BUIStates; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using RadarConsoleWindow = Content.Client.Shuttles.UI.RadarConsoleWindow; namespace Content.Client.Shuttles.BUI; @@ -20,18 +21,7 @@ protected override void Open() { base.Open(); - _window = new RadarConsoleWindow(); - _window.OnClose += Close; - _window.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - _window?.Dispose(); - } + _window = this.CreateWindow<RadarConsoleWindow>(); } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs index af7b6055c80..9643b5e9fe6 100644 --- a/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs +++ b/Content.Client/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs @@ -2,12 +2,13 @@ using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.Events; using JetBrains.Annotations; +using Robust.Client.UserInterface; using Robust.Shared.Map; namespace Content.Client.Shuttles.BUI; [UsedImplicitly] -public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface +public sealed partial class ShuttleConsoleBoundUserInterface : BoundUserInterface // Frontier: added partial { [ViewVariables] private ShuttleConsoleWindow? _window; @@ -19,14 +20,13 @@ public ShuttleConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owne protected override void Open() { base.Open(); - _window = new ShuttleConsoleWindow(); - _window.OpenCentered(); - _window.OnClose += Close; + _window = this.CreateWindow<ShuttleConsoleWindow>(); _window.RequestFTL += OnFTLRequest; _window.RequestBeaconFTL += OnFTLBeaconRequest; _window.DockRequest += OnDockRequest; _window.UndockRequest += OnUndockRequest; + NfOpen(); // Frontier } private void OnUndockRequest(NetEntity entity) diff --git a/Content.Client/Shuttles/UI/NavScreen.xaml b/Content.Client/Shuttles/UI/NavScreen.xaml index c97aeda05be..130d08c9cd4 100644 --- a/Content.Client/Shuttles/UI/NavScreen.xaml +++ b/Content.Client/Shuttles/UI/NavScreen.xaml @@ -65,5 +65,30 @@ Text="{controls:Loc 'shuttle-console-dock-toggle'}" TextAlign="Center" ToggleMode="True"/> + <!-- Frontier - Inertia dampener controls--> + <controls:BoxContainer Name="DampenerModeButtons" + Orientation="Horizontal" + HorizontalAlignment="Stretch" + Margin="5"> + <controls:Button Name="DampenerOff" + Text="{controls:Loc 'shuttle-console-inertia-dampener-off'}" + TextAlign="Center" + ToggleMode="True" + MinWidth="82" + MaxWidth="82"/> + <controls:Button Name="DampenerOn" + Text="{controls:Loc 'shuttle-console-inertia-dampener-dampen'}" + TextAlign="Center" + ToggleMode="True" + MinWidth="82" + MaxWidth="82"/> + <controls:Button Name="AnchorOn" + Text="{controls:Loc 'shuttle-console-inertia-dampener-anchor'}" + TextAlign="Center" + ToggleMode="True" + MinWidth="82" + MaxWidth="82"/> + </controls:BoxContainer> + <!-- End Frontier - Inertia dampener controls--> + </controls:BoxContainer> </controls:BoxContainer> - </controls:BoxContainer> diff --git a/Content.Client/Shuttles/UI/NavScreen.xaml.cs b/Content.Client/Shuttles/UI/NavScreen.xaml.cs index 91d95aaa042..73af6f62185 100644 --- a/Content.Client/Shuttles/UI/NavScreen.xaml.cs +++ b/Content.Client/Shuttles/UI/NavScreen.xaml.cs @@ -28,6 +28,8 @@ public NavScreen() DockToggle.OnToggled += OnDockTogglePressed; DockToggle.Pressed = NavRadar.ShowDocks; + + NfInitialize(); // Frontier Initialization for the NavScreen } public void SetShuttle(EntityUid? shuttle) @@ -50,6 +52,7 @@ private void OnDockTogglePressed(BaseButton.ButtonEventArgs args) public void UpdateState(NavInterfaceState scc) { NavRadar.UpdateState(scc); + NfUpdateState(); // Frontier Update State } public void SetMatrix(EntityCoordinates? coordinates, Angle? angle) diff --git a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs index a4b42fb672c..8df3eb4d93e 100644 --- a/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs +++ b/Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs @@ -62,6 +62,8 @@ public ShuttleConsoleWindow() { UndockRequest?.Invoke(entity); }; + + NfInitialize(); // Frontier Initialization for the ShuttleConsoleWindow } private void ClearModes(ShuttleConsoleMode mode) diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs index 0b8720add21..51fd68e679f 100644 --- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs +++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Client.Station; // Frontier using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; @@ -7,13 +8,11 @@ using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Collections; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; -using Robust.Shared.Utility; namespace Content.Client.Shuttles.UI; @@ -21,6 +20,8 @@ namespace Content.Client.Shuttles.UI; public sealed partial class ShuttleNavControl : BaseShuttleControl { [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IUserInterfaceManager _uiManager = default!; + private readonly StationSystem _station; // Frontier private readonly SharedShuttleSystem _shuttles; private readonly SharedTransformSystem _transform; @@ -48,6 +49,7 @@ public ShuttleNavControl() : base(64f, 256f, 256f) RobustXamlLoader.Load(this); _shuttles = EntManager.System<SharedShuttleSystem>(); _transform = EntManager.System<SharedTransformSystem>(); + _station = EntManager.System<StationSystem>(); // Frontier } public void SetMatrix(EntityCoordinates? coordinates, Angle? angle) @@ -110,6 +112,8 @@ public void UpdateState(NavInterfaceState state) ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange); _docks = state.Docks; + + NfUpdateState(state); // Frontier Update State } protected override void Draw(DrawingHandleScreen handle) diff --git a/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs b/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs index 3cc2a35d795..ed9bf40a481 100644 --- a/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs +++ b/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Silicons.Borgs; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Silicons.Borgs; @@ -18,9 +19,8 @@ protected override void Open() { base.Open(); - var owner = Owner; - - _menu = new BorgMenu(owner); + _menu = this.CreateWindow<BorgMenu>(); + _menu.SetEntity(Owner); _menu.BrainButtonPressed += () => { @@ -41,10 +41,6 @@ protected override void Open() { SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module))); }; - - _menu.OnClose += Close; - - _menu.OpenCentered(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -55,12 +51,4 @@ protected override void UpdateState(BoundUserInterfaceState state) return; _menu?.UpdateState(msg); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Dispose(); - } } diff --git a/Content.Client/Silicons/Borgs/BorgMenu.xaml b/Content.Client/Silicons/Borgs/BorgMenu.xaml index 7d8fd9fe57d..4cc2e41a8fb 100644 --- a/Content.Client/Silicons/Borgs/BorgMenu.xaml +++ b/Content.Client/Silicons/Borgs/BorgMenu.xaml @@ -10,7 +10,7 @@ VerticalExpand="True"> <BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="15 10 15 15"> <BoxContainer Orientation="Horizontal"> - <ProgressBar Name="ChargeBar" Access="Public" HorizontalExpand="True" MinValue="0" MaxValue="1"> + <ProgressBar Name="ChargeBar" Access="Public" HorizontalExpand="True" MinValue="0" Value="1" MaxValue="1"> <Label Name="ChargeLabel" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5 0 0 0"/> </ProgressBar> <Control MinWidth="5"/> diff --git a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs index 046d8e299fe..08f15d9de22 100644 --- a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs +++ b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs @@ -21,24 +21,33 @@ public sealed partial class BorgMenu : FancyWindow public Action<string>? NameChanged; public Action<EntityUid>? RemoveModuleButtonPressed; - private readonly BorgChassisComponent? _chassis; - public readonly EntityUid Entity; public float AccumulatedTime; private string _lastValidName; + private List<EntityUid> _modules = new(); - public BorgMenu(EntityUid entity) + public EntityUid Entity; + + public BorgMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - Entity = entity; + _lastValidName = NameLineEdit.Text; - if (_entity.TryGetComponent<BorgChassisComponent>(Entity, out var chassis)) - _chassis = chassis; + EjectBatteryButton.OnPressed += _ => EjectBatteryButtonPressed?.Invoke(); + BrainButton.OnPressed += _ => BrainButtonPressed?.Invoke(); + NameLineEdit.OnTextChanged += OnNameChanged; + NameLineEdit.OnTextEntered += OnNameEntered; + NameLineEdit.OnFocusExit += OnNameFocusExit; + + UpdateBrainButton(); + } + + public void SetEntity(EntityUid entity) + { + Entity = entity; BorgSprite.SetEntity(entity); - ChargeBar.MaxValue = 1f; - ChargeBar.Value = 1f; if (_entity.TryGetComponent<NameIdentifierComponent>(Entity, out var nameIdentifierComponent)) { @@ -54,17 +63,6 @@ public BorgMenu(EntityUid entity) NameIdentifierLabel.Visible = false; NameLineEdit.Text = _entity.GetComponent<MetaDataComponent>(Entity).EntityName; } - - _lastValidName = NameLineEdit.Text; - - EjectBatteryButton.OnPressed += _ => EjectBatteryButtonPressed?.Invoke(); - BrainButton.OnPressed += _ => BrainButtonPressed?.Invoke(); - - NameLineEdit.OnTextChanged += OnNameChanged; - NameLineEdit.OnTextEntered += OnNameEntered; - NameLineEdit.OnFocusExit += OnNameFocusExit; - - UpdateBrainButton(); } protected override void FrameUpdate(FrameEventArgs args) @@ -88,7 +86,7 @@ public void UpdateState(BorgBuiState state) private void UpdateBrainButton() { - if (_chassis?.BrainEntity is { } brain) + if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain) { BrainButton.Text = _entity.GetComponent<MetaDataComponent>(brain).EntityName; BrainView.Visible = true; @@ -107,15 +105,31 @@ private void UpdateBrainButton() private void UpdateModulePanel() { - if (_chassis == null) + if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis)) return; ModuleCounter.Text = Loc.GetString("borg-ui-module-counter", - ("actual", _chassis.ModuleCount), - ("max", _chassis.MaxModules)); + ("actual", chassis.ModuleCount), + ("max", chassis.MaxModules)); + + if (chassis.ModuleContainer.Count == _modules.Count) + { + var isSame = true; + foreach (var module in chassis.ModuleContainer.ContainedEntities) + { + if (_modules.Contains(module)) + continue; + isSame = false; + break; + } + + if (isSame) + return; + } ModuleContainer.Children.Clear(); - foreach (var module in _chassis.ModuleContainer.ContainedEntities) + _modules.Clear(); + foreach (var module in chassis.ModuleContainer.ContainedEntities) { var control = new BorgModuleControl(module, _entity); control.RemoveButtonPressed += () => diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawContainer.xaml b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawContainer.xaml new file mode 100644 index 00000000000..1bcac4bca65 --- /dev/null +++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawContainer.xaml @@ -0,0 +1,31 @@ +<BoxContainer + xmlns="https://spacestation14.io" + xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls" + HorizontalExpand="True" + Orientation="Vertical" + > + <customControls:HSeparator></customControls:HSeparator> + <BoxContainer> + <Label StyleClasses="SiliconLawPositionLabel" Name="PositionText" Margin="5 0 0 0"></Label> + <PanelContainer + Margin="20 10 0 0" + MinHeight="128" + > + <PanelContainer.PanelOverride> + <graphics:StyleBoxFlat BackgroundColor="#1B1B1B"></graphics:StyleBoxFlat> + </PanelContainer.PanelOverride> + <BoxContainer Orientation="Horizontal" SeparationOverride="5"> + <TextEdit Name="LawContent" HorizontalExpand="True" Editable="True" MinWidth="500" MinHeight="80"></TextEdit> + </BoxContainer> + </PanelContainer> + </BoxContainer> + <BoxContainer Orientation="Horizontal" Margin="0 5 0 0" MaxHeight="64" Align="Begin"> + <Button Name="MoveUp" Text="{Loc silicon-law-ui-minus-one}" StyleClasses="OpenRight"></Button> + <Button Name="MoveDown" Text="{Loc silicon-law-ui-plus-one}" StyleClasses="OpenLeft"></Button> + <CheckBox Name="Corrupted" Text="{Loc silicon-law-ui-check-corrupted}" ToolTip="{Loc silicon-law-ui-check-corrupted-tooltip}"></CheckBox> + </BoxContainer> + <BoxContainer Orientation="Horizontal" Align="End" Margin="0 10 5 10"> + <Button Name="Delete" Text="{Loc silicon-law-ui-delete}" ModulateSelfOverride="Red"></Button> + </BoxContainer> +</BoxContainer> diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawContainer.xaml.cs b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawContainer.xaml.cs new file mode 100644 index 00000000000..2e44b820df9 --- /dev/null +++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawContainer.xaml.cs @@ -0,0 +1,61 @@ +using Content.Shared.Silicons.Laws; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Utility; + +namespace Content.Client.Silicons.Laws.SiliconLawEditUi; + +[GenerateTypedNameReferences] +public sealed partial class SiliconLawContainer : BoxContainer +{ + public const string StyleClassSiliconLawPositionLabel = "SiliconLawPositionLabel"; + + public static readonly string CorruptedString = + Loc.GetString("ion-storm-law-scrambled-number", ("length", 5)); + + private SiliconLaw? _law; + + public event Action<SiliconLaw>? MoveLawUp; + public event Action<SiliconLaw>? MoveLawDown; + public event Action<SiliconLaw>? DeleteAction; + + + public SiliconLawContainer() + { + RobustXamlLoader.Load(this); + + MoveUp.OnPressed += _ => MoveLawUp?.Invoke(_law!); + MoveDown.OnPressed += _ => MoveLawDown?.Invoke(_law!); + Corrupted.OnPressed += _ => + { + if (Corrupted.Pressed) + { + _law!.LawIdentifierOverride = CorruptedString; + } + else + { + _law!.LawIdentifierOverride = null; + } + }; + + LawContent.OnTextChanged += _ => _law!.LawString = Rope.Collapse(LawContent.TextRope).Trim(); + LawContent.Placeholder = new Rope.Leaf(Loc.GetString("silicon-law-ui-placeholder")); + Delete.OnPressed += _ => DeleteAction?.Invoke(_law!); + } + + public void SetLaw(SiliconLaw law) + { + _law = law; + LawContent.TextRope = new Rope.Leaf(Loc.GetString(law.LawString)); + PositionText.Text = law.Order.ToString(); + if (!string.IsNullOrEmpty(law.LawIdentifierOverride)) + { + Corrupted.Pressed = true; + } + else + { + Corrupted.Pressed = false; + } + } +} diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs new file mode 100644 index 00000000000..a4d59d1f315 --- /dev/null +++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs @@ -0,0 +1,38 @@ +using Content.Client.Eui; +using Content.Shared.Eui; +using Content.Shared.Silicons.Laws; + +namespace Content.Client.Silicons.Laws.SiliconLawEditUi; + +public sealed class SiliconLawEui : BaseEui +{ + public readonly EntityManager _entityManager = default!; + + private SiliconLawUi _siliconLawUi; + private EntityUid _target; + + public SiliconLawEui() + { + _entityManager = IoCManager.Resolve<EntityManager>(); + + _siliconLawUi = new SiliconLawUi(); + _siliconLawUi.OnClose += () => SendMessage(new CloseEuiMessage()); + _siliconLawUi.Save.OnPressed += _ => SendMessage(new SiliconLawsSaveMessage(_siliconLawUi.GetLaws(), _entityManager.GetNetEntity(_target))); + } + + public override void HandleState(EuiStateBase state) + { + if (state is not SiliconLawsEuiState s) + { + return; + } + + _target = _entityManager.GetEntity(s.Target); + _siliconLawUi.SetLaws(s.Laws); + } + + public override void Opened() + { + _siliconLawUi.OpenCentered(); + } +} diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawUi.xaml b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawUi.xaml new file mode 100644 index 00000000000..19dcbac6202 --- /dev/null +++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawUi.xaml @@ -0,0 +1,22 @@ +<controls:FancyWindow + xmlns="https://spacestation14.io" + xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" + Title="{Loc silicon-law-ui-title}" + MinSize="560 400" +> + <!--> + this shit does not layout properly unless I put the horizontal boxcontainer inside of a vertical one + ???? + <!--> + <BoxContainer Orientation="Vertical"> + <BoxContainer Orientation="Horizontal" Align="End"> + <Button Name="NewLawButton" Text="{Loc silicon-law-ui-new-law}" MaxSize="256 64" StyleClasses="OpenRight"></Button> + <Button Name="Save" Text="{Loc silicon-law-ui-save}" MaxSize="256 64" Access="Public" StyleClasses="OpenLeft"></Button> + </BoxContainer> + </BoxContainer> + <BoxContainer Orientation="Vertical" Margin="4 60 0 0"> + <ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False"> + <BoxContainer Orientation="Vertical" Name="LawContainer" Access="Public" VerticalExpand="True" /> + </ScrollContainer> + </BoxContainer> +</controls:FancyWindow> diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawUi.xaml.cs b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawUi.xaml.cs new file mode 100644 index 00000000000..372961ea9a3 --- /dev/null +++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawUi.xaml.cs @@ -0,0 +1,89 @@ +using System.Linq; +using Content.Client.UserInterface.Controls; +using Content.Shared.FixedPoint; +using Content.Shared.Silicons.Laws; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Silicons.Laws.SiliconLawEditUi; + +[GenerateTypedNameReferences] +public sealed partial class SiliconLawUi : FancyWindow +{ + private List<SiliconLaw> _laws = new(); + + public SiliconLawUi() + { + RobustXamlLoader.Load(this); + NewLawButton.OnPressed += _ => AddNewLaw(); + } + + private void AddNewLaw() + { + var newLaw = new SiliconLaw(); + newLaw.Order = FixedPoint2.New(_laws.Count + 1); + _laws.Add(newLaw); + SetLaws(_laws); + } + + public void SetLaws(List<SiliconLaw> sLaws) + { + _laws = sLaws; + LawContainer.RemoveAllChildren(); + foreach (var law in sLaws.OrderBy(l => l.Order)) + { + var lawControl = new SiliconLawContainer(); + lawControl.SetLaw(law); + lawControl.MoveLawDown += MoveLawDown; + lawControl.MoveLawUp += MoveLawUp; + lawControl.DeleteAction += DeleteLaw; + + LawContainer.AddChild(lawControl); + } + } + + public void DeleteLaw(SiliconLaw law) + { + _laws.Remove(law); + SetLaws(_laws); + } + + public void MoveLawDown(SiliconLaw law) + { + if (_laws.Count == 0) + { + return; + } + + var index = _laws.IndexOf(law); + if (index == -1) + { + return; + } + + _laws[index].Order += FixedPoint2.New(1); + SetLaws(_laws); + } + + public void MoveLawUp(SiliconLaw law) + { + if (_laws.Count == 0) + { + return; + } + + var index = _laws.IndexOf(law); + if (index == -1) + { + return; + } + + _laws[index].Order += FixedPoint2.New(-1); + SetLaws(_laws); + } + + public List<SiliconLaw> GetLaws() + { + return _laws; + } +} diff --git a/Content.Client/Silicons/Laws/Ui/SiliconLawBoundUserInterface.cs b/Content.Client/Silicons/Laws/Ui/SiliconLawBoundUserInterface.cs index 2aee0a38c0f..93349794b82 100644 --- a/Content.Client/Silicons/Laws/Ui/SiliconLawBoundUserInterface.cs +++ b/Content.Client/Silicons/Laws/Ui/SiliconLawBoundUserInterface.cs @@ -1,6 +1,6 @@ using Content.Shared.Silicons.Laws.Components; using JetBrains.Annotations; -using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Silicons.Laws.Ui; @@ -20,18 +20,7 @@ protected override void Open() { base.Open(); - _menu = new(); - - _menu.OnClose += Close; - _menu.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Close(); + _menu = this.CreateWindow<SiliconLawMenu>(); } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Silicons/StationAi/StationAiOverlay.cs b/Content.Client/Silicons/StationAi/StationAiOverlay.cs new file mode 100644 index 00000000000..efa1b8dbeff --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiOverlay.cs @@ -0,0 +1,119 @@ +using System.Numerics; +using Content.Shared.Silicons.StationAi; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Silicons.StationAi; + +public sealed class StationAiOverlay : Overlay +{ + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private readonly HashSet<Vector2i> _visibleTiles = new(); + + private IRenderTexture? _staticTexture; + private IRenderTexture? _stencilTexture; + + public StationAiOverlay() + { + IoCManager.InjectDependencies(this); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (_stencilTexture?.Texture.Size != args.Viewport.Size) + { + _staticTexture?.Dispose(); + _stencilTexture?.Dispose(); + _stencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil"); + _staticTexture = _clyde.CreateRenderTarget(args.Viewport.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), + name: "station-ai-static"); + } + + var worldHandle = args.WorldHandle; + + var worldBounds = args.WorldBounds; + + var playerEnt = _player.LocalEntity; + _entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform); + var gridUid = playerXform?.GridUid ?? EntityUid.Invalid; + _entManager.TryGetComponent(gridUid, out MapGridComponent? grid); + + var invMatrix = args.Viewport.GetWorldToLocalMatrix(); + + if (grid != null) + { + // TODO: Pass in attached entity's grid. + // TODO: Credit OD on the moved to code + // TODO: Call the moved-to code here. + + _visibleTiles.Clear(); + var lookups = _entManager.System<EntityLookupSystem>(); + var xforms = _entManager.System<SharedTransformSystem>(); + _entManager.System<StationAiVisionSystem>().GetView((gridUid, grid), worldBounds, _visibleTiles); + + var gridMatrix = xforms.GetWorldMatrix(gridUid); + var matty = Matrix3x2.Multiply(gridMatrix, invMatrix); + + // Draw visible tiles to stencil + worldHandle.RenderInRenderTarget(_stencilTexture!, () => + { + worldHandle.SetTransform(matty); + + foreach (var tile in _visibleTiles) + { + var aabb = lookups.GetLocalBounds(tile, grid.TileSize); + worldHandle.DrawRect(aabb, Color.White); + } + }, + Color.Transparent); + + // Once this is gucci optimise rendering. + worldHandle.RenderInRenderTarget(_staticTexture!, + () => + { + worldHandle.SetTransform(invMatrix); + var shader = _proto.Index<ShaderPrototype>("CameraStatic").Instance(); + worldHandle.UseShader(shader); + worldHandle.DrawRect(worldBounds, Color.White); + }, + Color.Black); + } + // Not on a grid + else + { + worldHandle.RenderInRenderTarget(_stencilTexture!, () => + { + }, + Color.Transparent); + + worldHandle.RenderInRenderTarget(_staticTexture!, + () => + { + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.DrawRect(worldBounds, Color.Black); + }, Color.Black); + } + + // Use the lighting as a mask + worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance()); + worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds); + + // Draw the static + worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilDraw").Instance()); + worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds); + + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(null); + + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.cs b/Content.Client/Silicons/StationAi/StationAiSystem.cs new file mode 100644 index 00000000000..2ed06175252 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiSystem.cs @@ -0,0 +1,80 @@ +using Content.Shared.Silicons.StationAi; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.Silicons.StationAi; + +public sealed partial class StationAiSystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlayMgr = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + private StationAiOverlay? _overlay; + + public override void Initialize() + { + base.Initialize(); + // InitializeAirlock(); + // InitializePowerToggle(); + + SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerAttachedEvent>(OnAiAttached); + SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerDetachedEvent>(OnAiDetached); + SubscribeLocalEvent<StationAiOverlayComponent, ComponentInit>(OnAiOverlayInit); + SubscribeLocalEvent<StationAiOverlayComponent, ComponentRemove>(OnAiOverlayRemove); + } + + private void OnAiOverlayInit(Entity<StationAiOverlayComponent> ent, ref ComponentInit args) + { + var attachedEnt = _player.LocalEntity; + + if (attachedEnt != ent.Owner) + return; + + AddOverlay(); + } + + private void OnAiOverlayRemove(Entity<StationAiOverlayComponent> ent, ref ComponentRemove args) + { + var attachedEnt = _player.LocalEntity; + + if (attachedEnt != ent.Owner) + return; + + RemoveOverlay(); + } + + private void AddOverlay() + { + if (_overlay != null) + return; + + _overlay = new StationAiOverlay(); + _overlayMgr.AddOverlay(_overlay); + } + + private void RemoveOverlay() + { + if (_overlay == null) + return; + + _overlayMgr.RemoveOverlay(_overlay); + _overlay = null; + } + + private void OnAiAttached(Entity<StationAiOverlayComponent> ent, ref LocalPlayerAttachedEvent args) + { + AddOverlay(); + } + + private void OnAiDetached(Entity<StationAiOverlayComponent> ent, ref LocalPlayerDetachedEvent args) + { + RemoveOverlay(); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayMgr.RemoveOverlay<StationAiOverlay>(); + } +} diff --git a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs index e8442d23908..7d6a6cf2a5a 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs @@ -1,6 +1,6 @@ using Content.Shared.SprayPainter; using Content.Shared.SprayPainter.Components; -using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; namespace Content.Client.SprayPainter.UI; @@ -10,9 +10,6 @@ public sealed class SprayPainterBoundUserInterface : BoundUserInterface [ViewVariables] private SprayPainterWindow? _window; - [ViewVariables] - private SprayPainterSystem? _painter; - public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -21,27 +18,15 @@ protected override void Open() { base.Open(); - if (!EntMan.TryGetComponent<SprayPainterComponent>(Owner, out var comp)) - return; - - _window = new SprayPainterWindow(); + _window = this.CreateWindow<SprayPainterWindow>(); - _painter = EntMan.System<SprayPainterSystem>(); - - _window.OnClose += Close; _window.OnSpritePicked = OnSpritePicked; _window.OnColorPicked = OnColorPicked; - _window.Populate(_painter.Entries, comp.Index, comp.PickedColor, comp.ColorPalette); - - _window.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - _window?.Dispose(); + if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp)) + { + _window.Populate(EntMan.System<SprayPainterSystem>().Entries, comp.Index, comp.PickedColor, comp.ColorPalette); + } } private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args) diff --git a/Content.Client/Sprite/SpriteFadeSystem.cs b/Content.Client/Sprite/SpriteFadeSystem.cs index dda3a6c948e..d9584b60a65 100644 --- a/Content.Client/Sprite/SpriteFadeSystem.cs +++ b/Content.Client/Sprite/SpriteFadeSystem.cs @@ -3,6 +3,7 @@ using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.State; +using Robust.Shared.Physics; namespace Content.Client.Sprite; @@ -15,6 +16,7 @@ public sealed class SpriteFadeSystem : EntitySystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IStateManager _stateManager = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; private readonly HashSet<FadingSpriteComponent> _comps = new(); @@ -48,7 +50,7 @@ _stateManager.CurrentState is GameplayState state && spriteQuery.TryGetComponent(player, out var playerSprite)) { var fadeQuery = GetEntityQuery<SpriteFadeComponent>(); - var mapPos = playerXform.MapPosition; + var mapPos = _transform.GetMapCoordinates(_playerManager.LocalEntity!.Value, xform: playerXform); // Also want to handle large entities even if they may not be clickable. foreach (var ent in state.GetClickableEntities(mapPos)) diff --git a/Content.Client/StationRecords/GeneralRecord.xaml b/Content.Client/StationRecords/GeneralRecord.xaml new file mode 100644 index 00000000000..add688c3f38 --- /dev/null +++ b/Content.Client/StationRecords/GeneralRecord.xaml @@ -0,0 +1,13 @@ +<Control xmlns="https://spacestation14.io"> + <BoxContainer Orientation="Vertical" Margin="5"> + <Label Name="RecordName" StyleClasses="LabelBig"/> + <Label Name="Age"/> + <Label Name="Title"/> + <Label Name="Job"/> + <Label Name="Species"/> + <Label Name="Gender"/> + <Label Name="Fingerprint"/> + <Label Name="Dna"/> + <Button Visible="False" Name="DeleteButton" Text="{Loc 'general-station-record-console-delete'}"/> + </BoxContainer> +</Control> diff --git a/Content.Client/StationRecords/GeneralRecord.xaml.cs b/Content.Client/StationRecords/GeneralRecord.xaml.cs new file mode 100644 index 00000000000..e84c2dc0982 --- /dev/null +++ b/Content.Client/StationRecords/GeneralRecord.xaml.cs @@ -0,0 +1,34 @@ +using Content.Shared.StationRecords; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Enums; + +namespace Content.Client.StationRecords; + +[GenerateTypedNameReferences] +public sealed partial class GeneralRecord : Control +{ + public Action<uint>? OnDeletePressed; + public GeneralRecord(GeneralStationRecord record, bool canDelete, uint? id) + { + RobustXamlLoader.Load(this); + RecordName.Text = record.Name; + Age.Text = Loc.GetString("general-station-record-console-record-age", ("age", record.Age.ToString())); + Title.Text = Loc.GetString("general-station-record-console-record-title", + ("job", Loc.GetString(record.JobTitle))); + Species.Text = Loc.GetString("general-station-record-console-record-species", ("species", record.Species)); + Gender.Text = Loc.GetString("general-station-record-console-record-gender", + ("gender", record.Gender.ToString())); + Fingerprint.Text = Loc.GetString("general-station-record-console-record-fingerprint", + ("fingerprint", record.Fingerprint ?? Loc.GetString("generic-not-available-shorthand"))); + Dna.Text = Loc.GetString("general-station-record-console-record-dna", + ("dna", record.DNA ?? Loc.GetString("generic-not-available-shorthand"))); + + if (canDelete && id != null ) + { + DeleteButton.Visible = true; + DeleteButton.OnPressed += _ => OnDeletePressed?.Invoke(id.Value); + } + } +} diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs index 3be3d98778d..e7bab71e38e 100644 --- a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs +++ b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs @@ -1,4 +1,5 @@ using Content.Shared.StationRecords; +using Robust.Client.UserInterface; namespace Content.Client.StationRecords; @@ -15,14 +16,12 @@ protected override void Open() { base.Open(); - _window = new(); + _window = this.CreateWindow<GeneralStationRecordConsoleWindow>(); _window.OnKeySelected += key => SendMessage(new SelectStationRecord(key)); _window.OnFiltersChanged += (type, filterValue) => SendMessage(new SetStationRecordFilter(type, filterValue)); - _window.OnClose += Close; - - _window.OpenCentered(); + _window.OnDeleted += id => SendMessage(new DeleteStationRecord(id)); } protected override void UpdateState(BoundUserInterfaceState state) @@ -34,11 +33,4 @@ protected override void UpdateState(BoundUserInterfaceState state) _window?.UpdateState(cast); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - _window?.Close(); - } } diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml index a915d329bc3..3615eb8e008 100644 --- a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml +++ b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml @@ -19,7 +19,7 @@ </BoxContainer> <BoxContainer Orientation="Vertical" Margin="5"> <Label Name="RecordContainerStatus" Visible="False" Text="{Loc 'general-station-record-console-select-record-info'}"/> - <BoxContainer Name="RecordContainer" Orientation="Vertical" /> + <Control Name="RecordContainer" Visible="False"/> </BoxContainer> </BoxContainer> </BoxContainer> diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs index fbdd6c2f0b5..272e6c3b251 100644 --- a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs +++ b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs @@ -1,7 +1,5 @@ using Content.Shared.StationRecords; using Robust.Client.AutoGenerated; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; @@ -13,6 +11,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow public Action<uint?>? OnKeySelected; public Action<StationRecordFilterType, string>? OnFiltersChanged; + public Action<uint>? OnDeleted; private bool _isPopulating; @@ -112,11 +111,10 @@ public void UpdateState(GeneralStationRecordConsoleState state) RecordContainerStatus.Text = state.SelectedKey == null ? Loc.GetString("general-station-record-console-no-record-found") : Loc.GetString("general-station-record-console-select-record-info"); - PopulateRecordContainer(state.Record); + PopulateRecordContainer(state.Record, state.CanDeleteEntries, state.SelectedKey); } else { - RecordContainer.DisposeAllChildren(); RecordContainer.RemoveAllChildren(); } } @@ -138,49 +136,13 @@ private void PopulateRecordListing(Dictionary<uint, string> listing, uint? selec RecordListing.SortItemsByText(); } - private void PopulateRecordContainer(GeneralStationRecord record) + private void PopulateRecordContainer(GeneralStationRecord record, bool enableDelete, uint? id) { - RecordContainer.DisposeAllChildren(); RecordContainer.RemoveAllChildren(); - // sure - var recordControls = new Control[] - { - new Label() - { - Text = record.Name, - StyleClasses = { "LabelBig" } - }, - new Label() - { - Text = Loc.GetString("general-station-record-console-record-age", ("age", record.Age.ToString())) - - }, - new Label() - { - Text = Loc.GetString("general-station-record-console-record-title", ("job", Loc.GetString(record.JobTitle))) - }, - new Label() - { - Text = Loc.GetString("general-station-record-console-record-species", ("species", record.Species)) - }, - new Label() - { - Text = Loc.GetString("general-station-record-console-record-gender", ("gender", record.Gender.ToString())) - }, - new Label() - { - Text = Loc.GetString("general-station-record-console-record-fingerprint", ("fingerprint", record.Fingerprint ?? Loc.GetString("generic-not-available-shorthand"))) - }, - new Label() - { - Text = Loc.GetString("general-station-record-console-record-dna", ("dna", record.DNA ?? Loc.GetString("generic-not-available-shorthand"))) - } - }; + var newRecord = new GeneralRecord(record, enableDelete, id); + newRecord.OnDeletePressed = OnDeleted; - foreach (var control in recordControls) - { - RecordContainer.AddChild(control); - } + RecordContainer.AddChild(newRecord); } private void FilterListingOfRecords(string text = "") @@ -195,4 +157,5 @@ private string GetTypeFilterLocals(StationRecordFilterType type) { return Loc.GetString($"general-station-record-{type.ToString().ToLower()}-filter"); } + } diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs index 372bd04f571..7bebac273b9 100644 --- a/Content.Client/StatusIcon/StatusIconOverlay.cs +++ b/Content.Client/StatusIcon/StatusIconOverlay.cs @@ -45,7 +45,7 @@ protected override void Draw(in OverlayDrawArgs args) var query = _entity.AllEntityQueryEnumerator<StatusIconComponent, SpriteComponent, TransformComponent, MetaDataComponent>(); while (query.MoveNext(out var uid, out var comp, out var sprite, out var xform, out var meta)) { - if (xform.MapID != args.MapId) + if (xform.MapID != args.MapId || !sprite.Visible) continue; var bounds = comp.Bounds ?? sprite.Bounds; @@ -72,6 +72,8 @@ protected override void Draw(in OverlayDrawArgs args) foreach (var proto in icons) { + if (!_statusIcon.IsVisible((uid, meta), proto)) + continue; var curTime = _timing.RealTime; var texture = _sprite.GetFrame(proto.Icon, curTime); diff --git a/Content.Client/StatusIcon/StatusIconSystem.cs b/Content.Client/StatusIcon/StatusIconSystem.cs index 980fd9f2a92..63f57767695 100644 --- a/Content.Client/StatusIcon/StatusIconSystem.cs +++ b/Content.Client/StatusIcon/StatusIconSystem.cs @@ -1,7 +1,11 @@ using Content.Shared.CCVar; +using Content.Shared.Ghost; using Content.Shared.StatusIcon; using Content.Shared.StatusIcon.Components; +using Content.Shared.Stealth.Components; +using Content.Shared.Whitelist; using Robust.Client.Graphics; +using Robust.Client.Player; using Robust.Shared.Configuration; namespace Content.Client.StatusIcon; @@ -13,6 +17,8 @@ public sealed class StatusIconSystem : SharedStatusIconSystem { [Dependency] private readonly IConfigurationManager _configuration = default!; [Dependency] private readonly IOverlayManager _overlay = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; private bool _globalEnabled; private bool _localEnabled; @@ -54,10 +60,34 @@ public List<StatusIconData> GetStatusIcons(EntityUid uid, MetaDataComponent? met if (meta.EntityLifeStage >= EntityLifeStage.Terminating) return list; - var inContainer = (meta.Flags & MetaDataFlags.InContainer) != 0; - var ev = new GetStatusIconsEvent(list, inContainer); + var ev = new GetStatusIconsEvent(list); RaiseLocalEvent(uid, ref ev); return ev.StatusIcons; } -} + /// <summary> + /// For overlay to check if an entity can be seen. + /// </summary> + public bool IsVisible(Entity<MetaDataComponent> ent, StatusIconData data) + { + var viewer = _playerManager.LocalSession?.AttachedEntity; + + // Always show our icons to our entity + if (viewer == ent.Owner) + return true; + + if (data.VisibleToGhosts && HasComp<GhostComponent>(viewer)) + return true; + + if (data.HideInContainer && (ent.Comp.Flags & MetaDataFlags.InContainer) != 0) + return false; + + if (data.HideOnStealth && TryComp<StealthComponent>(ent, out var stealth) && stealth.Enabled) + return false; + + if (data.ShowTo != null && !_entityWhitelist.IsValid(data.ShowTo, viewer)) + return false; + + return true; + } +} diff --git a/Content.Client/Stealth/StealthSystem.cs b/Content.Client/Stealth/StealthSystem.cs index b60ffc2a408..0b94e41f6bc 100644 --- a/Content.Client/Stealth/StealthSystem.cs +++ b/Content.Client/Stealth/StealthSystem.cs @@ -1,4 +1,5 @@ using Content.Client.Interactable.Components; +using Content.Client.StatusIcon; using Content.Shared.Stealth; using Content.Shared.Stealth.Components; using Robust.Client.GameObjects; @@ -18,6 +19,7 @@ public override void Initialize() base.Initialize(); _shader = _protoMan.Index<ShaderPrototype>("Stealth").InstanceUnique(); + SubscribeLocalEvent<StealthComponent, ComponentShutdown>(OnShutdown); SubscribeLocalEvent<StealthComponent, ComponentStartup>(OnStartup); SubscribeLocalEvent<StealthComponent, BeforePostShaderRenderEvent>(OnShaderRender); @@ -93,4 +95,3 @@ private void OnShaderRender(EntityUid uid, StealthComponent component, BeforePos args.Sprite.Color = new Color(visibility, visibility, 1, 1); } } - diff --git a/Content.Client/Storage/Systems/ItemCounterSystem.cs b/Content.Client/Storage/Systems/ItemCounterSystem.cs index 605f47d3b8a..fcb1ca17dc6 100644 --- a/Content.Client/Storage/Systems/ItemCounterSystem.cs +++ b/Content.Client/Storage/Systems/ItemCounterSystem.cs @@ -31,7 +31,7 @@ private void OnAppearanceChange(EntityUid uid, ItemCounterComponent comp, ref Ap if (!_appearanceSystem.TryGetData<bool>(uid, StackVisuals.Hide, out var hidden, args.Component)) hidden = false; - + if (comp.IsComposite) ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite); else diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs index 88ad0e3de8b..8c48258de00 100644 --- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs +++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs @@ -1,6 +1,8 @@ using Content.Shared.Store; using JetBrains.Annotations; using System.Linq; +using Content.Shared.Store.Components; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.Store.Ui; @@ -13,14 +15,11 @@ public sealed class StoreBoundUserInterface : BoundUserInterface [ViewVariables] private StoreMenu? _menu; - [ViewVariables] - private string _windowName = Loc.GetString("store-ui-default-title"); - [ViewVariables] private string _search = string.Empty; [ViewVariables] - private HashSet<ListingData> _listings = new(); + private HashSet<ListingDataWithCostModifiers> _listings = new(); public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { @@ -28,14 +27,13 @@ public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) protected override void Open() { - _menu = new StoreMenu(_windowName); - - _menu.OpenCentered(); - _menu.OnClose += Close; + _menu = this.CreateWindow<StoreMenu>(); + if (EntMan.TryGetComponent<StoreComponent>(Owner, out var store)) + _menu.Title = Loc.GetString(store.Name); _menu.OnListingButtonPressed += (_, listing) => { - SendMessage(new StoreBuyListingMessage(listing)); + SendMessage(new StoreBuyListingMessage(listing.ID)); }; _menu.OnCategoryButtonPressed += (_, category) => @@ -64,44 +62,26 @@ protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); - if (_menu == null) - return; - switch (state) { case StoreUpdateState msg: _listings = msg.Listings; - _menu.UpdateBalance(msg.Balance); + _menu?.UpdateBalance(msg.Balance); + UpdateListingsWithSearchFilter(); - _menu.SetFooterVisibility(msg.ShowFooter); - _menu.UpdateRefund(msg.AllowRefund); - break; - case StoreInitializeState msg: - _windowName = msg.Name; - if (_menu != null && _menu.Window != null) - { - _menu.Window.Title = msg.Name; - } + _menu?.SetFooterVisibility(msg.ShowFooter); + _menu?.UpdateRefund(msg.AllowRefund); break; } } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - _menu?.Close(); - _menu?.Dispose(); - } - private void UpdateListingsWithSearchFilter() { if (_menu == null) return; - var filteredListings = new HashSet<ListingData>(_listings); + var filteredListings = new HashSet<ListingDataWithCostModifiers>(_listings); if (!string.IsNullOrEmpty(_search)) { filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) && diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml b/Content.Client/Store/Ui/StoreListingControl.xaml index 12b4d7b5b30..3142f1cb061 100644 --- a/Content.Client/Store/Ui/StoreListingControl.xaml +++ b/Content.Client/Store/Ui/StoreListingControl.xaml @@ -2,6 +2,8 @@ <BoxContainer Margin="8,8,8,8" Orientation="Vertical"> <BoxContainer Orientation="Horizontal"> <Label Name="StoreItemName" HorizontalExpand="True" /> + <Label Name="DiscountSubText" + HorizontalAlignment="Right"/> <Button Name="StoreItemBuyButton" MinWidth="64" diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml.cs b/Content.Client/Store/Ui/StoreListingControl.xaml.cs index 030f07dc7ca..20629104acc 100644 --- a/Content.Client/Store/Ui/StoreListingControl.xaml.cs +++ b/Content.Client/Store/Ui/StoreListingControl.xaml.cs @@ -17,11 +17,12 @@ public sealed partial class StoreListingControl : Control [Dependency] private readonly IGameTiming _timing = default!; private readonly ClientGameTicker _ticker; - private readonly ListingData _data; + private readonly ListingDataWithCostModifiers _data; private readonly bool _hasBalance; private readonly string _price; - public StoreListingControl(ListingData data, string price, bool hasBalance, Texture? texture = null) + private readonly string _discount; + public StoreListingControl(ListingDataWithCostModifiers data, string price, string discount, bool hasBalance, Texture? texture = null) { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); @@ -31,6 +32,7 @@ public StoreListingControl(ListingData data, string price, bool hasBalance, Text _data = data; _hasBalance = hasBalance; _price = price; + _discount = discount; StoreItemName.Text = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype); StoreItemDescription.SetMessage(ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(_data, _prototype)); @@ -63,6 +65,7 @@ private void UpdateBuyButtonText() } else { + DiscountSubText.Text = _discount; StoreItemBuyButton.Text = _price; } } diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs index 7eb597f2f39..58aebaddc67 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml.cs +++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Text; using Content.Client.Actions; using Content.Client.Message; using Content.Shared.FixedPoint; @@ -23,7 +24,7 @@ public sealed partial class StoreMenu : DefaultWindow private StoreWithdrawWindow? _withdrawWindow; public event EventHandler<string>? SearchTextUpdated; - public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed; + public event Action<BaseButton.ButtonEventArgs, ListingDataWithCostModifiers>? OnListingButtonPressed; public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed; public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt; public event Action<BaseButton.ButtonEventArgs>? OnRefundAttempt; @@ -31,9 +32,9 @@ public sealed partial class StoreMenu : DefaultWindow public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Balance = new(); public string CurrentCategory = string.Empty; - private List<ListingData> _cachedListings = new(); + private List<ListingDataWithCostModifiers> _cachedListings = new(); - public StoreMenu(string name) + public StoreMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); @@ -41,9 +42,6 @@ public StoreMenu(string name) WithdrawButton.OnButtonDown += OnWithdrawButtonDown; RefundButton.OnButtonDown += OnRefundButtonDown; SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text); - - if (Window != null) - Window.Title = name; } public void UpdateBalance(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance) @@ -72,15 +70,17 @@ public void UpdateBalance(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> ba WithdrawButton.Disabled = disabled; } - public void UpdateListing(List<ListingData> listings) + public void UpdateListing(List<ListingDataWithCostModifiers> listings) { _cachedListings = listings; + UpdateListing(); } public void UpdateListing() { - var sorted = _cachedListings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum()); + var sorted = _cachedListings.OrderBy(l => l.Priority) + .ThenBy(l => l.Cost.Values.Sum()); // should probably chunk these out instead. to-do if this clogs the internet tubes. // maybe read clients prototypes instead? @@ -118,13 +118,12 @@ private void OnRefundButtonDown(BaseButton.ButtonEventArgs args) OnRefundAttempt?.Invoke(args); } - private void AddListingGui(ListingData listing) + private void AddListingGui(ListingDataWithCostModifiers listing) { if (!listing.Categories.Contains(CurrentCategory)) return; - var listingPrice = listing.Cost; - var hasBalance = HasListingPrice(Balance, listingPrice); + var hasBalance = listing.CanBuyWith(Balance); var spriteSys = _entityManager.EntitySysManager.GetEntitySystem<SpriteSystem>(); @@ -147,33 +146,20 @@ private void AddListingGui(ListingData listing) } } - var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture); - - if (listing.DiscountValue > 0) - newListing.StoreItemBuyButton.AddStyleClass(StyleNano.ButtonColorDangerDefault.ToString()); + var listingInStock = GetListingPriceString(listing); + var discount = GetDiscountString(listing); + var newListing = new StoreListingControl(listing, listingInStock, discount, hasBalance, texture); newListing.StoreItemBuyButton.OnButtonDown += args => OnListingButtonPressed?.Invoke(args, listing); StoreListingsContainer.AddChild(newListing); } - public bool HasListingPrice(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> currency, Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> price) - { - foreach (var type in price) - { - if (!currency.ContainsKey(type.Key)) - return false; - - if (currency[type.Key] < type.Value) - return false; - } - return true; - } - - public string GetListingPriceString(ListingData listing) + private string GetListingPriceString(ListingDataWithCostModifiers listing) { var text = string.Empty; + if (listing.Cost.Count < 1) text = Loc.GetString("store-currency-free"); else @@ -181,20 +167,72 @@ public string GetListingPriceString(ListingData listing) foreach (var (type, amount) in listing.Cost) { var currency = _prototypeManager.Index(type); - text += Loc.GetString("store-ui-price-display", ("amount", amount), - ("currency", Loc.GetString(currency.DisplayName, ("amount", amount)))); + + text += Loc.GetString( + "store-ui-price-display", + ("amount", amount), + ("currency", Loc.GetString(currency.DisplayName, ("amount", amount))) + ); } } return text.TrimEnd(); } + private string GetDiscountString(ListingDataWithCostModifiers listingDataWithCostModifiers) + { + string discountMessage; + + if (!listingDataWithCostModifiers.IsCostModified) + { + return string.Empty; + } + + var relativeModifiersSummary = listingDataWithCostModifiers.GetModifiersSummaryRelative(); + if (relativeModifiersSummary.Count > 1) + { + var sb = new StringBuilder(); + sb.Append('('); + foreach (var (currency, amount) in relativeModifiersSummary) + { + var currencyPrototype = _prototypeManager.Index(currency); + if (sb.Length != 0) + { + sb.Append(", "); + } + var currentDiscountMessage = Loc.GetString( + "store-ui-discount-display-with-currency", + ("amount", amount.ToString("P0")), + ("currency", Loc.GetString(currencyPrototype.DisplayName)) + ); + sb.Append(currentDiscountMessage); + } + + sb.Append(')'); + discountMessage = sb.ToString(); + } + else + { + // if cost was modified - it should have diff relatively to original cost in 1 or more currency + // ReSharper disable once GenericEnumeratorNotDisposed Dictionary enumerator doesn't require dispose + var enumerator = relativeModifiersSummary.GetEnumerator(); + enumerator.MoveNext(); + var amount = enumerator.Current.Value; + discountMessage = Loc.GetString( + "store-ui-discount-display", + ("amount", (amount.ToString("P0"))) + ); + } + + return discountMessage; + } + private void ClearListings() { StoreListingsContainer.Children.Clear(); } - public void PopulateStoreCategoryButtons(HashSet<ListingData> listings) + public void PopulateStoreCategoryButtons(HashSet<ListingDataWithCostModifiers> listings) { var allCategories = new List<StoreCategoryPrototype>(); foreach (var listing in listings) diff --git a/Content.Client/Strip/StrippingMenu.cs b/Content.Client/Strip/StrippingMenu.cs index eea867b7948..1c46b4be35c 100644 --- a/Content.Client/Strip/StrippingMenu.cs +++ b/Content.Client/Strip/StrippingMenu.cs @@ -1,4 +1,3 @@ -using Content.Client.Inventory; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Timing; @@ -11,14 +10,12 @@ public sealed class StrippingMenu : DefaultWindow public LayoutContainer InventoryContainer = new(); public BoxContainer HandsContainer = new() { Orientation = LayoutOrientation.Horizontal }; public BoxContainer SnareContainer = new(); - private StrippableBoundUserInterface _bui; public bool Dirty = true; - public StrippingMenu(string title, StrippableBoundUserInterface bui) - { - Title = title; - _bui = bui; + public event Action? OnDirty; + public StrippingMenu() + { var box = new BoxContainer() { Orientation = LayoutOrientation.Vertical, Margin = new Thickness(0, 8) }; Contents.AddChild(box); box.AddChild(SnareContainer); @@ -39,7 +36,7 @@ protected override void FrameUpdate(FrameEventArgs args) return; Dirty = false; - _bui.UpdateMenu(); + OnDirty?.Invoke(); } } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index eff0171d740..2bbf7af5506 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -4,6 +4,7 @@ using Content.Client.Examine; using Content.Client.PDA; using Content.Client.Resources; +using Content.Client.Silicons.Laws.SiliconLawEditUi; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls.FancyTree; using Content.Client.Verbs.UI; @@ -1626,6 +1627,10 @@ public StyleNano(IResourceCache resCache) : base(resCache) { BackgroundColor = FancyTreeSelectedRowColor, }), + + // Silicon law edit ui + Element<Label>().Class(SiliconLawContainer.StyleClassSiliconLawPositionLabel) + .Prop(Label.StylePropertyFontColor, NanoGold), // Pinned button style new StyleRule( new SelectorElement(typeof(TextureButton), new[] { StyleClassPinButtonPinned }, null, null), diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs index 9132dd6ed5f..e3646c00cc3 100644 --- a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs +++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs @@ -1,6 +1,7 @@ using Content.Client.Eye; using Content.Shared.SurveillanceCamera; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.SurveillanceCamera.UI; @@ -25,20 +26,12 @@ protected override void Open() { base.Open(); - _window = new SurveillanceCameraMonitorWindow(); - - if (State != null) - { - UpdateState(State); - } - - _window.OpenCentered(); + _window = this.CreateWindow<SurveillanceCameraMonitorWindow>(); _window.CameraSelected += OnCameraSelected; _window.SubnetOpened += OnSubnetRequest; _window.CameraRefresh += OnCameraRefresh; _window.SubnetRefresh += OnSubnetRefresh; - _window.OnClose += Close; _window.CameraSwitchTimer += OnCameraSwitchTimer; _window.CameraDisconnect += OnCameraDisconnect; } diff --git a/Content.Client/Tabletop/TabletopSystem.cs b/Content.Client/Tabletop/TabletopSystem.cs index 696c1455e0c..0b55a1839c0 100644 --- a/Content.Client/Tabletop/TabletopSystem.cs +++ b/Content.Client/Tabletop/TabletopSystem.cs @@ -258,7 +258,7 @@ private void StopDragging(bool broadcast = true) // Set the dragging player on the component to noone if (broadcast && _draggedEntity != null && EntityManager.HasComponent<TabletopDraggableComponent>(_draggedEntity.Value)) { - RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transform(_draggedEntity.Value).MapPosition, GetNetEntity(_table!.Value))); + RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transforms.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value))); RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false)); } @@ -277,7 +277,8 @@ private static MapCoordinates ClampPositionToViewport(MapCoordinates coordinates if (coordinates == MapCoordinates.Nullspace) return MapCoordinates.Nullspace; var eye = viewport.Eye; - if (eye == null) return MapCoordinates.Nullspace; + if (eye == null) + return MapCoordinates.Nullspace; var size = (Vector2) viewport.ViewportSize / EyeManager.PixelsPerMeter; // Convert to tiles instead of pixels var eyePosition = eye.Position.Position; diff --git a/Content.Client/Thief/ThiefBackpackBoundUserInterface.cs b/Content.Client/Thief/ThiefBackpackBoundUserInterface.cs index 37384daafef..0631d98993a 100644 --- a/Content.Client/Thief/ThiefBackpackBoundUserInterface.cs +++ b/Content.Client/Thief/ThiefBackpackBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Thief; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Thief; @@ -15,21 +16,9 @@ protected override void Open() { base.Open(); - _window = new ThiefBackpackMenu(this); - _window.OnClose += Close; - _window.OpenCentered(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - if (_window != null) - _window.OnClose -= Close; - - _window?.Dispose(); + _window = this.CreateWindow<ThiefBackpackMenu>(); + _window.OnApprove += SendApprove; + _window.OnSetChange += SendChangeSelected; } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Thief/ThiefBackpackMenu.xaml.cs b/Content.Client/Thief/ThiefBackpackMenu.xaml.cs index b2314cf3fe2..d9be1a6305a 100644 --- a/Content.Client/Thief/ThiefBackpackMenu.xaml.cs +++ b/Content.Client/Thief/ThiefBackpackMenu.xaml.cs @@ -12,45 +12,41 @@ public sealed partial class ThiefBackpackMenu : FancyWindow [Dependency] private readonly IEntitySystemManager _sysMan = default!; private readonly SpriteSystem _spriteSystem; - private readonly ThiefBackpackBoundUserInterface _owner; + public event Action? OnApprove; + public event Action<int>? OnSetChange; - public ThiefBackpackMenu(ThiefBackpackBoundUserInterface owner) + public ThiefBackpackMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _spriteSystem = _sysMan.GetEntitySystem<SpriteSystem>(); - _owner = owner; - - ApproveButton.OnButtonDown += (args) => + ApproveButton.OnPressed += args => { - _owner.SendApprove(); + OnApprove?.Invoke(); }; } public void UpdateState(ThiefBackpackBoundUserInterfaceState state) { - SetsGrid.RemoveAllChildren(); - int count = 0; - int selectedNumber = 0; - foreach (var set in state.Sets) + SetsGrid.DisposeAllChildren(); + var selectedNumber = 0; + foreach (var (set, info) in state.Sets) { - var child = new ThiefBackpackSet(set.Value, _spriteSystem); + var child = new ThiefBackpackSet(info, _spriteSystem); child.SetButton.OnButtonDown += (args) => { - _owner.SendChangeSelected(set.Key); + OnSetChange?.Invoke(set); }; SetsGrid.AddChild(child); - count++; - - if (set.Value.Selected) + if (info.Selected) selectedNumber++; } SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets)); - ApproveButton.Disabled = selectedNumber == state.MaxSelectedSets ? false : true; + ApproveButton.Disabled = selectedNumber != state.MaxSelectedSets; } } diff --git a/Content.Client/Throwing/ThrownItemVisualizerSystem.cs b/Content.Client/Throwing/ThrownItemVisualizerSystem.cs index b25b4fbb7de..28a07ae94a7 100644 --- a/Content.Client/Throwing/ThrownItemVisualizerSystem.cs +++ b/Content.Client/Throwing/ThrownItemVisualizerSystem.cs @@ -59,7 +59,6 @@ private void OnShutdown(EntityUid uid, ThrownItemComponent component, ComponentS if (length <= TimeSpan.Zero) return null; - length += TimeSpan.FromSeconds(ThrowingSystem.FlyTime); var scale = ent.Comp2.Scale; var lenFloat = (float) length.TotalSeconds; diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 42791521668..2c5f69fbd03 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -130,7 +130,7 @@ public void OnStateEntered(GameplayState state) var boundKey = hotbarKeys[i]; builder = builder.Bind(boundKey, new PointerInputCmdHandler((in PointerInputCmdArgs args) => { - if (args.State != BoundKeyState.Up) + if (args.State != BoundKeyState.Down) return false; TriggerAction(boundId); @@ -202,10 +202,13 @@ private bool TargetingOnUse(in PointerInputCmdArgs args) switch (action) { case WorldTargetActionComponent mapTarget: - return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss; + return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss; case EntityTargetActionComponent entTarget: - return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss; + return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss; + + case EntityWorldTargetActionComponent entMapTarget: + return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.InteractOnMiss; default: Logger.Error($"Unknown targeting action: {actionId.GetType()}"); @@ -234,8 +237,6 @@ private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, Wor if (action.Event != null) { action.Event.Target = coords; - action.Event.Performer = user; - action.Event.Action = actionId; } _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime); @@ -269,8 +270,6 @@ private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, En if (action.Event != null) { action.Event.Target = entity; - action.Event.Performer = user; - action.Event.Action = actionId; } _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime); @@ -284,12 +283,49 @@ private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, En return true; } + private bool TryTargetEntityWorld(in PointerInputCmdArgs args, + EntityUid actionId, + EntityWorldTargetActionComponent action, + EntityUid user, + ActionsComponent actionComp) + { + if (_actionsSystem == null) + return false; + + var entity = args.EntityUid; + var coords = args.Coordinates; + + if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action))) + { + if (action.DeselectOnMiss) + StopTargeting(); + + return false; + } + + if (action.ClientExclusive) + { + if (action.Event != null) + { + action.Event.Entity = entity; + action.Event.Coords = coords; + } + + _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime); + } + else + EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords))); + + if (!action.Repeat) + StopTargeting(); + + return true; + } + public void UnloadButton() { if (ActionButton == null) - { return; - } ActionButton.OnPressed -= ActionButtonPressed; } @@ -297,9 +333,7 @@ public void UnloadButton() public void LoadButton() { if (ActionButton == null) - { return; - } ActionButton.OnPressed += ActionButtonPressed; } @@ -509,7 +543,7 @@ private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp existing.Add(button); } - int i = 0; + var i = 0; foreach (var action in actions) { if (i < existing.Count) @@ -752,17 +786,11 @@ private bool OnMenuBeginDrag() { if (EntityManager.TryGetComponent(action.EntityIcon, out SpriteComponent? sprite) && sprite.Icon?.GetFrame(RsiDirection.South, 0) is {} frame) - { _dragShadow.Texture = frame; - } else if (action.Icon != null) - { _dragShadow.Texture = _spriteSystem.Frame0(action.Icon); - } else - { _dragShadow.Texture = null; - } } LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - new Vector2(32, 32)); @@ -807,7 +835,7 @@ private void UnloadGui() private void LoadGui() { - DebugTools.Assert(_window == null); + UnloadGui(); _window = UIManager.CreateWindow<ActionsWindow>(); LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop); @@ -857,10 +885,8 @@ private void AssignSlots(List<SlotAssignment> assignments) _container?.SetActionData(_actionsSystem, _pages[_currentPageIndex]); } - public void RemoveActionContainer() - { + public void RemoveActionContainer() => _container = null; - } public void OnSystemLoaded(ActionsSystem system) { diff --git a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs index a7397aff38d..d36a91c3733 100644 --- a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs +++ b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs @@ -3,6 +3,7 @@ using Content.Client.Administration.UI; using Content.Client.Administration.UI.Tabs.ObjectsTab; using Content.Client.Administration.UI.Tabs.PanicBunkerTab; +using Content.Client.Administration.UI.Tabs.BabyJailTab; using Content.Client.Administration.UI.Tabs.PlayerTab; using Content.Client.Gameplay; using Content.Client.Lobby; @@ -37,11 +38,13 @@ public sealed class AdminUIController : UIController, private AdminMenuWindow? _window; private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton; private PanicBunkerStatus? _panicBunker; + private BabyJailStatus? _babyJail; public override void Initialize() { base.Initialize(); SubscribeNetworkEvent<PanicBunkerChangedEvent>(OnPanicBunkerUpdated); + SubscribeNetworkEvent<BabyJailChangedEvent>(OnBabyJailUpdated); } private void OnPanicBunkerUpdated(PanicBunkerChangedEvent msg, EntitySessionEventArgs args) @@ -56,6 +59,18 @@ private void OnPanicBunkerUpdated(PanicBunkerChangedEvent msg, EntitySessionEven } } + private void OnBabyJailUpdated(BabyJailChangedEvent msg, EntitySessionEventArgs args) + { + var showDialog = _babyJail == null && msg.Status.Enabled; + _babyJail = msg.Status; + _window?.BabyJailControl.UpdateStatus(msg.Status); + + if (showDialog) + { + UIManager.CreateWindow<BabyJailStatusWindow>().OpenCentered(); + } + } + public void OnStateEntered(GameplayState state) { EnsureWindow(); @@ -101,6 +116,13 @@ private void EnsureWindow() if (_panicBunker != null) _window.PanicBunkerControl.UpdateStatus(_panicBunker); + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + + if (_babyJail != null) + _window.BabyJailControl.UpdateStatus(_babyJail); + _window.PlayerTabControl.OnEntryKeyBindDown += PlayerTabEntryKeyBindDown; _window.ObjectsTabControl.OnEntryKeyBindDown += ObjectsTabEntryKeyBindDown; _window.OnOpen += OnWindowOpen; @@ -198,14 +220,17 @@ private void PlayerTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data args.Handle(); } - private void ObjectsTabEntryKeyBindDown(ObjectsTabEntry entry, GUIBoundKeyEventArgs args) + private void ObjectsTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data) { - var uid = entry.AssocEntity; + if (data is not ObjectsListData { Info: var info }) + return; + + var uid = info.Entity; var function = args.Function; if (function == EngineKeyFunctions.UIClick) _conHost.ExecuteCommand($"vv {uid}"); - else if (function == EngineKeyFunctions.UseSecondary) + else if (function == EngineKeyFunctions.UIRightClick) _verb.OpenVerbMenu(uid, true); else return; diff --git a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs index ee8cb28d2cc..4ae74a5d65e 100644 --- a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs +++ b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Atmos.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.UserInterface.Systems.Atmos.GasTank { @@ -14,7 +15,7 @@ public GasTankBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKe { } - public void SetOutputPressure(in float value) + public void SetOutputPressure(float value) { SendMessage(new GasTankSetPressureMessage { @@ -30,9 +31,10 @@ public void ToggleInternals() protected override void Open() { base.Open(); - _window = new GasTankWindow(this); - _window.OnClose += Close; - _window.OpenCentered(); + _window = this.CreateWindow<GasTankWindow>(); + _window.SetTitle(EntMan.GetComponent<MetaDataComponent>(Owner).EntityName); + _window.OnOutputPressure += SetOutputPressure; + _window.OnToggleInternals += ToggleInternals; } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs index 7797a096de3..fd5624ad8a7 100644 --- a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs +++ b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs @@ -10,201 +10,206 @@ using Robust.Client.UserInterface.CustomControls; using static Robust.Client.UserInterface.Controls.BoxContainer; -namespace Content.Client.UserInterface.Systems.Atmos.GasTank +namespace Content.Client.UserInterface.Systems.Atmos.GasTank; + +public sealed class GasTankWindow + : BaseWindow { - public sealed class GasTankWindow - : BaseWindow - { - private GasTankBoundUserInterface _owner; - private readonly Label _lblName; - private readonly BoxContainer _topContainer; - private readonly Control _contentContainer; + [Dependency] private readonly IResourceCache _cache = default!; + private readonly RichTextLabel _lblPressure; + private readonly FloatSpinBox _spbPressure; + private readonly RichTextLabel _lblInternals; + private readonly Button _btnInternals; + private readonly Label _topLabel; - private readonly IResourceCache _resourceCache = default!; - private readonly RichTextLabel _lblPressure; - private readonly FloatSpinBox _spbPressure; - private readonly RichTextLabel _lblInternals; - private readonly Button _btnInternals; + public event Action<float>? OnOutputPressure; + public event Action? OnToggleInternals; - public GasTankWindow(GasTankBoundUserInterface owner) - { - TextureButton btnClose; - _resourceCache = IoCManager.Resolve<IResourceCache>(); - _owner = owner; - var rootContainer = new LayoutContainer {Name = "GasTankRoot"}; - AddChild(rootContainer); + public GasTankWindow() + { + IoCManager.InjectDependencies(this); + Control contentContainer; + BoxContainer topContainer; + TextureButton btnClose; + var rootContainer = new LayoutContainer { Name = "GasTankRoot" }; + AddChild(rootContainer); - MouseFilter = MouseFilterMode.Stop; + MouseFilter = MouseFilterMode.Stop; - var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); - var back = new StyleBoxTexture - { - Texture = panelTex, - Modulate = Color.FromHex("#25252A"), - }; + var panelTex = _cache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); + var back = new StyleBoxTexture + { + Texture = panelTex, + Modulate = Color.FromHex("#25252A"), + }; - back.SetPatchMargin(StyleBox.Margin.All, 10); + back.SetPatchMargin(StyleBox.Margin.All, 10); - var topPanel = new PanelContainer - { - PanelOverride = back, - MouseFilter = MouseFilterMode.Pass - }; + var topPanel = new PanelContainer + { + PanelOverride = back, + MouseFilter = MouseFilterMode.Pass + }; - var bottomWrap = new LayoutContainer - { - Name = "BottomWrap" - }; + var bottomWrap = new LayoutContainer + { + Name = "BottomWrap" + }; - rootContainer.AddChild(topPanel); - rootContainer.AddChild(bottomWrap); + rootContainer.AddChild(topPanel); + rootContainer.AddChild(bottomWrap); - LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide); - LayoutContainer.SetMarginBottom(topPanel, -85); + LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide); + LayoutContainer.SetMarginBottom(topPanel, -85); - LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide); - LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both); + LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide); + LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both); - var topContainerWrap = new BoxContainer + var topContainerWrap = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Children = { - Orientation = LayoutOrientation.Vertical, - Children = + (topContainer = new BoxContainer { - (_topContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }), - new Control {MinSize = new Vector2(0, 110)} - } - }; + Orientation = LayoutOrientation.Vertical + }), + new Control {MinSize = new Vector2(0, 110)} + } + }; - rootContainer.AddChild(topContainerWrap); + rootContainer.AddChild(topContainerWrap); - LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide); + LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide); - var font = _resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); + var font = _cache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); - var topRow = new BoxContainer + _topLabel = new Label + { + FontOverride = font, + FontColorOverride = StyleNano.NanoGold, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + HorizontalAlignment = HAlignment.Left, + Margin = new Thickness(0, 0, 20, 0), + }; + + var topRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + Margin = new Thickness(4, 2, 12, 2), + Children = { - Orientation = LayoutOrientation.Horizontal, - Margin = new Thickness(4, 2, 12, 2), - Children = + _topLabel, + (btnClose = new TextureButton { - (_lblName = new Label - { - Text = Loc.GetString("gas-tank-window-label"), - FontOverride = font, - FontColorOverride = StyleNano.NanoGold, - VerticalAlignment = VAlignment.Center, - HorizontalExpand = true, - HorizontalAlignment = HAlignment.Left, - Margin = new Thickness(0, 0, 20, 0), - }), - (btnClose = new TextureButton - { - StyleClasses = {DefaultWindow.StyleClassWindowCloseButton}, - VerticalAlignment = VAlignment.Center - }) - } - }; - - var middle = new PanelContainer + StyleClasses = {DefaultWindow.StyleClassWindowCloseButton}, + VerticalAlignment = VAlignment.Center + }) + } + }; + + var middle = new PanelContainer + { + PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202025") }, + Children = { - PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#202025")}, - Children = + (contentContainer = new BoxContainer { - (_contentContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Margin = new Thickness(8, 4), - }) - } - }; - - _topContainer.AddChild(topRow); - _topContainer.AddChild(new PanelContainer - { - MinSize = new Vector2(0, 2), - PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")} - }); - _topContainer.AddChild(middle); - _topContainer.AddChild(new PanelContainer - { - MinSize = new Vector2(0, 2), - PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")} - }); - + Orientation = LayoutOrientation.Vertical, + Margin = new Thickness(8, 4), + }) + } + }; - _lblPressure = new RichTextLabel(); - _contentContainer.AddChild(_lblPressure); + topContainer.AddChild(topRow); + topContainer.AddChild(new PanelContainer + { + MinSize = new Vector2(0, 2), + PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") } + }); + topContainer.AddChild(middle); + topContainer.AddChild(new PanelContainer + { + MinSize = new Vector2(0, 2), + PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") } + }); - //internals - _lblInternals = new RichTextLabel - {MinSize = new Vector2(200, 0), VerticalAlignment = VAlignment.Center}; - _btnInternals = new Button {Text = Loc.GetString("gas-tank-window-internals-toggle-button") }; - _contentContainer.AddChild( - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Margin = new Thickness(0, 7, 0, 0), - Children = {_lblInternals, _btnInternals} - }); + _lblPressure = new RichTextLabel(); + contentContainer.AddChild(_lblPressure); - // Separator - _contentContainer.AddChild(new Control - { - MinSize = new Vector2(0, 10) - }); + //internals + _lblInternals = new RichTextLabel + { MinSize = new Vector2(200, 0), VerticalAlignment = VAlignment.Center }; + _btnInternals = new Button { Text = Loc.GetString("gas-tank-window-internals-toggle-button") }; - _contentContainer.AddChild(new Label + contentContainer.AddChild( + new BoxContainer { - Text = Loc.GetString("gas-tank-window-output-pressure-label"), - Align = Label.AlignMode.Center + Orientation = LayoutOrientation.Horizontal, + Margin = new Thickness(0, 7, 0, 0), + Children = { _lblInternals, _btnInternals } }); - _spbPressure = new FloatSpinBox - { - IsValid = f => f >= 0 || f <= 3000, - Margin = new Thickness(25, 0, 25, 7) - }; - _contentContainer.AddChild(_spbPressure); - // Handlers - _spbPressure.OnValueChanged += args => - { - _owner.SetOutputPressure(args.Value); - }; - - _btnInternals.OnPressed += args => - { - _owner.ToggleInternals(); - }; + // Separator + contentContainer.AddChild(new Control + { + MinSize = new Vector2(0, 10) + }); - btnClose.OnPressed += _ => Close(); - } + contentContainer.AddChild(new Label + { + Text = Loc.GetString("gas-tank-window-output-pressure-label"), + Align = Label.AlignMode.Center + }); + _spbPressure = new FloatSpinBox + { + IsValid = f => f >= 0 || f <= 3000, + Margin = new Thickness(25, 0, 25, 7) + }; + contentContainer.AddChild(_spbPressure); - public void UpdateState(GasTankBoundUserInterfaceState state) + // Handlers + _spbPressure.OnValueChanged += args => { - _lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}"))); - _btnInternals.Disabled = !state.CanConnectInternals; - _lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text", - ("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected")))); - if (state.OutputPressure.HasValue) - { - _spbPressure.Value = state.OutputPressure.Value; - } - } + OnOutputPressure?.Invoke(args.Value); + }; - protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + _btnInternals.OnPressed += args => { - return DragMode.Move; - } + OnToggleInternals?.Invoke(); + }; + + btnClose.OnPressed += _ => Close(); + } - protected override bool HasPoint(Vector2 point) + public void SetTitle(string name) + { + _topLabel.Text = name; + } + + public void UpdateState(GasTankBoundUserInterfaceState state) + { + _lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}"))); + _btnInternals.Disabled = !state.CanConnectInternals; + _lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text", + ("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected")))); + if (state.OutputPressure.HasValue) { - return false; + _spbPressure.Value = state.OutputPressure.Value; } } + + protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + { + return DragMode.Move; + } + + protected override bool HasPoint(Vector2 point) + { + return false; + } } diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index b1d8b01050c..dd8aeee9f54 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -9,6 +9,8 @@ using Content.Client.Examine; using Content.Client.Gameplay; using Content.Client.Ghost; +using Content.Client.Mind; +using Content.Client.Roles; using Content.Client.Stylesheets; using Content.Client.UserInterface.Screens; using Content.Client.UserInterface.Systems.Chat.Widgets; @@ -21,6 +23,7 @@ using Content.Shared.Examine; using Content.Shared.Input; using Content.Shared.Radio; +using Content.Shared.Roles.RoleCodeword; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; @@ -61,6 +64,9 @@ public sealed class ChatUIController : UIController [UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default; [UISystemDependency] private readonly ChatSystem? _chatSys = default; [UISystemDependency] private readonly PsionicChatUpdateSystem? _psionic = default!; //Nyano - Summary: makes the psionic chat available. + [UISystemDependency] private readonly TransformSystem? _transform = default; + [UISystemDependency] private readonly MindSystem? _mindSystem = default!; + [UISystemDependency] private readonly RoleCodewordSystem? _roleCodewordSystem = default!; [ValidatePrototypeId<ColorPalettePrototype>] private const string ChatNamePalette = "ChatNames"; @@ -638,7 +644,7 @@ private void UpdateQueuedSpeechBubbles(FrameEventArgs delta) var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data) => uid == data.compOwner || uid == data.attachedEntity; var playerPos = player != null - ? EntityManager.GetComponent<TransformComponent>(player.Value).MapPosition + ? _transform?.GetMapCoordinates(player.Value) ?? MapCoordinates.Nullspace : MapCoordinates.Nullspace; var occluded = player != null && _examine.IsOccluded(player.Value); @@ -657,7 +663,7 @@ private void UpdateQueuedSpeechBubbles(FrameEventArgs delta) continue; } - var otherPos = EntityManager.GetComponent<TransformComponent>(ent).MapPosition; + var otherPos = _transform?.GetMapCoordinates(ent) ?? MapCoordinates.Nullspace; if (occluded && !_examine.InRangeUnOccluded( playerPos, @@ -832,6 +838,19 @@ public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true) msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name"))); } + // Color any codewords for minds that have roles that use them + if (_player.LocalUser != null && _mindSystem != null && _roleCodewordSystem != null) + { + if (_mindSystem.TryGetMind(_player.LocalUser.Value, out var mindId) && _ent.TryGetComponent(mindId, out RoleCodewordComponent? codewordComp)) + { + foreach (var (_, codewordData) in codewordComp.RoleCodewords) + { + foreach (string codeword in codewordData.Codewords) + msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, codeword, "color", codewordData.Color.ToHex()); + } + } + } + // Log all incoming chat to repopulate when filter is un-toggled if (!msg.HideChat) { diff --git a/Content.Client/UserInterface/Systems/Info/InfoUIController.cs b/Content.Client/UserInterface/Systems/Info/InfoUIController.cs index 6d398ec6aa5..6edecebb16c 100644 --- a/Content.Client/UserInterface/Systems/Info/InfoUIController.cs +++ b/Content.Client/UserInterface/Systems/Info/InfoUIController.cs @@ -1,13 +1,9 @@ -using System.Globalization; using Content.Client.Gameplay; using Content.Client.Guidebook; using Content.Client.Info; -using Content.Shared.Administration.Managers; using Content.Shared.CCVar; using Content.Shared.Info; -using Robust.Client; using Robust.Client.Console; -using Robust.Client.Player; using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controls; using Robust.Shared.Configuration; @@ -18,24 +14,20 @@ namespace Content.Client.UserInterface.Systems.Info; public sealed class InfoUIController : UIController, IOnStateExited<GameplayState> { - [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly INetManager _netManager = default!; - [Dependency] private readonly ISharedAdminManager _adminManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; private RulesPopup? _rulesPopup; private RulesAndInfoWindow? _infoWindow; - private static DateTime NextRulesReadTime => DateTime.UtcNow + TimeSpan.FromDays(60); - public override void Initialize() { base.Initialize(); - _client.PlayerJoinedServer += OnJoinedServer; + + _netManager.RegisterNetMessage<RulesAcceptedMessage>(); _netManager.RegisterNetMessage<ShowRulesPopupMessage>(OnShowRulesPopupMessage); _consoleHost.RegisterCommand("fuckrules", @@ -47,22 +39,6 @@ public override void Initialize() }); } - private void OnJoinedServer(object? sender, PlayerEventArgs args) - { - if (_playerManager.LocalSession is not { } localSession) - return; - - if (_adminManager.IsAdmin(localSession) && _cfg.GetCVar(CCVars.RulesExemptLocal)) - return; - - var nextReadTarget = DateTime.Parse(_cfg.GetCVar(CCVars.RulesNextPopupTime)); - if (nextReadTarget >= DateTime.UtcNow) - return; - - var time = _cfg.GetCVar(CCVars.RulesWaitTime); - ShowRules(time); - } - private void OnShowRulesPopupMessage(ShowRulesPopupMessage message) { ShowRules(message.PopupTime); @@ -100,8 +76,7 @@ private void OnQuitPressed() private void OnAcceptPressed() { - _cfg.SetCVar(CCVars.RulesNextPopupTime, NextRulesReadTime.ToString(CultureInfo.InvariantCulture)); - _cfg.SaveToFile(); + _netManager.ClientSendMessage(new RulesAcceptedMessage()); _rulesPopup?.Orphan(); _rulesPopup = null; diff --git a/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs b/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs index 752c89ca970..58c8a1451bd 100644 --- a/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs +++ b/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs @@ -7,14 +7,17 @@ using Content.Client.UserInterface.Systems.DecalPlacer; using Content.Client.UserInterface.Systems.Sandbox.Windows; using Content.Shared.Input; +using Content.Shared.Silicons.StationAi; using JetBrains.Annotations; using Robust.Client.Console; using Robust.Client.Debugging; using Robust.Client.Graphics; using Robust.Client.Input; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controllers.Implementations; +using Robust.Shared.Console; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Player; @@ -27,10 +30,12 @@ namespace Content.Client.UserInterface.Systems.Sandbox; [UsedImplicitly] public sealed class SandboxUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<SandboxSystem> { + [Dependency] private readonly IConsoleHost _console = default!; [Dependency] private readonly IEyeManager _eye = default!; [Dependency] private readonly IInputManager _input = default!; [Dependency] private readonly ILightManager _light = default!; [Dependency] private readonly IClientAdminManager _admin = default!; + [Dependency] private readonly IPlayerManager _player = default!; [UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!; [UISystemDependency] private readonly MarkerSystem _marker = default!; @@ -116,6 +121,21 @@ private void EnsureWindow() _window.ShowMarkersButton.Pressed = _marker.MarkersVisible; _window.ShowBbButton.Pressed = (_debugPhysics.Flags & PhysicsDebugFlags.Shapes) != 0x0; + _window.AiOverlayButton.OnPressed += args => + { + var player = _player.LocalEntity; + + if (player == null) + return; + + var pnent = EntityManager.GetNetEntity(player.Value); + + // Need NetworkedAddComponent but engine PR. + if (args.Button.Pressed) + _console.ExecuteCommand($"addcomp {pnent.Id} StationAiOverlay"); + else + _console.ExecuteCommand($"rmcomp {pnent.Id} StationAiOverlay"); + }; _window.RespawnButton.OnPressed += _ => _sandbox.Respawn(); _window.SpawnTilesButton.OnPressed += _ => TileSpawningController.ToggleWindow(); _window.SpawnEntitiesButton.OnPressed += _ => EntitySpawningController.ToggleWindow(); diff --git a/Content.Client/UserInterface/Systems/Sandbox/Windows/SandboxWindow.xaml b/Content.Client/UserInterface/Systems/Sandbox/Windows/SandboxWindow.xaml index 64367ea27af..05e65cf29c3 100644 --- a/Content.Client/UserInterface/Systems/Sandbox/Windows/SandboxWindow.xaml +++ b/Content.Client/UserInterface/Systems/Sandbox/Windows/SandboxWindow.xaml @@ -4,6 +4,7 @@ Title="{Loc sandbox-window-title}" Resizable="False"> <BoxContainer Orientation="Vertical" SeparationOverride="4"> + <Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/> <Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/> <Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/> <Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/> diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs index 17ddba77ffc..eafab84ed63 100644 --- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs +++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Shared.VendingMachines; using Robust.Client.UserInterface.Controls; using System.Linq; +using Robust.Client.UserInterface; namespace Content.Client.VendingMachines { @@ -28,15 +29,14 @@ protected override void Open() _cachedInventory = vendingMachineSys.GetAllInventory(Owner); - _menu = new VendingMachineMenu { Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName }; + _menu = this.CreateWindow<VendingMachineMenu>(); + _menu.OpenCenteredLeft(); + _menu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; - _menu.OnClose += Close; _menu.OnItemSelected += OnItemSelected; _menu.OnSearchChanged += OnSearchChanged; _menu.Populate(_cachedInventory, out _cachedFilteredIndex); - - _menu.OpenCenteredLeft(); } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index 49a3785eb28..5f1f49e5fd0 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -22,6 +22,7 @@ public sealed class VerbSystem : SharedVerbSystem [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly ExamineSystem _examine = default!; [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly EntityLookupSystem _entityLookup = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; @@ -122,7 +123,6 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true if ((visibility & MenuVisibility.Invisible) == 0) { var spriteQuery = GetEntityQuery<SpriteComponent>(); - var tagQuery = GetEntityQuery<TagComponent>(); for (var i = entities.Count - 1; i >= 0; i--) { @@ -130,7 +130,7 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true if (!spriteQuery.TryGetComponent(entity, out var spriteComponent) || !spriteComponent.Visible || - _tagSystem.HasTag(entity, "HideContextMenu", tagQuery)) + _tagSystem.HasTag(entity, "HideContextMenu")) { entities.RemoveSwap(i); } @@ -141,7 +141,7 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true if ((visibility & MenuVisibility.NoFov) == 0) { var xformQuery = GetEntityQuery<TransformComponent>(); - var playerPos = xformQuery.GetComponent(player.Value).MapPosition; + var playerPos = _transform.GetMapCoordinates(player.Value, xform: xformQuery.GetComponent(player.Value)); for (var i = entities.Count - 1; i >= 0; i--) { @@ -149,7 +149,7 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true if (!_examine.InRangeUnOccluded( playerPos, - xformQuery.GetComponent(entity).MapPosition, + _transform.GetMapCoordinates(entity, xform: xformQuery.GetComponent(entity)), ExamineSystemShared.ExamineRange, null)) { diff --git a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs index f700c6663b9..e76ca1cf8f7 100644 --- a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs +++ b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs @@ -1,12 +1,13 @@ using Content.Shared.VoiceMask; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.VoiceMask; public sealed class VoiceMaskBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IPrototypeManager _protomanager = default!; [ViewVariables] private VoiceMaskNameChangeWindow? _window; @@ -19,12 +20,12 @@ protected override void Open() { base.Open(); - _window = new(_proto); + _window = this.CreateWindow<VoiceMaskNameChangeWindow>(); + _window.ReloadVerbs(_protomanager); + _window.AddVerbs(); - _window.OpenCentered(); _window.OnNameChange += OnNameSelected; _window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb)); - _window.OnClose += Close; } private void OnNameSelected(string name) diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs index 16a28f9d9b3..7ca4dd4b957 100644 --- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs +++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs @@ -17,7 +17,7 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow private string? _verb; - public VoiceMaskNameChangeWindow(IPrototypeManager proto) + public VoiceMaskNameChangeWindow() { RobustXamlLoader.Load(this); @@ -31,13 +31,9 @@ public VoiceMaskNameChangeWindow(IPrototypeManager proto) OnVerbChange?.Invoke((string?) args.Button.GetItemMetadata(args.Id)); SpeechVerbSelector.SelectId(args.Id); }; - - ReloadVerbs(proto); - - AddVerbs(); } - private void ReloadVerbs(IPrototypeManager proto) + public void ReloadVerbs(IPrototypeManager proto) { foreach (var verb in proto.EnumeratePrototypes<SpeechVerbPrototype>()) { @@ -46,7 +42,7 @@ private void ReloadVerbs(IPrototypeManager proto) _verbs.Sort((a, b) => a.Item1.CompareTo(b.Item1)); } - private void AddVerbs() + public void AddVerbs() { SpeechVerbSelector.Clear(); diff --git a/Content.Client/Weapons/Melee/MeleeArcOverlay.cs b/Content.Client/Weapons/Melee/MeleeArcOverlay.cs index dbd68c15e24..e7b6c8b0d60 100644 --- a/Content.Client/Weapons/Melee/MeleeArcOverlay.cs +++ b/Content.Client/Weapons/Melee/MeleeArcOverlay.cs @@ -20,10 +20,11 @@ public sealed class MeleeArcOverlay : Overlay private readonly IPlayerManager _playerManager; private readonly MeleeWeaponSystem _melee; private readonly SharedCombatModeSystem _combatMode; + private readonly SharedTransformSystem _transform = default!; public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; - public MeleeArcOverlay(IEntityManager entManager, IEyeManager eyeManager, IInputManager inputManager, IPlayerManager playerManager, MeleeWeaponSystem melee, SharedCombatModeSystem combatMode) + public MeleeArcOverlay(IEntityManager entManager, IEyeManager eyeManager, IInputManager inputManager, IPlayerManager playerManager, MeleeWeaponSystem melee, SharedCombatModeSystem combatMode, SharedTransformSystem transform) { _entManager = entManager; _eyeManager = eyeManager; @@ -31,6 +32,7 @@ public MeleeArcOverlay(IEntityManager entManager, IEyeManager eyeManager, IInput _playerManager = playerManager; _melee = melee; _combatMode = combatMode; + _transform = transform; } protected override void Draw(in OverlayDrawArgs args) @@ -52,7 +54,7 @@ protected override void Draw(in OverlayDrawArgs args) if (mapPos.MapId != args.MapId) return; - var playerPos = xform.MapPosition; + var playerPos = _transform.GetMapCoordinates(player.Value, xform: xform); if (mapPos.MapId != playerPos.MapId) return; diff --git a/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs b/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs index 6b259a7fd54..eda469deaf0 100644 --- a/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs +++ b/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs @@ -35,6 +35,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) collection.Resolve<IInputManager>(), collection.Resolve<IPlayerManager>(), sysManager.GetEntitySystem<MeleeWeaponSystem>(), - sysManager.GetEntitySystem<SharedCombatModeSystem>())); + sysManager.GetEntitySystem<SharedCombatModeSystem>(), + sysManager.GetEntitySystem<SharedTransformSystem>())); } } diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index 9f2ee5eb8f6..d86172de718 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -146,7 +146,7 @@ public override void Update(float frameTime) if (!weapon.DisableClick && (!weapon.SwapKeys ? useDown : altDown)) { - var attackerPos = Transform(entity).MapPosition; + var attackerPos = TransformSystem.GetMapCoordinates(entity); if (mousePos.MapId != attackerPos.MapId || (attackerPos.Position - mousePos.Position).Length() > weapon.Range) diff --git a/Content.Client/Weapons/Melee/UI/MeleeSpeechBoundUserInterface.cs b/Content.Client/Weapons/Melee/UI/MeleeSpeechBoundUserInterface.cs index f3e0c0a539a..3f01808c422 100644 --- a/Content.Client/Weapons/Melee/UI/MeleeSpeechBoundUserInterface.cs +++ b/Content.Client/Weapons/Melee/UI/MeleeSpeechBoundUserInterface.cs @@ -1,5 +1,6 @@ using Robust.Client.GameObjects; using Content.Shared.Speech.Components; +using Robust.Client.UserInterface; namespace Content.Client.Weapons.Melee.UI; @@ -19,17 +20,10 @@ protected override void Open() { base.Open(); - _window = new MeleeSpeechWindow(); - if (State != null) - UpdateState(State); - - _window.OpenCentered(); - - _window.OnClose += Close; + _window = this.CreateWindow<MeleeSpeechWindow>(); _window.OnBattlecryEntered += OnBattlecryChanged; } - private void OnBattlecryChanged(string newBattlecry) { SendMessage(new MeleeSpeechBattlecryChangedMessage(newBattlecry)); diff --git a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs index 62df764ae50..63d21c84635 100644 --- a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs +++ b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs @@ -18,8 +18,9 @@ public sealed class GunSpreadOverlay : Overlay private readonly IInputManager _input; private readonly IPlayerManager _player; private readonly GunSystem _guns; + private readonly SharedTransformSystem _transform; - public GunSpreadOverlay(IEntityManager entManager, IEyeManager eyeManager, IGameTiming timing, IInputManager input, IPlayerManager player, GunSystem system) + public GunSpreadOverlay(IEntityManager entManager, IEyeManager eyeManager, IGameTiming timing, IInputManager input, IPlayerManager player, GunSystem system, SharedTransformSystem transform) { _entManager = entManager; _eye = eyeManager; @@ -27,6 +28,7 @@ public GunSpreadOverlay(IEntityManager entManager, IEyeManager eyeManager, IGame _timing = timing; _player = player; _guns = system; + _transform = transform; } protected override void Draw(in OverlayDrawArgs args) @@ -41,7 +43,7 @@ protected override void Draw(in OverlayDrawArgs args) return; } - var mapPos = xform.MapPosition; + var mapPos = _transform.GetMapCoordinates(player.Value, xform: xform); if (mapPos.MapId == MapId.Nullspace) return; diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index 4a7711032e4..0ad22bf1d87 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -60,7 +60,8 @@ public bool SpreadOverlay Timing, _inputManager, _player, - this)); + this, + TransformSystem)); } else { @@ -76,6 +77,7 @@ public override void Initialize() base.Initialize(); UpdatesOutsidePrediction = true; SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect); + SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo); SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash); // Plays animated effects on the client. @@ -85,9 +87,16 @@ public override void Initialize() InitializeSpentAmmo(); } + private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args) + { + UpdateAmmoCount(uid, ammoComp); + } + private void OnMuzzleFlash(MuzzleFlashEvent args) { - CreateEffect(GetEntity(args.Uid), args); + var gunUid = GetEntity(args.Uid); + + CreateEffect(gunUid, args, gunUid); } private void OnHitscan(HitscanEvent ev) @@ -154,7 +163,7 @@ public override void Update(float frameTime) var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary; - if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down) + if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated) { if (gun.ShotCounter != 0) EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) }); @@ -175,7 +184,7 @@ public override void Update(float frameTime) } // Define target coordinates relative to gun entity, so that network latency on moving grids doesn't fuck up the target location. - var coordinates = EntityCoordinates.FromMap(entity, mousePos, TransformSystem, EntityManager); + var coordinates = TransformSystem.ToCoordinates(entity, mousePos); NetEntity? target = null; if (_state.CurrentState is GameplayStateBase screen) @@ -199,7 +208,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? // Rather than splitting client / server for every ammo provider it's easier // to just delete the spawned entities. This is for programmer sanity despite the wasted perf. // This also means any ammo specific stuff can be grabbed as necessary. - var direction = fromCoordinates.ToMapPos(EntityManager, TransformSystem) - toCoordinates.ToMapPos(EntityManager, TransformSystem); + var direction = TransformSystem.ToMapCoordinates(fromCoordinates).Position - TransformSystem.ToMapCoordinates(toCoordinates).Position; var worldAngle = direction.ToAngle().Opposite(); foreach (var (ent, shootable) in ammo) @@ -270,11 +279,19 @@ protected override void Popup(string message, EntityUid? uid, EntityUid? user) PopupSystem.PopupEntity(message, uid.Value, user.Value); } - protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null) + protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? tracked = null) { if (!Timing.IsFirstTimePredicted) return; + // EntityUid check added to stop throwing exceptions due to https://github.com/space-wizards/space-station-14/issues/28252 + // TODO: Check to see why invalid entities are firing effects. + if (gunUid == EntityUid.Invalid) + { + Log.Debug($"Invalid Entity sent MuzzleFlashEvent (proto: {message.Prototype}, gun: {ToPrettyString(gunUid)})"); + return; + } + var gunXform = Transform(gunUid); var gridUid = gunXform.GridUid; EntityCoordinates coordinates; @@ -295,10 +312,10 @@ protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, var ent = Spawn(message.Prototype, coordinates); TransformSystem.SetWorldRotationNoLerp(ent, message.Angle); - if (user != null) + if (tracked != null) { var track = EnsureComp<TrackUserComponent>(ent); - track.User = user; + track.User = tracked; track.Offset = Vector2.UnitX / 2f; } @@ -374,6 +391,6 @@ protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, var uidPlayer = EnsureComp<AnimationPlayerComponent>(gunUid); _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light"); - _animPlayer.Play((gunUid, uidPlayer), animTwo,"muzzle-flash-light"); + _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light"); } } diff --git a/Content.Client/WhiteDream/BloodCult/BloodCultistSystem.cs b/Content.Client/WhiteDream/BloodCult/BloodCultistSystem.cs index b732df9801c..c300dfb24d2 100644 --- a/Content.Client/WhiteDream/BloodCult/BloodCultistSystem.cs +++ b/Content.Client/WhiteDream/BloodCult/BloodCultistSystem.cs @@ -1,12 +1,14 @@ using System.Numerics; using Content.Shared.Antag; using Content.Shared.Ghost; +using Content.Shared.StatusIcon; using Content.Shared.StatusIcon.Components; using Content.Shared.WhiteDream.BloodCult; using Content.Shared.WhiteDream.BloodCult.BloodCultist; using Content.Shared.WhiteDream.BloodCult.Components; using Content.Shared.WhiteDream.BloodCult.Constructs; using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -15,15 +17,16 @@ namespace Content.Client.WhiteDream.BloodCult; public sealed class BloodCultistSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; public override void Initialize() { SubscribeLocalEvent<PentagramComponent, ComponentStartup>(OnPentagramAdded); SubscribeLocalEvent<PentagramComponent, ComponentShutdown>(OnPentagramRemoved); - SubscribeLocalEvent<ConstructComponent, CanDisplayStatusIconsEvent>(OnCanShowCultIcon); - SubscribeLocalEvent<BloodCultistComponent, CanDisplayStatusIconsEvent>(OnCanShowCultIcon); - SubscribeLocalEvent<BloodCultLeaderComponent, CanDisplayStatusIconsEvent>(OnCanShowCultIcon); + SubscribeLocalEvent<ConstructComponent, GetStatusIconsEvent>(OnCanShowConstructIcon); + SubscribeLocalEvent<BloodCultistComponent, GetStatusIconsEvent>(OnCanShowCultMemberIcon); + SubscribeLocalEvent<BloodCultLeaderComponent, GetStatusIconsEvent>(OnCanShowCultLeaderIcon); } private void OnPentagramAdded(EntityUid uid, PentagramComponent component, ComponentStartup args) @@ -38,7 +41,7 @@ private void OnPentagramAdded(EntityUid uid, PentagramComponent component, Compo var layer = sprite.AddLayer(new SpriteSpecifier.Rsi(component.RsiPath, randomState)); sprite.LayerMapSet(PentagramKey.Key, layer); - sprite.LayerSetOffset(layer, new Vector2(0.0f, adj)); + sprite.LayerSetOffset(layer, new(0.0f, adj)); } private void OnPentagramRemoved(EntityUid uid, PentagramComponent component, ComponentShutdown args) @@ -50,13 +53,39 @@ private void OnPentagramRemoved(EntityUid uid, PentagramComponent component, Com } /// <summary> - /// Determine whether a client should display the cult icon. + /// Determine whether a client should display the construct icon. /// </summary> - private void OnCanShowCultIcon<T>(EntityUid uid, T comp, ref CanDisplayStatusIconsEvent args) - where T : IAntagStatusIconComponent + private void OnCanShowConstructIcon(Entity<ConstructComponent> ent, ref GetStatusIconsEvent args) { - if (!CanDisplayIcon(args.User, comp.IconVisibleToGhost)) - args.Cancelled = true; + if (CanDisplayIcon(ent.Owner, ent.Comp.IconVisibleToGhost)) + { + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); + } + } + + /// <summary> + /// Determine whether a client should display the cult member icon. + /// </summary> + private void OnCanShowCultMemberIcon(Entity<BloodCultistComponent> ent, ref GetStatusIconsEvent args) + { + if (CanDisplayIcon(ent.Owner, ent.Comp.IconVisibleToGhost)) + { + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); + } + } + + /// <summary> + /// Determine whether a client should display the cult leader icon. + /// </summary> + private void OnCanShowCultLeaderIcon(Entity<BloodCultLeaderComponent> ent, ref GetStatusIconsEvent args) + { + if (CanDisplayIcon(ent.Owner, ent.Comp.IconVisibleToGhost)) + { + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); + } } /// <summary> diff --git a/Content.Client/Wires/UI/WiresBoundUserInterface.cs b/Content.Client/Wires/UI/WiresBoundUserInterface.cs index 5a8869a204e..edf1a2d3770 100644 --- a/Content.Client/Wires/UI/WiresBoundUserInterface.cs +++ b/Content.Client/Wires/UI/WiresBoundUserInterface.cs @@ -1,5 +1,6 @@ using Content.Shared.Wires; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Wires.UI { @@ -15,10 +16,8 @@ public WiresBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) protected override void Open() { base.Open(); - - _menu = new WiresMenu(this); - _menu.OnClose += Close; - _menu.OpenCenteredLeft(); + _menu = this.CreateWindow<WiresMenu>(); + _menu.OnAction += PerformAction; } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Wires/UI/WiresMenu.cs b/Content.Client/Wires/UI/WiresMenu.cs index 7bccc208616..eccc548297c 100644 --- a/Content.Client/Wires/UI/WiresMenu.cs +++ b/Content.Client/Wires/UI/WiresMenu.cs @@ -1,4 +1,3 @@ -using System; using System.Numerics; using Content.Client.Examine; using Content.Client.Resources; @@ -12,10 +11,6 @@ using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Animations; using Robust.Shared.Input; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Maths; -using Robust.Shared.Random; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Wires.UI @@ -24,8 +19,6 @@ public sealed class WiresMenu : BaseWindow { [Dependency] private readonly IResourceCache _resourceCache = default!; - public WiresBoundUserInterface Owner { get; } - private readonly Control _wiresHBox; private readonly Control _topContainer; private readonly Control _statusContainer; @@ -35,11 +28,12 @@ public sealed class WiresMenu : BaseWindow public TextureButton CloseButton { get; set; } - public WiresMenu(WiresBoundUserInterface owner) + public event Action<int, WiresAction>? OnAction; + + public WiresMenu() { IoCManager.InjectDependencies(this); - Owner = owner; var rootContainer = new LayoutContainer {Name = "WireRoot"}; AddChild(rootContainer); @@ -257,12 +251,12 @@ public void Populate(WiresBoundUserInterfaceState state) control.WireClicked += () => { - Owner.PerformAction(wire.Id, wire.IsCut ? WiresAction.Mend : WiresAction.Cut); + OnAction?.Invoke(wire.Id, wire.IsCut ? WiresAction.Mend : WiresAction.Cut); }; control.ContactsClicked += () => { - Owner.PerformAction(wire.Id, WiresAction.Pulse); + OnAction?.Invoke(wire.Id, WiresAction.Pulse); }; } diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs index 2538caf6eb8..c7a74815b6b 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.Xenoarchaeology.Equipment; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; namespace Content.Client.Xenoarchaeology.Ui; @@ -18,10 +19,7 @@ protected override void Open() { base.Open(); - _consoleMenu = new AnalysisConsoleMenu(); - - _consoleMenu.OnClose += Close; - _consoleMenu.OpenCentered(); + _consoleMenu = this.CreateWindow<AnalysisConsoleMenu>(); _consoleMenu.OnServerSelectionButtonPressed += () => { diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs index 2890bb3dbf7..2723db1efbf 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs @@ -1,6 +1,7 @@ using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Xenoarchaeology.Equipment; +using Microsoft.VisualBasic; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.UserInterface.Controls; diff --git a/Content.Client/Zombies/ZombieSystem.cs b/Content.Client/Zombies/ZombieSystem.cs index 49b5d6aec18..d250e418504 100644 --- a/Content.Client/Zombies/ZombieSystem.cs +++ b/Content.Client/Zombies/ZombieSystem.cs @@ -1,59 +1,53 @@ using System.Linq; using Content.Shared.Ghost; using Content.Shared.Humanoid; +using Content.Shared.StatusIcon; using Content.Shared.StatusIcon.Components; using Content.Shared.Zombies; using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; namespace Content.Client.Zombies; -public sealed class ZombieSystem : EntitySystem +public sealed class ZombieSystem : SharedZombieSystem { + [Dependency] private readonly IPrototypeManager _prototype = default!; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent<ZombieComponent, ComponentStartup>(OnStartup); - SubscribeLocalEvent<ZombieComponent, CanDisplayStatusIconsEvent>(OnCanDisplayStatusIcons); - SubscribeLocalEvent<InitialInfectedComponent, CanDisplayStatusIconsEvent>(OnCanDisplayStatusIcons); + SubscribeLocalEvent<ZombieComponent, GetStatusIconsEvent>(GetZombieIcon); + SubscribeLocalEvent<InitialInfectedComponent, GetStatusIconsEvent>(GetInitialInfectedIcon); } - private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args) + private void GetZombieIcon(Entity<ZombieComponent> ent, ref GetStatusIconsEvent args) { - if (HasComp<HumanoidAppearanceComponent>(uid)) - return; - - if (!TryComp<SpriteComponent>(uid, out var sprite)) - return; - - for (var i = 0; i < sprite.AllLayers.Count(); i++) - { - sprite.LayerSetColor(i, component.SkinColor); - } + var iconPrototype = _prototype.Index(ent.Comp.StatusIcon); + args.StatusIcons.Add(iconPrototype); } - /// <summary> - /// Determines whether a player should be able to see the StatusIcon for zombies. - /// </summary> - private void OnCanDisplayStatusIcons(EntityUid uid, ZombieComponent component, ref CanDisplayStatusIconsEvent args) + private void GetInitialInfectedIcon(Entity<InitialInfectedComponent> ent, ref GetStatusIconsEvent args) { - if (HasComp<ZombieComponent>(args.User) || HasComp<InitialInfectedComponent>(args.User) || HasComp<ShowZombieIconsComponent>(args.User)) - return; - - if (component.IconVisibleToGhost && HasComp<GhostComponent>(args.User)) + if (HasComp<ZombieComponent>(ent)) return; - args.Cancelled = true; + var iconPrototype = _prototype.Index(ent.Comp.StatusIcon); + args.StatusIcons.Add(iconPrototype); } - private void OnCanDisplayStatusIcons(EntityUid uid, InitialInfectedComponent component, ref CanDisplayStatusIconsEvent args) + private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args) { - if (HasComp<InitialInfectedComponent>(args.User) && !HasComp<ZombieComponent>(args.User)) + if (HasComp<HumanoidAppearanceComponent>(uid)) return; - if (component.IconVisibleToGhost && HasComp<GhostComponent>(args.User)) + if (!TryComp<SpriteComponent>(uid, out var sprite)) return; - args.Cancelled = true; + for (var i = 0; i < sprite.AllLayers.Count(); i++) + { + sprite.LayerSetColor(i, component.SkinColor); + } } } diff --git a/Content.Client/_NF/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs b/Content.Client/_NF/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs new file mode 100644 index 00000000000..715c67f888f --- /dev/null +++ b/Content.Client/_NF/Shuttles/BUI/ShuttleConsoleBoundUserInterface.cs @@ -0,0 +1,26 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. +using Content.Client.Shuttles.UI; +using Content.Shared._NF.Shuttles.Events; + +namespace Content.Client.Shuttles.BUI +{ + public sealed partial class ShuttleConsoleBoundUserInterface + { + private void NfOpen() + { + _window ??= new ShuttleConsoleWindow(); + _window.OnInertiaDampeningModeChanged += OnInertiaDampeningModeChanged; + } + private void OnInertiaDampeningModeChanged(NetEntity? entityUid, InertiaDampeningMode mode) + { + SendMessage(new SetInertiaDampeningRequest + { + ShuttleEntityUid = entityUid, + Mode = mode, + }); + } + + } +} diff --git a/Content.Client/_NF/Shuttles/UI/NavScreen.xaml.cs b/Content.Client/_NF/Shuttles/UI/NavScreen.xaml.cs new file mode 100644 index 00000000000..2b2493490cc --- /dev/null +++ b/Content.Client/_NF/Shuttles/UI/NavScreen.xaml.cs @@ -0,0 +1,48 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. +using Content.Shared._NF.Shuttles.Events; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.Shuttles.UI; + +public sealed partial class NavScreen +{ + private readonly ButtonGroup _buttonGroup = new(); + public event Action<NetEntity?, InertiaDampeningMode>? OnInertiaDampeningModeChanged; + + private void NfInitialize() + { + DampenerOff.OnPressed += _ => SetDampenerMode(InertiaDampeningMode.Off); + DampenerOn.OnPressed += _ => SetDampenerMode(InertiaDampeningMode.Dampen); + AnchorOn.OnPressed += _ => SetDampenerMode(InertiaDampeningMode.Anchor); + + DampenerOff.Group = _buttonGroup; + DampenerOn.Group = _buttonGroup; + AnchorOn.Group = _buttonGroup; + + // Send off a request to get the current dampening mode. + _entManager.TryGetNetEntity(_shuttleEntity, out var shuttle); + OnInertiaDampeningModeChanged?.Invoke(shuttle, InertiaDampeningMode.Query); + } + + private void SetDampenerMode(InertiaDampeningMode mode) + { + NavRadar.DampeningMode = mode; + _entManager.TryGetNetEntity(_shuttleEntity, out var shuttle); + OnInertiaDampeningModeChanged?.Invoke(shuttle, mode); + } + + private void NfUpdateState() + { + if (NavRadar.DampeningMode == InertiaDampeningMode.Station) + DampenerModeButtons.Visible = false; + else + { + DampenerModeButtons.Visible = true; + DampenerOff.Pressed = NavRadar.DampeningMode == InertiaDampeningMode.Off; + DampenerOn.Pressed = NavRadar.DampeningMode == InertiaDampeningMode.Dampen; + AnchorOn.Pressed = NavRadar.DampeningMode == InertiaDampeningMode.Anchor; + } + } +} diff --git a/Content.Client/_NF/Shuttles/UI/ShuttleConsoleWindow.xaml.cs b/Content.Client/_NF/Shuttles/UI/ShuttleConsoleWindow.xaml.cs new file mode 100644 index 00000000000..7ba3f952b46 --- /dev/null +++ b/Content.Client/_NF/Shuttles/UI/ShuttleConsoleWindow.xaml.cs @@ -0,0 +1,21 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. +using Content.Shared._NF.Shuttles.Events; + +namespace Content.Client.Shuttles.UI +{ + public sealed partial class ShuttleConsoleWindow + { + public event Action<NetEntity?, InertiaDampeningMode>? OnInertiaDampeningModeChanged; + + private void NfInitialize() + { + NavContainer.OnInertiaDampeningModeChanged += (entity, mode) => + { + OnInertiaDampeningModeChanged?.Invoke(entity, mode); + }; + } + + } +} diff --git a/Content.Client/_NF/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/_NF/Shuttles/UI/ShuttleNavControl.xaml.cs new file mode 100644 index 00000000000..dabb1712ffc --- /dev/null +++ b/Content.Client/_NF/Shuttles/UI/ShuttleNavControl.xaml.cs @@ -0,0 +1,30 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. +using Content.Shared._NF.Shuttles.Events; +using Content.Shared.Shuttles.BUIStates; +using Robust.Shared.Physics.Components; +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Shared.Collections; + +namespace Content.Client.Shuttles.UI; + +public sealed partial class ShuttleNavControl +{ + public InertiaDampeningMode DampeningMode { get; set; } + + private void NfUpdateState(NavInterfaceState state) + { + + if (!EntManager.GetCoordinates(state.Coordinates).HasValue || + !EntManager.TryGetComponent( + EntManager.GetCoordinates(state.Coordinates).GetValueOrDefault().EntityId, + out TransformComponent? transform) || + !EntManager.TryGetComponent(transform.GridUid, out PhysicsComponent? _)) + return; + + DampeningMode = state.DampeningMode; + } +} + diff --git a/Content.IntegrationTests/AssemblyInfo.cs b/Content.IntegrationTests/AssemblyInfo.cs new file mode 100644 index 00000000000..76fc42f3a91 --- /dev/null +++ b/Content.IntegrationTests/AssemblyInfo.cs @@ -0,0 +1,8 @@ +[assembly: Parallelizable(ParallelScope.Children)] + +// I don't know why this parallelism limit was originally put here. +// I *do* know that I tried removing it, and ran into the following .NET runtime problem: +// https://github.com/dotnet/runtime/issues/107197 +// So we can't really parallelize integration tests harder either until the runtime fixes that, +// *or* we fix serv3 to not spam expression trees. +[assembly: LevelOfParallelism(3)] diff --git a/Content.IntegrationTests/Pair/TestPair.Cvars.cs b/Content.IntegrationTests/Pair/TestPair.Cvars.cs new file mode 100644 index 00000000000..81df31fc9a3 --- /dev/null +++ b/Content.IntegrationTests/Pair/TestPair.Cvars.cs @@ -0,0 +1,69 @@ +#nullable enable +using System.Collections.Generic; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Pair; + +public sealed partial class TestPair +{ + private readonly Dictionary<string, object> _modifiedClientCvars = new(); + private readonly Dictionary<string, object> _modifiedServerCvars = new(); + + private void OnServerCvarChanged(CVarChangeInfo args) + { + _modifiedServerCvars.TryAdd(args.Name, args.OldValue); + } + + private void OnClientCvarChanged(CVarChangeInfo args) + { + _modifiedClientCvars.TryAdd(args.Name, args.OldValue); + } + + internal void ClearModifiedCvars() + { + _modifiedClientCvars.Clear(); + _modifiedServerCvars.Clear(); + } + + /// <summary> + /// Reverts any cvars that were modified during a test back to their original values. + /// </summary> + public async Task RevertModifiedCvars() + { + await Server.WaitPost(() => + { + foreach (var (name, value) in _modifiedServerCvars) + { + if (Server.CfgMan.GetCVar(name).Equals(value)) + continue; + Server.Log.Info($"Resetting cvar {name} to {value}"); + Server.CfgMan.SetCVar(name, value); + } + + // I just love order dependent cvars + if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik)) + Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik); + + }); + + await Client.WaitPost(() => + { + foreach (var (name, value) in _modifiedClientCvars) + { + if (Client.CfgMan.GetCVar(name).Equals(value)) + continue; + + var flags = Client.CfgMan.GetCVarFlags(name); + if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER)) + continue; + + Client.Log.Info($"Resetting cvar {name} to {value}"); + Client.CfgMan.SetCVar(name, value); + } + }); + + ClearModifiedCvars(); + } +} diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs index c0f4b3b745f..b67b106de76 100644 --- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs +++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs @@ -2,10 +2,12 @@ using System.IO; using System.Linq; using Content.Server.GameTicking; +using Content.Server.Preferences.Managers; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Mind; using Content.Shared.Mind.Components; +using Content.Shared.Preferences; using Robust.Client; using Robust.Server.Player; using Robust.Shared.Exceptions; @@ -34,12 +36,18 @@ private async Task OnDirtyDispose() private async Task OnCleanDispose() { + await Server.WaitIdleAsync(); + await Client.WaitIdleAsync(); + await Server.RemoveAllDummySessions(); + if (TestMap != null) { await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid)); TestMap = null; } + await RevertModifiedCvars(); + var usageTime = Watch.Elapsed; Watch.Restart(); await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms"); @@ -132,6 +140,7 @@ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut) if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby) { await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round."); + Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false); Assert.That(gameTicker.DummyTicker, Is.False); Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true); await Server.WaitPost(() => gameTicker.RestartRound()); diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs index 916a94c9c4c..42add948063 100644 --- a/Content.IntegrationTests/Pair/TestPair.cs +++ b/Content.IntegrationTests/Pair/TestPair.cs @@ -4,10 +4,12 @@ using System.Linq; using Content.Server.GameTicking; using Content.Shared.Players; +using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Player; +using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.UnitTesting; @@ -25,6 +27,13 @@ public sealed partial class TestPair public readonly List<string> TestHistory = new(); public PoolSettings Settings = default!; public TestMapData? TestMap; + + private int _nextServerSeed; + private int _nextClientSeed; + + public int ServerSeed; + public int ClientSeed; + public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!; public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!; @@ -58,6 +67,9 @@ public async Task Initialize(PoolSettings settings, TextWriter testOut, List<str (Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut); ActivateContext(testOut); + Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged; + Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged; + if (!settings.NoLoadTestPrototypes) await LoadPrototypes(testPrototypes!); @@ -67,22 +79,27 @@ public async Task Initialize(PoolSettings settings, TextWriter testOut, List<str await Server.WaitPost(() => gameTicker.RestartRound()); } - if (settings.ShouldBeConnected) + // Always initially connect clients to generate an initial random set of preferences/profiles. + // This is to try and prevent issues where if the first test that connects the client is consistently some test + // that uses a fixed seed, it would effectively prevent it from beingrandomized. + + Client.SetConnectTarget(Server); + await Client.WaitIdleAsync(); + var netMgr = Client.ResolveDependency<IClientNetManager>(); + await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!)); + await ReallyBeIdle(10); + await Client.WaitRunTicks(1); + + if (!settings.ShouldBeConnected) { - Client.SetConnectTarget(Server); - await Client.WaitIdleAsync(); - var netMgr = Client.ResolveDependency<IClientNetManager>(); - - await Client.WaitPost(() => - { - if (!netMgr.IsConnected) - { - netMgr.ClientConnect(null!, 0, null!); - } - }); + await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect")); await ReallyBeIdle(10); - await Client.WaitRunTicks(1); } + + var cRand = Client.ResolveDependency<IRobustRandom>(); + var sRand = Server.ResolveDependency<IRobustRandom>(); + _nextClientSeed = cRand.Next(); + _nextServerSeed = sRand.Next(); } public void Kill() @@ -122,4 +139,33 @@ public enum PairState : byte CleanDisposed = 2, Dead = 3, } + + public void SetupSeed() + { + var sRand = Server.ResolveDependency<IRobustRandom>(); + if (Settings.ServerSeed is { } severSeed) + { + ServerSeed = severSeed; + sRand.SetSeed(ServerSeed); + } + else + { + ServerSeed = _nextServerSeed; + sRand.SetSeed(ServerSeed); + _nextServerSeed = sRand.Next(); + } + + var cRand = Client.ResolveDependency<IRobustRandom>(); + if (Settings.ClientSeed is { } clientSeed) + { + ClientSeed = clientSeed; + cRand.SetSeed(ClientSeed); + } + else + { + ClientSeed = _nextClientSeed; + cRand.SetSeed(ClientSeed); + _nextClientSeed = cRand.Next(); + } + } } diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs index 3f489de6497..636a0f93a26 100644 --- a/Content.IntegrationTests/PoolManager.cs +++ b/Content.IntegrationTests/PoolManager.cs @@ -23,8 +23,6 @@ using Robust.Shared.Timing; using Robust.UnitTesting; -[assembly: LevelOfParallelism(3)] - namespace Content.IntegrationTests; /// <summary> @@ -255,7 +253,7 @@ await testOut.WriteLineAsync( } finally { - if (pair != null && pair.TestHistory.Count > 1) + if (pair != null && pair.TestHistory.Count > 0) { await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start"); for (var i = 0; i < pair.TestHistory.Count; i++) @@ -271,10 +269,14 @@ await testOut.WriteLineAsync( var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed; await testOut.WriteLineAsync( $"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms"); - await testOut.WriteLineAsync( - $"{nameof(GetServerClientPair)}: Returning pair {pair.Id}"); + + pair.ClearModifiedCvars(); pair.Settings = poolSettings; pair.TestHistory.Add(currentTestName); + pair.SetupSeed(); + await testOut.WriteLineAsync( + $"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}"); + pair.Watch.Restart(); return pair; } diff --git a/Content.IntegrationTests/PoolManagerTestEventHandler.cs b/Content.IntegrationTests/PoolManagerTestEventHandler.cs index d37dffff50a..cd35c7c7bdd 100644 --- a/Content.IntegrationTests/PoolManagerTestEventHandler.cs +++ b/Content.IntegrationTests/PoolManagerTestEventHandler.cs @@ -1,7 +1,4 @@ - -[assembly: Parallelizable(ParallelScope.Children)] - -namespace Content.IntegrationTests; +namespace Content.IntegrationTests; [SetUpFixture] public sealed class PoolManagerTestEventHandler diff --git a/Content.IntegrationTests/PoolSettings.cs b/Content.IntegrationTests/PoolSettings.cs index a78173808f8..9da514e66b8 100644 --- a/Content.IntegrationTests/PoolSettings.cs +++ b/Content.IntegrationTests/PoolSettings.cs @@ -1,5 +1,7 @@ #nullable enable +using Robust.Shared.Random; + namespace Content.IntegrationTests; /// <summary> @@ -9,16 +11,6 @@ namespace Content.IntegrationTests; /// </summary> public sealed class PoolSettings { - /// <summary> - /// If the returned pair must not be reused - /// </summary> - public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes; - - /// <summary> - /// If the given pair must be brand new - /// </summary> - public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes; - /// <summary> /// Set to true if the test will ruin the server/client pair. /// </summary> @@ -34,8 +26,6 @@ public sealed class PoolSettings /// </summary> public bool DummyTicker { get; init; } = true; - public bool UseDummyTicker => !InLobby && DummyTicker; - /// <summary> /// If true, this enables the creation of admin logs during the test. /// </summary> @@ -48,8 +38,6 @@ public sealed class PoolSettings /// </summary> public bool Connected { get; init; } - public bool ShouldBeConnected => InLobby || Connected; - /// <summary> /// Set to true if the given server/client pair should be in the lobby. /// If the pair is not in the lobby at the end of the test, this test must be marked as dirty. @@ -92,6 +80,34 @@ public sealed class PoolSettings /// </summary> public string? TestName { get; set; } + /// <summary> + /// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/> + /// </summary> + public int? ServerSeed { get; set; } + + /// <summary> + /// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/> + /// </summary> + public int? ClientSeed { get; set; } + + #region Inferred Properties + + /// <summary> + /// If the returned pair must not be reused + /// </summary> + public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes; + + /// <summary> + /// If the given pair must be brand new + /// </summary> + public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes; + + public bool UseDummyTicker => !InLobby && DummyTicker; + + public bool ShouldBeConnected => InLobby || Connected; + + #endregion + /// <summary> /// Tries to guess if we can skip recycling the server/client pair. /// </summary> @@ -114,4 +130,4 @@ public bool CanFastRecycle(PoolSettings nextSettings) && Map == nextSettings.Map && InLobby == nextSettings.InLobby; } -} \ No newline at end of file +} diff --git a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs index 45addff00bf..b07c352c81d 100644 --- a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs +++ b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs @@ -32,14 +32,16 @@ public async Task TestActionDetach() // PVS-detach action entities // We do this by just giving them the ghost layer var visSys = server.System<VisibilitySystem>(); - server.Post(() => + + await server.WaitPost(() => { var enumerator = server.Transform(ent).ChildEnumerator; while (enumerator.MoveNext(out var child)) { - visSys.AddLayer(child, (int) VisibilityFlags.Ghost); + 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 @@ -47,7 +49,7 @@ public async Task TestActionDetach() Assert.That(cSys.GetActions(cEnt).Count(), Is.EqualTo(initActions)); // Re-enter PVS view - server.Post(() => + await server.WaitPost(() => { var enumerator = server.Transform(ent).ChildEnumerator; while (enumerator.MoveNext(out var child)) diff --git a/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs b/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs new file mode 100644 index 00000000000..3a1ec7fd40e --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs @@ -0,0 +1,53 @@ +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Piping.Components; +using Content.Server.Atmos.Piping.EntitySystems; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Atmos; + +[TestFixture] +public sealed class GridJoinTest +{ + private const string CanisterProtoId = "AirCanister"; + + [Test] + public async Task TestGridJoinAtmosphere() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + + var entMan = server.EntMan; + var protoMan = server.ProtoMan; + var atmosSystem = entMan.System<AtmosphereSystem>(); + var atmosDeviceSystem = entMan.System<AtmosDeviceSystem>(); + var transformSystem = entMan.System<SharedTransformSystem>(); + + var testMap = await pair.CreateTestMap(); + + await server.WaitPost(() => + { + // Spawn an atmos device on the grid + var canister = entMan.Spawn(CanisterProtoId); + transformSystem.SetCoordinates(canister, testMap.GridCoords); + var deviceComp = entMan.GetComponent<AtmosDeviceComponent>(canister); + var canisterEnt = (canister, deviceComp); + + // Make sure the canister is tracked as an off-grid device + Assert.That(atmosDeviceSystem.IsJoinedOffGrid(canisterEnt)); + + // Add an atmosphere to the grid + entMan.AddComponent<GridAtmosphereComponent>(testMap.Grid); + + // Force AtmosDeviceSystem to update off-grid devices + // This means the canister is now considered on-grid, + // but it's still tracked as off-grid! + Assert.DoesNotThrow(() => atmosDeviceSystem.Update(atmosSystem.AtmosTime)); + + // Make sure that the canister is now properly tracked as on-grid + Assert.That(atmosDeviceSystem.IsJoinedOffGrid(canisterEnt), Is.False); + }); + + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs index 99160df3c66..e2b82ff966f 100644 --- a/Content.IntegrationTests/Tests/CargoTest.cs +++ b/Content.IntegrationTests/Tests/CargoTest.cs @@ -3,8 +3,13 @@ using System.Numerics; using Content.Server.Cargo.Components; using Content.Server.Cargo.Systems; +using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.Cargo.Prototypes; +using Content.Shared.IdentityManagement; using Content.Shared.Stacks; +using Content.Shared.Tag; +using Content.Shared.Whitelist; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -120,7 +125,16 @@ await server.WaitAssertion(() => foreach (var proto in protoIds) { - var ent = entManager.SpawnEntity(proto, coord); + EntityUid? ent = null; + try + { + ent = entManager.SpawnEntity(proto, coord); + } + catch (Exception e) + { + Assert.Fail($"Prototype {proto} failed to spawn! Is your configuration invalid?"); + return; + } if (entManager.TryGetComponent<StackPriceComponent>(ent, out var stackpricecomp) && stackpricecomp.Price > 0) @@ -149,6 +163,80 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + /// <summary> + /// Tests to see if any items that are valid for cargo bounties can be sliced into items that + /// are also valid for the same bounty entry. + /// </summary> + [Test] + public async Task NoSliceableBountyArbitrageTest() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + + var testMap = await pair.CreateTestMap(); + + var entManager = server.ResolveDependency<IEntityManager>(); + var mapManager = server.ResolveDependency<IMapManager>(); + var protoManager = server.ResolveDependency<IPrototypeManager>(); + var componentFactory = server.ResolveDependency<IComponentFactory>(); + var whitelist = entManager.System<EntityWhitelistSystem>(); + var cargo = entManager.System<CargoSystem>(); + var sliceableSys = entManager.System<SliceableFoodSystem>(); + + var bounties = protoManager.EnumeratePrototypes<CargoBountyPrototype>().ToList(); + + await server.WaitAssertion(() => + { + var mapId = testMap.MapId; + var grid = mapManager.CreateGridEntity(mapId); + var coord = new EntityCoordinates(grid.Owner, 0, 0); + + var sliceableEntityProtos = protoManager.EnumeratePrototypes<EntityPrototype>() + .Where(p => !p.Abstract) + .Where(p => !pair.IsTestPrototype(p)) + .Where(p => p.TryGetComponent<SliceableFoodComponent>(out _, componentFactory)) + .Select(p => p.ID) + .ToList(); + + foreach (var proto in sliceableEntityProtos) + { + var ent = entManager.SpawnEntity(proto, coord); + var sliceable = entManager.GetComponent<SliceableFoodComponent>(ent); + + // Check each bounty + foreach (var bounty in bounties) + { + // Check each entry in the bounty + foreach (var entry in bounty.Entries) + { + // See if the entity counts as part of this bounty entry + if (!cargo.IsValidBountyEntry(ent, entry)) + continue; + + // Spawn a slice + var slice = entManager.SpawnEntity(sliceable.Slice, coord); + + // See if the slice also counts for this bounty entry + if (!cargo.IsValidBountyEntry(slice, entry)) + { + entManager.DeleteEntity(slice); + continue; + } + + entManager.DeleteEntity(slice); + + // If for some reason it can only make one slice, that's okay, I guess + Assert.That(sliceable.TotalCount, Is.EqualTo(1), $"{proto} counts as part of cargo bounty {bounty.ID} and slices into {sliceable.TotalCount} slices which count for the same bounty!"); + } + } + + entManager.DeleteEntity(ent); + } + mapManager.DeleteMap(mapId); + }); + + await pair.CleanReturnAsync(); + } [TestPrototypes] private const string StackProto = @" diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs index 4db9eabf5c6..9e57cd4b0e6 100644 --- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs +++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs @@ -32,9 +32,9 @@ public async Task PardonTest() // No bans on record Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null); + Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty); + Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); }); // Try to pardon a ban that does not exist @@ -43,9 +43,9 @@ public async Task PardonTest() // Still no bans on record Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null); + Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty); + Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); }); var banReason = "test"; @@ -57,9 +57,9 @@ public async Task PardonTest() // Should have one ban on record now Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null); + Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); await pair.RunTicksSync(5); @@ -70,13 +70,13 @@ public async Task PardonTest() await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2")); // The existing ban is unaffected - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null); + Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); var ban = await sDatabase.GetServerBanAsync(1); Assert.Multiple(async () => { Assert.That(ban, Is.Not.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); // Check that it matches Assert.That(ban.Id, Is.EqualTo(1)); @@ -95,7 +95,7 @@ public async Task PardonTest() await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1")); // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null); + Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban var pardonedBan = await sDatabase.GetServerBanAsync(1); @@ -105,7 +105,7 @@ public async Task PardonTest() Assert.That(pardonedBan, Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); Assert.That(pardonedBan.Id, Is.EqualTo(1)); Assert.That(pardonedBan.UserId, Is.EqualTo(clientId)); @@ -133,13 +133,13 @@ public async Task PardonTest() Assert.Multiple(async () => { // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null); + Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); // Reconnect client. Slightly faster than dirtying the pair. diff --git a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs new file mode 100644 index 00000000000..6b47636d0e2 --- /dev/null +++ b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs @@ -0,0 +1,371 @@ +using System.Linq; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Execution; +using Content.Shared.FixedPoint; +using Content.Shared.Ghost; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Mind; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Tag; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Commands; + +[TestFixture] +public sealed class SuicideCommandTests +{ + + [TestPrototypes] + private const string Prototypes = @" +- type: entity + id: SharpTestObject + name: very sharp test object + components: + - type: Item + - type: MeleeWeapon + damage: + types: + Slash: 5 + - type: Execution + +- type: entity + id: MixedDamageTestObject + name: mixed damage test object + components: + - type: Item + - type: MeleeWeapon + damage: + types: + Slash: 5 + Blunt: 5 + - type: Execution + +- type: entity + id: TestMaterialReclaimer + name: test version of the material reclaimer + components: + - type: MaterialReclaimer"; + + /// <summary> + /// Run the suicide command in the console + /// Should successfully kill the player and ghost them + /// </summary> + [Test] + public async Task TestSuicide() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Connected = true, + Dirty = true, + DummyTicker = false + }); + var server = pair.Server; + var consoleHost = server.ResolveDependency<IConsoleHost>(); + var entManager = server.ResolveDependency<IEntityManager>(); + var playerMan = server.ResolveDependency<IPlayerManager>(); + var mindSystem = entManager.System<SharedMindSystem>(); + var mobStateSystem = entManager.System<MobStateSystem>(); + + // We need to know the player and whether they can be hurt, killed, and whether they have a mind + var player = playerMan.Sessions.First().AttachedEntity!.Value; + var mind = mindSystem.GetMind(player); + + MindComponent mindComponent = default; + MobStateComponent mobStateComp = default; + await server.WaitPost(() => + { + if (mind != null) + mindComponent = entManager.GetComponent<MindComponent>(mind.Value); + + mobStateComp = entManager.GetComponent<MobStateComponent>(player); + }); + + + // Check that running the suicide command kills the player + // and properly ghosts them without them being able to return to their body + await server.WaitAssertion(() => + { + consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); + Assert.Multiple(() => + { + Assert.That(mobStateSystem.IsDead(player, mobStateComp)); + Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) && + !ghostComp.CanReturnToBody); + }); + }); + + await pair.CleanReturnAsync(); + } + + /// <summary> + /// Run the suicide command while the player is already injured + /// This should only deal as much damage as necessary to get to the dead threshold + /// </summary> + [Test] + public async Task TestSuicideWhileDamaged() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Connected = true, + Dirty = true, + DummyTicker = false + }); + var server = pair.Server; + var consoleHost = server.ResolveDependency<IConsoleHost>(); + var entManager = server.ResolveDependency<IEntityManager>(); + var playerMan = server.ResolveDependency<IPlayerManager>(); + var protoMan = server.ResolveDependency<IPrototypeManager>(); + + var damageableSystem = entManager.System<DamageableSystem>(); + var mindSystem = entManager.System<SharedMindSystem>(); + var mobStateSystem = entManager.System<MobStateSystem>(); + + // We need to know the player and whether they can be hurt, killed, and whether they have a mind + var player = playerMan.Sessions.First().AttachedEntity!.Value; + var mind = mindSystem.GetMind(player); + + MindComponent mindComponent = default; + MobStateComponent mobStateComp = default; + MobThresholdsComponent mobThresholdsComp = default; + DamageableComponent damageableComp = default; + await server.WaitPost(() => + { + if (mind != null) + mindComponent = entManager.GetComponent<MindComponent>(mind.Value); + + mobStateComp = entManager.GetComponent<MobStateComponent>(player); + mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player); + damageableComp = entManager.GetComponent<DamageableComponent>(player); + + if (protoMan.TryIndex<DamageTypePrototype>("Slash", out var slashProto)) + damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5))); + }); + + // Check that running the suicide command kills the player + // and properly ghosts them without them being able to return to their body + // and that all the damage is concentrated in the Slash category + await server.WaitAssertion(() => + { + consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); + var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last(); + + Assert.Multiple(() => + { + Assert.That(mobStateSystem.IsDead(player, mobStateComp)); + Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) && + !ghostComp.CanReturnToBody); + Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(lethalDamageThreshold)); + }); + }); + + await pair.CleanReturnAsync(); + } + + /// <summary> + /// Run the suicide command in the console + /// Should only ghost the player but not kill them + /// </summary> + [Test] + public async Task TestSuicideWhenCannotSuicide() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Connected = true, + Dirty = true, + DummyTicker = false + }); + var server = pair.Server; + var consoleHost = server.ResolveDependency<IConsoleHost>(); + var entManager = server.ResolveDependency<IEntityManager>(); + var playerMan = server.ResolveDependency<IPlayerManager>(); + var mindSystem = entManager.System<SharedMindSystem>(); + var mobStateSystem = entManager.System<MobStateSystem>(); + var tagSystem = entManager.System<TagSystem>(); + + // We need to know the player and whether they can be hurt, killed, and whether they have a mind + var player = playerMan.Sessions.First().AttachedEntity!.Value; + var mind = mindSystem.GetMind(player); + MindComponent mindComponent = default; + MobStateComponent mobStateComp = default; + await server.WaitPost(() => + { + if (mind != null) + mindComponent = entManager.GetComponent<MindComponent>(mind.Value); + mobStateComp = entManager.GetComponent<MobStateComponent>(player); + }); + + tagSystem.AddTag(player, "CannotSuicide"); + + // Check that running the suicide command kills the player + // and properly ghosts them without them being able to return to their body + await server.WaitAssertion(() => + { + consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); + Assert.Multiple(() => + { + Assert.That(mobStateSystem.IsAlive(player, mobStateComp)); + Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) && + !ghostComp.CanReturnToBody); + }); + }); + + await pair.CleanReturnAsync(); + } + + + /// <summary> + /// Run the suicide command while the player is holding an execution-capable weapon + /// </summary> + [Test] + public async Task TestSuicideByHeldItem() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Connected = true, + Dirty = true, + DummyTicker = false + }); + var server = pair.Server; + var consoleHost = server.ResolveDependency<IConsoleHost>(); + var entManager = server.ResolveDependency<IEntityManager>(); + var playerMan = server.ResolveDependency<IPlayerManager>(); + + var handsSystem = entManager.System<SharedHandsSystem>(); + var mindSystem = entManager.System<SharedMindSystem>(); + var mobStateSystem = entManager.System<MobStateSystem>(); + var transformSystem = entManager.System<TransformSystem>(); + var damageableSystem = entManager.System<DamageableSystem>(); + + // We need to know the player and whether they can be hurt, killed, and whether they have a mind + var player = playerMan.Sessions.First().AttachedEntity!.Value; + var mind = mindSystem.GetMind(player); + + MindComponent mindComponent = default; + MobStateComponent mobStateComp = default; + MobThresholdsComponent mobThresholdsComp = default; + DamageableComponent damageableComp = default; + HandsComponent handsComponent = default; + await server.WaitPost(() => + { + if (mind != null) + mindComponent = entManager.GetComponent<MindComponent>(mind.Value); + + mobStateComp = entManager.GetComponent<MobStateComponent>(player); + mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player); + damageableComp = entManager.GetComponent<DamageableComponent>(player); + handsComponent = entManager.GetComponent<HandsComponent>(player); + }); + + // Spawn the weapon of choice and put it in the player's hands + await server.WaitPost(() => + { + var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player)); + Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!)); + entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent); + Assert.That(executionComponent, Is.Not.EqualTo(null)); + }); + + // Check that running the suicide command kills the player + // and properly ghosts them without them being able to return to their body + // and that all the damage is concentrated in the Slash category + await server.WaitAssertion(() => + { + // Heal all damage first (possible low pressure damage taken) + damageableSystem.SetAllDamage(player, damageableComp, 0); + consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); + var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last(); + + Assert.Multiple(() => + { + Assert.That(mobStateSystem.IsDead(player, mobStateComp)); + Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) && + !ghostComp.CanReturnToBody); + Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold)); + }); + }); + + await pair.CleanReturnAsync(); + } + + /// <summary> + /// Run the suicide command while the player is holding an execution-capable weapon + /// with damage spread between slash and blunt + /// </summary> + [Test] + public async Task TestSuicideByHeldItemSpreadDamage() + { + await using var pair = await PoolManager.GetServerClient(new() + { + Connected = true, + Dirty = true, + DummyTicker = false + }); + var server = pair.Server; + var consoleHost = server.ResolveDependency<IConsoleHost>(); + var entManager = server.ResolveDependency<IEntityManager>(); + var playerMan = server.ResolveDependency<IPlayerManager>(); + + var handsSystem = entManager.System<SharedHandsSystem>(); + var mindSystem = entManager.System<SharedMindSystem>(); + var mobStateSystem = entManager.System<MobStateSystem>(); + var transformSystem = entManager.System<TransformSystem>(); + var damageableSystem = entManager.System<DamageableSystem>(); + + // We need to know the player and whether they can be hurt, killed, and whether they have a mind + var player = playerMan.Sessions.First().AttachedEntity!.Value; + var mind = mindSystem.GetMind(player); + + MindComponent mindComponent = default; + MobStateComponent mobStateComp = default; + MobThresholdsComponent mobThresholdsComp = default; + DamageableComponent damageableComp = default; + HandsComponent handsComponent = default; + await server.WaitPost(() => + { + if (mind != null) + mindComponent = entManager.GetComponent<MindComponent>(mind.Value); + + mobStateComp = entManager.GetComponent<MobStateComponent>(player); + mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player); + damageableComp = entManager.GetComponent<DamageableComponent>(player); + handsComponent = entManager.GetComponent<HandsComponent>(player); + }); + + // Spawn the weapon of choice and put it in the player's hands + await server.WaitPost(() => + { + var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player)); + Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!)); + entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent); + Assert.That(executionComponent, Is.Not.EqualTo(null)); + }); + + // Check that running the suicide command kills the player + // and properly ghosts them without them being able to return to their body + // and that slash damage is split in half + await server.WaitAssertion(() => + { + // Heal all damage first (possible low pressure damage taken) + damageableSystem.SetAllDamage(player, damageableComp, 0); + consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); + var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last(); + + Assert.Multiple(() => + { + Assert.That(mobStateSystem.IsDead(player, mobStateComp)); + Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) && + !ghostComp.CanReturnToBody); + Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2)); + }); + }); + + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs index d3a10dcfaf4..9779ded87c8 100644 --- a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs @@ -36,9 +36,22 @@ await server.WaitAssertion(() => if (!proto.Components.ContainsKey("Construction")) continue; - var ent = entMan.SpawnEntity(proto.ID, new MapCoordinates(Vector2.Zero, map.MapId)); - var construction = entMan.GetComponent<ConstructionComponent>(ent); - + EntityUid? ent = null; + + try + { + ent = entMan.SpawnEntity(proto.ID, new MapCoordinates(Vector2.Zero, map.MapId)); + } + catch (Exception e) + { + Assert.Fail(proto.ID); + return; + } + + if (ent == null) + return; + + var construction = entMan.GetComponent<ConstructionComponent>(ent.Value); var graph = protoMan.Index<ConstructionGraphPrototype>(construction.Graph); entMan.DeleteEntity(ent); diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs index fb77bf18d83..e47c73611a4 100644 --- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs +++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs @@ -171,7 +171,7 @@ await server.WaitPost(() => // Sloth: Okay I'm sorry but I hate having to rewrite tests for every refactor // If you see this yell at me in discord so I can continue to pretend this didn't happen. // REMINDER THAT I STILL HAVE TO FIX THIS TEST EVERY OTHER PHYSICS PR - // Assert.That(AirlockPhysicsDummy.Transform.MapPosition.X, Is.GreaterThan(AirlockPhysicsDummyStartingX)); + // _transform.GetMapCoordinates(UID HERE, xform: Assert.That(AirlockPhysicsDummy.Transform).X, Is.GreaterThan(AirlockPhysicsDummyStartingX)); // Blocked by the airlock await server.WaitAssertion(() => diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index 56645660673..0a52129b403 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -268,6 +268,11 @@ await server.WaitPost(() => if (protoId == "MobHumanSpaceNinja") continue; + // TODO fix tests properly upstream + // Fails due to audio components made when making anouncements + if (protoId == "StandardNanotrasenStation") + continue; + var count = server.EntMan.EntityCount; var clientCount = client.EntMan.EntityCount; EntityUid uid = default; diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index 822f7967cfd..71f3a0988a8 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -53,7 +53,6 @@ public async Task TryStopNukeOpsFromConstantlyFailing() var invSys = server.System<InventorySystem>(); var factionSys = server.System<NpcFactionSystem>(); - Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False); server.CfgMan.SetCVar(CCVars.GridFill, true); // Initially in the lobby diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs index 74641126aee..6aa2763888d 100644 --- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs +++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs @@ -25,6 +25,9 @@ public sealed class WeightlessStatusTests id: WeightlessGravityGeneratorDummy components: - type: GravityGenerator + - type: PowerCharge + windowTitle: gravity-generator-window-title + idlePower: 50 chargeRate: 1000000000 # Set this really high so it discharges in a single tick. activePower: 500 - type: ApcPowerReceiver diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs index 64f7a6d0820..b32d6c2b8d8 100644 --- a/Content.IntegrationTests/Tests/GravityGridTest.cs +++ b/Content.IntegrationTests/Tests/GravityGridTest.cs @@ -21,6 +21,9 @@ public sealed class GravityGridTest id: GridGravityGeneratorDummy components: - type: GravityGenerator + - type: PowerCharge + windowTitle: gravity-generator-window-title + idlePower: 50 chargeRate: 1000000000 # Set this really high so it discharges in a single tick. activePower: 500 - type: ApcPowerReceiver diff --git a/Content.IntegrationTests/Tests/Hands/HandTests.cs b/Content.IntegrationTests/Tests/Hands/HandTests.cs index fdcd7f9096f..9ecabbeebf6 100644 --- a/Content.IntegrationTests/Tests/Hands/HandTests.cs +++ b/Content.IntegrationTests/Tests/Hands/HandTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; +using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -24,6 +25,7 @@ public async Task TestPickupDrop() var playerMan = server.ResolveDependency<IPlayerManager>(); var mapMan = server.ResolveDependency<IMapManager>(); var sys = entMan.System<SharedHandsSystem>(); + var tSys = entMan.System<TransformSystem>(); var data = await pair.CreateTestMap(); await pair.RunTicksSync(5); @@ -35,7 +37,7 @@ await server.WaitPost(() => { player = playerMan.Sessions.First().AttachedEntity!.Value; var xform = entMan.GetComponent<TransformComponent>(player); - item = entMan.SpawnEntity("Crowbar", xform.MapPosition); + item = entMan.SpawnEntity("Crowbar", tSys.GetMapCoordinates(player, xform: xform)); hands = entMan.GetComponent<HandsComponent>(player); sys.TryPickup(player, item, hands.ActiveHand!); }); diff --git a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs index e5ac0f785aa..801433ae72b 100644 --- a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs +++ b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Shared.Interaction; +using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -32,6 +33,7 @@ public async Task EntityEntityTest() var sEntities = server.ResolveDependency<IEntityManager>(); var mapManager = server.ResolveDependency<IMapManager>(); var conSystem = sEntities.EntitySysManager.GetEntitySystem<SharedContainerSystem>(); + var tSystem = sEntities.EntitySysManager.GetEntitySystem<TransformSystem>(); EntityUid origin = default; EntityUid other = default; @@ -46,7 +48,7 @@ await server.WaitAssertion(() => origin = sEntities.SpawnEntity(HumanId, coordinates); other = sEntities.SpawnEntity(HumanId, coordinates); conSystem.EnsureContainer<Container>(other, "InRangeUnobstructedTestOtherContainer"); - mapCoordinates = sEntities.GetComponent<TransformComponent>(other).MapPosition; + mapCoordinates = tSystem.GetMapCoordinates(other); }); await server.WaitIdleAsync(); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index a19b62cd70a..0f2c314ed01 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -171,7 +171,7 @@ await Server.WaitPost(() => // turn on welders if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated) { - Assert.That(ItemToggleSys.TryActivate(item, playerEnt, itemToggle: itemToggle)); + Assert.That(ItemToggleSys.TryActivate((item, itemToggle), user: playerEnt)); } }); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 457d3e31920..b3d684e01a0 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -104,7 +104,7 @@ public abstract partial class InteractionTest protected Content.Server.Construction.ConstructionSystem SConstruction = default!; protected SharedDoAfterSystem DoAfterSys = default!; protected ToolSystem ToolSys = default!; - protected SharedItemToggleSystem ItemToggleSys = default!; + protected ItemToggleSystem ItemToggleSys = default!; protected InteractionTestSystem STestSystem = default!; protected SharedTransformSystem Transform = default!; protected SharedMapSystem MapSystem = default!; @@ -165,7 +165,7 @@ public virtual async Task Setup() HandSys = SEntMan.System<HandsSystem>(); InteractSys = SEntMan.System<SharedInteractionSystem>(); ToolSys = SEntMan.System<ToolSystem>(); - ItemToggleSys = SEntMan.System<SharedItemToggleSystem>(); + ItemToggleSys = SEntMan.System<ItemToggleSystem>(); DoAfterSys = SEntMan.System<SharedDoAfterSystem>(); Transform = SEntMan.System<SharedTransformSystem>(); MapSystem = SEntMan.System<SharedMapSystem>(); diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs index 12b395f86d3..54b394f67b8 100644 --- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs +++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs @@ -192,7 +192,7 @@ public async Task NoMaterialArbitrage() { foreach (var recipe in recipes) { - foreach (var (matId, amount) in recipe.RequiredMaterials) + foreach (var (matId, amount) in recipe.Materials) { var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier); if (spawnedMats.TryGetValue(matId, out var numSpawned)) @@ -272,7 +272,7 @@ public async Task NoMaterialArbitrage() { foreach (var recipe in recipes) { - foreach (var (matId, amount) in recipe.RequiredMaterials) + foreach (var (matId, amount) in recipe.Materials) { var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier); if (deconstructedMats.TryGetValue(matId, out var numSpawned)) @@ -327,7 +327,7 @@ public async Task NoMaterialArbitrage() { foreach (var recipe in recipes) { - foreach (var (matId, amount) in recipe.RequiredMaterials) + foreach (var (matId, amount) in recipe.Materials) { var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier); if (compositionComponent.MaterialComposition.TryGetValue(matId, out var numSpawned)) diff --git a/Content.IntegrationTests/Tests/StoreTests.cs b/Content.IntegrationTests/Tests/StoreTests.cs new file mode 100644 index 00000000000..877db1d9a02 --- /dev/null +++ b/Content.IntegrationTests/Tests/StoreTests.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Content.Server.Store.Systems; +using Content.Server.Traitor.Uplink; +using Content.Shared.FixedPoint; +using Content.Shared.Inventory; +using Content.Shared.Store; +using Content.Shared.Store.Components; +using Content.Shared.StoreDiscount.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.IntegrationTests.Tests; + +[TestFixture] +public sealed class StoreTests +{ + + [TestPrototypes] + private const string Prototypes = @" +- type: entity + name: InventoryPdaDummy + id: InventoryPdaDummy + parent: BasePDA + components: + - type: Clothing + QuickEquip: false + slots: + - idcard + - type: Pda +"; + [Test] + public async Task StoreDiscountAndRefund() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + + var testMap = await pair.CreateTestMap(); + await server.WaitIdleAsync(); + + var serverRandom = server.ResolveDependency<IRobustRandom>(); + serverRandom.SetSeed(534); + + var entManager = server.ResolveDependency<IEntityManager>(); + + var mapSystem = server.System<SharedMapSystem>(); + var prototypeManager = server.ProtoMan; + + Assert.That(mapSystem.IsInitialized(testMap.MapId)); + + + EntityUid human = default; + EntityUid uniform = default; + EntityUid pda = default; + + var uplinkSystem = entManager.System<UplinkSystem>(); + + var listingPrototypes = prototypeManager.EnumeratePrototypes<ListingPrototype>() + .ToArray(); + + var coordinates = testMap.GridCoords; + await server.WaitAssertion(() => + { + var invSystem = entManager.System<InventorySystem>(); + + human = entManager.SpawnEntity("HumanUniformDummy", coordinates); + uniform = entManager.SpawnEntity("UniformDummy", coordinates); + pda = entManager.SpawnEntity("InventoryPdaDummy", coordinates); + + Assert.That(invSystem.TryEquip(human, uniform, "jumpsuit")); + Assert.That(invSystem.TryEquip(human, pda, "id")); + + FixedPoint2 originalBalance = 20; + uplinkSystem.AddUplink(human, originalBalance, null, true); + + var storeComponent = entManager.GetComponent<StoreComponent>(pda); + var discountComponent = entManager.GetComponent<StoreDiscountComponent>(pda); + Assert.That( + discountComponent.Discounts, + Has.Exactly(3).Items, + $"After applying discount total discounted items count was expected to be '3' " + + $"but was actually {discountComponent.Discounts.Count}- this can be due to discount " + + $"categories settings (maxItems, weight) not being realistically set, or default " + + $"discounted count being changed from '3' in StoreDiscountSystem.InitializeDiscounts." + ); + var discountedListingItems = storeComponent.FullListingsCatalog + .Where(x => x.IsCostModified) + .OrderBy(x => x.ID) + .ToArray(); + Assert.That(discountComponent.Discounts + .Select(x => x.ListingId.Id), + Is.EquivalentTo(discountedListingItems.Select(x => x.ID)), + $"{nameof(StoreComponent)}.{nameof(StoreComponent.FullListingsCatalog)} does not contain all " + + $"items that are marked as discounted, or they don't have flag '{nameof(ListingDataWithCostModifiers.IsCostModified)}'" + + $"flag as 'true'. This marks the fact that cost modifier of discount is not applied properly!" + ); + + // Refund action requests re-generation of listing items so we will be re-acquiring items from component a lot of times. + var itemIds = discountedListingItems.Select(x => x.ID); + foreach (var itemId in itemIds) + { + Assert.Multiple(() => + { + storeComponent.RefundAllowed = true; + + var discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId); + var plainDiscountedCost = discountedListingItem.Cost[UplinkSystem.TelecrystalCurrencyPrototype]; + + var prototype = listingPrototypes.First(x => x.ID == discountedListingItem.ID); + + var prototypeCost = prototype.Cost[UplinkSystem.TelecrystalCurrencyPrototype]; + var discountDownTo = prototype.DiscountDownTo[UplinkSystem.TelecrystalCurrencyPrototype]; + Assert.That(plainDiscountedCost.Value, Is.GreaterThanOrEqualTo(discountDownTo.Value), "Expected discounted cost to be greater then DiscountDownTo value."); + Assert.That(plainDiscountedCost.Value, Is.LessThan(prototypeCost.Value), "Expected discounted cost to be lower then prototype cost."); + + + var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID){Actor = human}; + server.EntMan.EventBus.RaiseComponentEvent(pda, storeComponent, buyMsg); + + var newBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype]; + Assert.That(newBalance.Value, Is.EqualTo((originalBalance - plainDiscountedCost).Value), "Expected to have balance reduced by discounted cost"); + Assert.That( + discountedListingItem.IsCostModified, + Is.False, + $"Expected item cost to not be modified after Buying discounted item." + ); + var costAfterBuy = discountedListingItem.Cost[UplinkSystem.TelecrystalCurrencyPrototype]; + Assert.That(costAfterBuy.Value, Is.EqualTo(prototypeCost.Value), "Expected cost after discount refund to be equal to prototype cost."); + + var refundMsg = new StoreRequestRefundMessage { Actor = human }; + server.EntMan.EventBus.RaiseComponentEvent(pda, storeComponent, refundMsg); + + // get refreshed item after refund re-generated items + discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId); + + var afterRefundBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype]; + Assert.That(afterRefundBalance.Value, Is.EqualTo(originalBalance.Value), "Expected refund to return all discounted cost value."); + Assert.That( + discountComponent.Discounts.First(x => x.ListingId == discountedListingItem.ID).Count, + Is.EqualTo(0), + "Discounted count should still be zero even after refund." + ); + + Assert.That( + discountedListingItem.IsCostModified, + Is.False, + $"Expected item cost to not be modified after Buying discounted item (even after refund was done)." + ); + var costAfterRefund = discountedListingItem.Cost[UplinkSystem.TelecrystalCurrencyPrototype]; + Assert.That(costAfterRefund.Value, Is.EqualTo(prototypeCost.Value), "Expected cost after discount refund to be equal to prototype cost."); + }); + } + + }); + + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Tag/TagTest.cs b/Content.IntegrationTests/Tests/Tag/TagTest.cs index cbcdd1c6c62..e6cd2accaf5 100644 --- a/Content.IntegrationTests/Tests/Tag/TagTest.cs +++ b/Content.IntegrationTests/Tests/Tag/TagTest.cs @@ -53,11 +53,13 @@ public async Task TagComponentTest() EntityUid sTagDummy = default; TagComponent sTagComponent = null!; + Entity<TagComponent> sTagEntity = default; await server.WaitPost(() => { sTagDummy = sEntityManager.SpawnEntity(TagEntityId, MapCoordinates.Nullspace); sTagComponent = sEntityManager.GetComponent<TagComponent>(sTagDummy); + sTagEntity = new Entity<TagComponent>(sTagDummy, sTagComponent); }); await server.WaitAssertion(() => @@ -130,49 +132,64 @@ await server.WaitAssertion(() => Assert.Multiple(() => { // Cannot add the starting tag again - Assert.That(tagSystem.AddTag(sTagDummy, sTagComponent, StartingTag), Is.False); - Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, StartingTag, StartingTag), Is.False); - Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, new List<string> { StartingTag, StartingTag }), Is.False); + Assert.That(tagSystem.AddTag(sTagEntity, StartingTag), Is.False); + + Assert.That(tagSystem.AddTags(sTagEntity, StartingTag, StartingTag), Is.False); + Assert.That(tagSystem.AddTags(sTagEntity, new List<ProtoId<TagPrototype>> { StartingTag, StartingTag }), Is.False); + Assert.That(tagSystem.AddTags(sTagEntity, new HashSet<ProtoId<TagPrototype>> { StartingTag, StartingTag }), Is.False); // Has the starting tag Assert.That(tagSystem.HasTag(sTagComponent, StartingTag), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, StartingTag, StartingTag), Is.True); - Assert.That(tagSystem.HasAllTags(sTagComponent, new List<string> { StartingTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, new List<ProtoId<TagPrototype>> { StartingTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, new HashSet<ProtoId<TagPrototype>> { StartingTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, StartingTag, StartingTag), Is.True); - Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<string> { StartingTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<ProtoId<TagPrototype>> { StartingTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new HashSet<ProtoId<TagPrototype>> { StartingTag, StartingTag }), Is.True); // Does not have the added tag yet Assert.That(tagSystem.HasTag(sTagComponent, AddedTag), Is.False); + Assert.That(tagSystem.HasAllTags(sTagComponent, AddedTag, AddedTag), Is.False); - Assert.That(tagSystem.HasAllTags(sTagComponent, new List<string> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAllTags(sTagComponent, new List<ProtoId<TagPrototype>> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAllTags(sTagComponent, new HashSet<ProtoId<TagPrototype>> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAnyTag(sTagComponent, AddedTag, AddedTag), Is.False); - Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<string> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<ProtoId<TagPrototype>> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new HashSet<ProtoId<TagPrototype>> { AddedTag, AddedTag }), Is.False); // Has a combination of the two tags Assert.That(tagSystem.HasAnyTag(sTagComponent, StartingTag, AddedTag), Is.True); - Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<string> { StartingTag, AddedTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new HashSet<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.True); // Does not have both tags Assert.That(tagSystem.HasAllTags(sTagComponent, StartingTag, AddedTag), Is.False); - Assert.That(tagSystem.HasAllTags(sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAllTags(sTagComponent, new List<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.False); + Assert.That(tagSystem.HasAllTags(sTagComponent, new HashSet<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.False); // Cannot remove a tag that does not exist - Assert.That(tagSystem.RemoveTag(sTagDummy, sTagComponent, AddedTag), Is.False); - Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, AddedTag, AddedTag), Is.False); - Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, new List<string> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.RemoveTag(sTagEntity, AddedTag), Is.False); + + Assert.That(tagSystem.RemoveTags(sTagEntity, AddedTag, AddedTag), Is.False); + Assert.That(tagSystem.RemoveTags(sTagEntity, new List<ProtoId<TagPrototype>> { AddedTag, AddedTag }), Is.False); + Assert.That(tagSystem.RemoveTags(sTagEntity, new HashSet<ProtoId<TagPrototype>> { AddedTag, AddedTag }), Is.False); }); // Can add the new tag - Assert.That(tagSystem.AddTag(sTagDummy, sTagComponent, AddedTag), Is.True); + Assert.That(tagSystem.AddTag(sTagEntity, AddedTag), Is.True); Assert.Multiple(() => { // Cannot add it twice - Assert.That(tagSystem.AddTag(sTagDummy, sTagComponent, AddedTag), Is.False); + Assert.That(tagSystem.AddTag(sTagEntity, AddedTag), Is.False); // Cannot add existing tags - Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, StartingTag, AddedTag), Is.False); - Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False); + Assert.That(tagSystem.AddTags(sTagEntity, StartingTag, AddedTag), Is.False); + Assert.That(tagSystem.AddTags(sTagEntity, new List<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.False); + Assert.That(tagSystem.AddTags(sTagEntity, new HashSet<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.False); // Now has two tags Assert.That(sTagComponent.Tags, Has.Count.EqualTo(2)); @@ -180,65 +197,103 @@ await server.WaitAssertion(() => // Has both tags Assert.That(tagSystem.HasTag(sTagComponent, StartingTag), Is.True); Assert.That(tagSystem.HasTag(sTagComponent, AddedTag), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, StartingTag, StartingTag), Is.True); Assert.That(tagSystem.HasAllTags(sTagComponent, AddedTag, StartingTag), Is.True); - Assert.That(tagSystem.HasAllTags(sTagComponent, new List<string> { StartingTag, AddedTag }), Is.True); - Assert.That(tagSystem.HasAllTags(sTagComponent, new List<string> { AddedTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, new List<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, new List<ProtoId<TagPrototype>> { AddedTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, new HashSet<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.True); + Assert.That(tagSystem.HasAllTags(sTagComponent, new HashSet<ProtoId<TagPrototype>> { AddedTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, StartingTag, AddedTag), Is.True); Assert.That(tagSystem.HasAnyTag(sTagComponent, AddedTag, StartingTag), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new List<ProtoId<TagPrototype>> { AddedTag, StartingTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new HashSet<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.True); + Assert.That(tagSystem.HasAnyTag(sTagComponent, new HashSet<ProtoId<TagPrototype>> { AddedTag, StartingTag }), Is.True); }); Assert.Multiple(() => { // Remove the existing starting tag - Assert.That(tagSystem.RemoveTag(sTagDummy, sTagComponent, StartingTag), Is.True); + Assert.That(tagSystem.RemoveTag(sTagEntity, StartingTag), Is.True); // Remove the existing added tag - Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, AddedTag, AddedTag), Is.True); + Assert.That(tagSystem.RemoveTags(sTagEntity, AddedTag, AddedTag), Is.True); }); Assert.Multiple(() => { // No tags left to remove - Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False); + Assert.That(tagSystem.RemoveTags(sTagEntity, new List<ProtoId<TagPrototype>> { StartingTag, AddedTag }), Is.False); // No tags left in the component Assert.That(sTagComponent.Tags, Is.Empty); }); -#if !DEBUG - return; + // It is run only in DEBUG build, + // as the checks are performed only in DEBUG build. +#if DEBUG + // Has single + Assert.Throws<DebugAssertException>(() => { tagSystem.HasTag(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasTag(sTagComponent, UnregisteredTag); }); + + // HasAny entityUid methods + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagDummy, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagDummy, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagDummy, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // HasAny component methods + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagComponent, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagComponent, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagComponent, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAnyTag(sTagComponent, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // HasAll entityUid methods + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagDummy, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagDummy, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagDummy, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // HasAll component methods + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagComponent, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagComponent, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagComponent, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.HasAllTags(sTagComponent, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // RemoveTag single + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTag(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTag(sTagEntity, UnregisteredTag); }); + + // RemoveTags entityUid methods + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagDummy, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagDummy, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagDummy, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // RemoveTags entity methods + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagEntity, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagEntity, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagEntity, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.RemoveTags(sTagEntity, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // AddTag single + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTag(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTag(sTagEntity, UnregisteredTag); }); + + // AddTags entityUid methods + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagDummy, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagDummy, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagDummy, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagDummy, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); + + // AddTags entity methods + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagEntity, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagEntity, UnregisteredTag, UnregisteredTag); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagEntity, new List<ProtoId<TagPrototype>> { UnregisteredTag }); }); + Assert.Throws<DebugAssertException>(() => { tagSystem.AddTags(sTagEntity, new HashSet<ProtoId<TagPrototype>> { UnregisteredTag }); }); #endif - - // Single - Assert.Throws<DebugAssertException>(() => - { - tagSystem.HasTag(sTagDummy, UnregisteredTag); - }); - Assert.Throws<DebugAssertException>(() => - { - tagSystem.HasTag(sTagComponent, UnregisteredTag); - }); - - // Any - Assert.Throws<DebugAssertException>(() => - { - tagSystem.HasAnyTag(sTagDummy, UnregisteredTag); - }); - Assert.Throws<DebugAssertException>(() => - { - tagSystem.HasAnyTag(sTagComponent, UnregisteredTag); - }); - - // All - Assert.Throws<DebugAssertException>(() => - { - tagSystem.HasAllTags(sTagDummy, UnregisteredTag); - }); - Assert.Throws<DebugAssertException>(() => - { - tagSystem.HasAllTags(sTagComponent, UnregisteredTag); - }); }); await pair.CleanReturnAsync(); } diff --git a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs index 040b09beecc..ecb11fc1ba4 100644 --- a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs +++ b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; -using System.Linq; +using System.Reflection; +using Content.Server.Administration.Managers; using Robust.Shared.Toolshed; namespace Content.IntegrationTests.Tests.Toolshed; @@ -10,10 +11,23 @@ public sealed class AdminTest : ToolshedTest [Test] public async Task AllCommandsHavePermissions() { + var toolMan = Server.ResolveDependency<ToolshedManager>(); + var admin = Server.ResolveDependency<IAdminManager>(); + var ignored = new HashSet<Assembly>() + {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly}; + await Server.WaitAssertion(() => { - Assert.That(InvokeCommand("cmd:list where { acmd:perms isnull }", out var res)); - Assert.That((IEnumerable<CommandSpec>) res, Is.Empty, "All commands must have admin permissions set up."); + Assert.Multiple(() => + { + foreach (var cmd in toolMan.DefaultEnvironment.AllCommands()) + { + if (ignored.Contains(cmd.Cmd.GetType().Assembly)) + continue; + + Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}"); + } + }); }); } } diff --git a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs index 2b5553ad7dd..fb210eba505 100644 --- a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs +++ b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Globalization; -using Robust.Shared.IoC; +using System.Reflection; using Robust.Shared.Localization; using Robust.Shared.Toolshed; @@ -14,10 +14,27 @@ public sealed class LocTest : ToolshedTest [Test] public async Task AllCommandsHaveDescriptions() { + var locMan = Server.ResolveDependency<ILocalizationManager>(); + var toolMan = Server.ResolveDependency<ToolshedManager>(); + var locStrings = new HashSet<string>(); + + var ignored = new HashSet<Assembly>() + {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly}; + await Server.WaitAssertion(() => { - Assert.That(InvokeCommand("cmd:list where { cmd:descloc loc:tryloc isnull }", out var res)); - Assert.That((IEnumerable<CommandSpec>)res!, Is.Empty, "All commands must have localized descriptions."); + Assert.Multiple(() => + { + foreach (var cmd in toolMan.DefaultEnvironment.AllCommands()) + { + if (ignored.Contains(cmd.Cmd.GetType().Assembly)) + continue; + + var descLoc = cmd.DescLocStr(); + Assert.That(locStrings.Add(descLoc), $"Duplicate command description key: {descLoc}"); + Assert.That(locMan.TryGetString(descLoc, out _), $"Failed to get command description for command {cmd.FullName()}"); + } + }); }); } } diff --git a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs index 7de81fb3dc2..6fc27e168cf 100644 --- a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs +++ b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs @@ -74,15 +74,15 @@ protected T InvokeCommand<T>(string command) return (T) res!; } - protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null, bool once = false) + protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null) { var parser = new ParserContext(command, Toolshed); - var success = CommandRun.TryParse(false, parser, inputType, expectedType, once, out _, out _, out var error); + var success = CommandRun.TryParse(parser, inputType, expectedType, out _); - if (error is not null) - ReportError(error); + if (parser.Error is not null) + ReportError(parser.Error); - if (error is null) + if (parser.Error is null) Assert.That(success, $"Parse failed despite no error being reported. Parsed {command}"); } @@ -153,11 +153,28 @@ public IEnumerable<IConError> GetErrors() return _errors; } + public bool HasErrors => _errors.Count > 0; + public void ClearErrors() { _errors.Clear(); } + public object? ReadVar(string name) + { + return Variables.GetValueOrDefault(name); + } + + public void WriteVar(string name, object? value) + { + Variables[name] = value; + } + + public IEnumerable<string> GetVars() + { + return Variables.Keys; + } + public Dictionary<string, object?> Variables { get; } = new(); protected void ExpectError(Type err) diff --git a/Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.Designer.cs new file mode 100644 index 00000000000..61aa1a8c73e --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.Designer.cs @@ -0,0 +1,1769 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20240112194620_Blacklist")] + partial class Blacklist + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("Backpack") + .IsRequired() + .HasColumnType("text") + .HasColumnName("backpack"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("Clothing") + .IsRequired() + .HasColumnType("text") + .HasColumnName("clothing"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime>("StartDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<ValueTuple<IPAddress, int>?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<ValueTuple<IPAddress, int>?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.cs b/Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.cs new file mode 100644 index 00000000000..a6a34626ed4 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class Blacklist : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "blacklist", + columns: table => new + { + user_id = table.Column<Guid>(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_blacklist", x => x.user_id); + }); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "blacklist"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240318022005_AdminMessageDismiss.cs b/Content.Server.Database/Migrations/Postgres/20240318022005_AdminMessageDismiss.cs index 9911aeffe2c..372eecc5859 100644 --- a/Content.Server.Database/Migrations/Postgres/20240318022005_AdminMessageDismiss.cs +++ b/Content.Server.Database/Migrations/Postgres/20240318022005_AdminMessageDismiss.cs @@ -17,7 +17,7 @@ protected override void Up(MigrationBuilder migrationBuilder) nullable: false, defaultValue: false); - migrationBuilder.Sql("UPDATE admin_messages SET dismissed = seen"); + migrationBuilder.Sql("UPDATE admin_messages SET dismissed = seen;"); migrationBuilder.AddCheckConstraint( name: "NotDismissedAndSeen", diff --git a/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs new file mode 100644 index 00000000000..2f06e5ff984 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs @@ -0,0 +1,1913 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20240606121555_ban_notify_trigger")] + partial class ban_notify_trigger + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.cs b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.cs new file mode 100644 index 00000000000..b84d230b455 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class ban_notify_trigger : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + create or replace function send_server_ban_notification() + returns trigger as $$ + declare + x_server_id integer; + begin + select round.server_id into x_server_id from round where round.round_id = NEW.round_id; + + perform pg_notify('ban_notification', json_build_object('ban_id', NEW.server_ban_id, 'server_id', x_server_id)::text); + return NEW; + end; + $$ LANGUAGE plpgsql; + """); + + migrationBuilder.Sql(""" + create or replace trigger notify_on_server_ban_insert + after insert on server_ban + for each row + execute function send_server_ban_notification(); + """); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + drop trigger notify_on_server_ban_insert on server_ban; + drop function send_server_ban_notification; + """); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240606175154_ReturnLastReadRules.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240606175154_ReturnLastReadRules.Designer.cs new file mode 100644 index 00000000000..e82e58bf5ce --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240606175154_ReturnLastReadRules.Designer.cs @@ -0,0 +1,1913 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20240606175154_ReturnLastReadRules")] + partial class ReturnLastReadRules + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240606175154_ReturnLastReadRules.cs b/Content.Server.Database/Migrations/Postgres/20240606175154_ReturnLastReadRules.cs new file mode 100644 index 00000000000..af4e5ad4c40 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240606175154_ReturnLastReadRules.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class ReturnLastReadRules : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<DateTime>( + name: "last_read_rules", + table: "player", + type: "timestamp with time zone", + nullable: true); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "last_read_rules", + table: "player"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs new file mode 100644 index 00000000000..8bd837236af --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs @@ -0,0 +1,1915 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20240621120713_ConnectionLogTimeIndex")] + partial class ConnectionLogTimeIndex + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs new file mode 100644 index 00000000000..736e9e37a22 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class ConnectionLogTimeIndex : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_connection_log_time", + table: "connection_log", + column: "time"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_connection_log_time", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240623005121_BanTemplate.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240623005121_BanTemplate.Designer.cs index 2fb55ddfec8..5d89f3dd29b 100644 --- a/Content.Server.Database/Migrations/Postgres/20240623005121_BanTemplate.Designer.cs +++ b/Content.Server.Database/Migrations/Postgres/20240623005121_BanTemplate.Designer.cs @@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Postgres { [DbContext(typeof(PostgresServerDbContext))] - [Migration("20240623005113_BanTemplate")] + [Migration("20240623005121_BanTemplate")] partial class BanTemplate { /// <inheritdoc /> @@ -186,6 +186,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("deleted_by_id"); + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + b.Property<DateTime?>("ExpirationTime") .HasColumnType("timestamp with time zone") .HasColumnName("expiration_time"); @@ -235,7 +239,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("RoundId") .HasDatabaseName("IX_admin_messages_round_id"); - b.ToTable("admin_messages", (string)null); + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); }); modelBuilder.Entity("Content.Server.Database.AdminNote", b => @@ -509,49 +516,49 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) }); modelBuilder.Entity("Content.Server.Database.BanTemplate", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("ban_template_id"); + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); - b.Property<bool>("AutoDelete") - .HasColumnType("boolean") - .HasColumnName("auto_delete"); + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); - b.Property<int>("ExemptFlags") - .HasColumnType("integer") - .HasColumnName("exempt_flags"); + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); - b.Property<bool>("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); - b.Property<TimeSpan>("Length") - .HasColumnType("interval") - .HasColumnName("length"); + b.Property<TimeSpan>("Length") + .HasColumnType("interval") + .HasColumnName("length"); - b.Property<string>("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); - b.Property<int>("Severity") - .HasColumnType("integer") - .HasColumnName("severity"); + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); - b.Property<string>("Title") - .IsRequired() - .HasColumnType("text") - .HasColumnName("title"); + b.Property<string>("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("Id") - .HasName("PK_ban_template"); + b.HasKey("Id") + .HasName("PK_ban_template"); - b.ToTable("ban_template", (string)null); - }); + b.ToTable("ban_template", (string)null); + }); modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { @@ -600,6 +607,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ServerId") .HasDatabaseName("IX_connection_log_server_id"); + b.HasIndex("Time"); + b.HasIndex("UserId"); b.ToTable("connection_log", null, t => @@ -645,33 +654,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("job", (string)null); }); - modelBuilder.Entity("Content.Server.Database.Loadout", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("loadout_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); - - b.Property<string>("LoadoutName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("loadout_name"); - - b.Property<int>("ProfileId") - .HasColumnType("integer") - .HasColumnName("profile_id"); - - b.HasKey("Id") - .HasName("PK_loadout"); - - b.HasIndex("ProfileId", "LoadoutName") - .IsUnique(); - - b.ToTable("loadout", (string)null); - }); - modelBuilder.Entity("Content.Server.Database.PlayTime", b => { b.Property<int>("Id") @@ -803,21 +785,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("age"); - b.Property<string>("Backpack") - .IsRequired() - .HasColumnType("text") - .HasColumnName("backpack"); - b.Property<string>("CharacterName") .IsRequired() .HasColumnType("text") .HasColumnName("char_name"); - b.Property<string>("Clothing") - .IsRequired() - .HasColumnType("text") - .HasColumnName("clothing"); - b.Property<string>("EyeColor") .IsRequired() .HasColumnType("text") @@ -900,6 +872,100 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("profile", (string)null); }); + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.Round", b => { b.Property<int>("Id") @@ -913,10 +979,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("server_id"); - b.Property<DateTime>("StartDate") - .ValueGeneratedOnAdd() + b.Property<DateTime?>("StartDate") .HasColumnType("timestamp with time zone") - .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) .HasColumnName("start_date"); b.HasKey("Id") @@ -1577,28 +1641,65 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.Loadout", b => + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => { b.HasOne("Content.Server.Database.Profile", "Profile") .WithMany("Loadouts") .HasForeignKey("ProfileId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_loadout_profile_profile_id"); + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.Profile", b => + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => { - b.HasOne("Content.Server.Database.Preference", "Preference") - .WithMany("Profiles") - .HasForeignKey("PreferenceId") + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_profile_preference_preference_id"); + .HasConstraintName("FK_role_whitelists_player_player_user_id"); - b.Navigation("Preference"); + b.Navigation("Player"); }); modelBuilder.Entity("Content.Server.Database.Round", b => @@ -1800,6 +1901,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("AdminWatchlistsLastEdited"); b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); }); modelBuilder.Entity("Content.Server.Database.Preference", b => @@ -1818,6 +1921,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Traits"); }); + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + modelBuilder.Entity("Content.Server.Database.Round", b => { b.Navigation("AdminLogs"); diff --git a/Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.Designer.cs b/Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.Designer.cs new file mode 100644 index 00000000000..155d6a163fd --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.Designer.cs @@ -0,0 +1,2072 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20241111170112_ModernHwid")] + partial class ModernHwid + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<TimeSpan>("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.cs b/Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.cs new file mode 100644 index 00000000000..c70a5ffaa58 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class ModernHwid : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<int>( + name: "hwid_type", + table: "server_role_ban", + type: "integer", + nullable: true, + defaultValue: 0); + + migrationBuilder.AddColumn<int>( + name: "hwid_type", + table: "server_ban", + type: "integer", + nullable: true, + defaultValue: 0); + + migrationBuilder.AddColumn<int>( + name: "last_seen_hwid_type", + table: "player", + type: "integer", + nullable: true, + defaultValue: 0); + + migrationBuilder.AddColumn<int>( + name: "hwid_type", + table: "connection_log", + type: "integer", + nullable: true, + defaultValue: 0); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "hwid_type", + table: "server_role_ban"); + + migrationBuilder.DropColumn( + name: "hwid_type", + table: "server_ban"); + + migrationBuilder.DropColumn( + name: "last_seen_hwid_type", + table: "player"); + + migrationBuilder.DropColumn( + name: "hwid_type", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.Designer.cs b/Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.Designer.cs new file mode 100644 index 00000000000..dc1b4a0eeb7 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.Designer.cs @@ -0,0 +1,2076 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20241111193608_ConnectionTrust")] + partial class ConnectionTrust + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<TimeSpan>("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<float>("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.cs b/Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.cs new file mode 100644 index 00000000000..debb36aaccb --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class ConnectionTrust : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<float>( + name: "trust", + table: "connection_log", + type: "real", + nullable: false, + defaultValue: 0f); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "trust", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20241222203147_UpstreamMerge.Designer.cs b/Content.Server.Database/Migrations/Postgres/20241222203147_UpstreamMerge.Designer.cs new file mode 100644 index 00000000000..3e7b22c9b4c --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20241222203147_UpstreamMerge.Designer.cs @@ -0,0 +1,2030 @@ +// <auto-generated /> +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20241222203147_UpstreamMerge")] + partial class UpstreamMerge + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<short>("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property<JsonDocument>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<TimeSpan>("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<IPAddress>("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property<float>("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Loadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("CustomColorTint") + .HasColumnType("text") + .HasColumnName("custom_color_tint"); + + b.Property<string>("CustomDescription") + .HasColumnType("text") + .HasColumnName("custom_description"); + + b.Property<bool?>("CustomHeirloom") + .HasColumnType("boolean") + .HasColumnName("custom_heirloom"); + + b.Property<string>("CustomName") + .HasColumnType("text") + .HasColumnName("custom_name"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_loadout"); + + b.HasIndex("ProfileId", "LoadoutName") + .IsUnique(); + + b.ToTable("loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<Guid>("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property<IPAddress>("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property<string>("Backpack") + .IsRequired() + .HasColumnType("text") + .HasColumnName("backpack"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property<string>("Clothing") + .IsRequired() + .HasColumnType("text") + .HasColumnName("clothing"); + + b.Property<string>("CustomSpecieName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("custom_specie_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property<float>("Height") + .HasColumnType("real") + .HasColumnName("height"); + + b.Property<JsonDocument>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.Property<float>("Width") + .HasColumnType("real") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<NpgsqlInet?>("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Loadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20241222203147_UpstreamMerge.cs b/Content.Server.Database/Migrations/Postgres/20241222203147_UpstreamMerge.cs new file mode 100644 index 00000000000..029a62460ba --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20241222203147_UpstreamMerge.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// <inheritdoc /> + public partial class UpstreamMerge : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP TABLE IF EXISTS ProileLoadouts;"); + + migrationBuilder.AddForeignKey( + name: "FK_loadout_profile_profile_id", + table: "loadout", + column: "profile_id", + principalTable: "profile", + principalColumn: "profile_id", + onDelete: ReferentialAction.Cascade); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_loadout_profile_profile_id", + table: "loadout"); + + migrationBuilder.CreateTable( + name: "ProfileLoadout", + columns: table => new + { + ProfileId = table.Column<int>(type: "integer", nullable: true) + }, + constraints: table => + { + table.ForeignKey( + name: "FK_loadout_profile_profile_id", + column: x => x.ProfileId, + principalTable: "profile", + principalColumn: "profile_id", + onDelete: ReferentialAction.Cascade); + }); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index 5d6971a7a6c..c39fb6872be 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -557,6 +557,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ban_template", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.Property<int>("Id") @@ -575,10 +588,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("smallint") .HasColumnName("denied"); - b.Property<byte[]>("HWId") - .HasColumnType("bytea") - .HasColumnName("hwid"); - b.Property<int>("ServerId") .ValueGeneratedOnAdd() .HasColumnType("integer") @@ -589,6 +598,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("time"); + b.Property<float>("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + b.Property<Guid>("UserId") .HasColumnType("uuid") .HasColumnName("user_id"); @@ -604,6 +617,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ServerId") .HasDatabaseName("IX_connection_log_server_id"); + b.HasIndex("Time"); + b.HasIndex("UserId"); b.ToTable("connection_log", null, t => @@ -736,15 +751,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("first_seen_time"); + b.Property<DateTime?>("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + b.Property<IPAddress>("LastSeenAddress") .IsRequired() .HasColumnType("inet") .HasColumnName("last_seen_address"); - b.Property<byte[]>("LastSeenHWId") - .HasColumnType("bytea") - .HasColumnName("last_seen_hwid"); - b.Property<DateTime>("LastSeenTime") .HasColumnType("timestamp with time zone") .HasColumnName("last_seen_time"); @@ -1026,10 +1041,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("expiration_time"); - b.Property<byte[]>("HWId") - .HasColumnType("bytea") - .HasColumnName("hwid"); - b.Property<bool>("Hidden") .HasColumnType("boolean") .HasColumnName("hidden"); @@ -1160,10 +1171,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("expiration_time"); - b.Property<byte[]>("HWId") - .HasColumnType("bytea") - .HasColumnName("hwid"); - b.Property<bool>("Hidden") .HasColumnType("boolean") .HasColumnName("hidden"); @@ -1605,6 +1612,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasConstraintName("FK_connection_log_server_server_id"); + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + b.Navigation("Server"); }); @@ -1632,6 +1667,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + modelBuilder.Entity("Content.Server.Database.Profile", b => { b.HasOne("Content.Server.Database.Preference", "Preference") @@ -1690,8 +1756,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("RoundId") .HasConstraintName("FK_server_ban_round_round_id"); + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + b.Navigation("CreatedBy"); + b.Navigation("HWId"); + b.Navigation("LastEditedBy"); b.Navigation("Round"); @@ -1739,8 +1833,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("RoundId") .HasConstraintName("FK_server_role_ban_round_round_id"); + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + b.Navigation("CreatedBy"); + b.Navigation("HWId"); + b.Navigation("LastEditedBy"); b.Navigation("Round"); diff --git a/Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.Designer.cs new file mode 100644 index 00000000000..049901bc7d4 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.Designer.cs @@ -0,0 +1,1701 @@ +// <auto-generated /> +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20240112194612_Blacklist")] + partial class Blacklist + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.4"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property<Guid>("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<sbyte>("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property<string>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property<int>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property<string>("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property<Guid>("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property<string>("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<int>("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property<string>("Backpack") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("backpack"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property<string>("Clothing") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("clothing"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property<byte[]>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<DateTime>("StartDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.cs b/Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.cs new file mode 100644 index 00000000000..4988a3c336c --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// <inheritdoc /> + public partial class Blacklist : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "blacklist", + columns: table => new + { + user_id = table.Column<Guid>(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_blacklist", x => x.user_id); + }); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "blacklist"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240318021959_AdminMessageDismiss.cs b/Content.Server.Database/Migrations/Sqlite/20240318021959_AdminMessageDismiss.cs index 4cad4b81367..e0f7d0e0c78 100644 --- a/Content.Server.Database/Migrations/Sqlite/20240318021959_AdminMessageDismiss.cs +++ b/Content.Server.Database/Migrations/Sqlite/20240318021959_AdminMessageDismiss.cs @@ -17,7 +17,7 @@ protected override void Up(MigrationBuilder migrationBuilder) nullable: false, defaultValue: false); - migrationBuilder.Sql("UPDATE admin_messages SET dismissed = seen"); + migrationBuilder.Sql("UPDATE admin_messages SET dismissed = seen;"); migrationBuilder.AddCheckConstraint( name: "NotDismissedAndSeen", diff --git a/Content.Server.Database/Migrations/Sqlite/20240606175141_ReturnLastReadRules.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240606175141_ReturnLastReadRules.Designer.cs new file mode 100644 index 00000000000..043230b02b2 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240606175141_ReturnLastReadRules.Designer.cs @@ -0,0 +1,1838 @@ +// <auto-generated /> +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20240606175141_ReturnLastReadRules")] + partial class ReturnLastReadRules + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property<Guid>("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<sbyte>("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property<string>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property<int>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property<string>("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property<Guid>("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property<string>("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<int>("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property<byte[]>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240606175141_ReturnLastReadRules.cs b/Content.Server.Database/Migrations/Sqlite/20240606175141_ReturnLastReadRules.cs new file mode 100644 index 00000000000..0b0fd8725c0 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240606175141_ReturnLastReadRules.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// <inheritdoc /> + public partial class ReturnLastReadRules : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<DateTime>( + name: "last_read_rules", + table: "player", + type: "TEXT", + nullable: true); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "last_read_rules", + table: "player"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs new file mode 100644 index 00000000000..b3e512548ad --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs @@ -0,0 +1,1840 @@ +// <auto-generated /> +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20240621120705_ConnectionLogTimeIndex")] + partial class ConnectionLogTimeIndex + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property<Guid>("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<sbyte>("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property<string>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property<int>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property<string>("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property<Guid>("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property<string>("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property<byte[]>("LastSeenHWId") + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<int>("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property<byte[]>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<byte[]>("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs new file mode 100644 index 00000000000..f97c62f6ccf --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// <inheritdoc /> + public partial class ConnectionLogTimeIndex : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_connection_log_time", + table: "connection_log", + column: "time"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_connection_log_time", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240623005113_BanTemplate.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240623005113_BanTemplate.Designer.cs index 0bf74f84a70..acce21c2bd9 100644 --- a/Content.Server.Database/Migrations/Sqlite/20240623005113_BanTemplate.Designer.cs +++ b/Content.Server.Database/Migrations/Sqlite/20240623005113_BanTemplate.Designer.cs @@ -169,6 +169,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("deleted_by_id"); + b.Property<bool>("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + b.Property<DateTime?>("ExpirationTime") .HasColumnType("TEXT") .HasColumnName("expiration_time"); @@ -218,7 +222,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("RoundId") .HasDatabaseName("IX_admin_messages_round_id"); - b.ToTable("admin_messages", (string)null); + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); }); modelBuilder.Entity("Content.Server.Database.AdminNote", b => @@ -480,47 +487,47 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) }); modelBuilder.Entity("Content.Server.Database.BanTemplate", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("ban_template_id"); + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); - b.Property<bool>("AutoDelete") - .HasColumnType("INTEGER") - .HasColumnName("auto_delete"); + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); - b.Property<int>("ExemptFlags") - .HasColumnType("INTEGER") - .HasColumnName("exempt_flags"); + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); - b.Property<bool>("Hidden") - .HasColumnType("INTEGER") - .HasColumnName("hidden"); + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); - b.Property<TimeSpan>("Length") - .HasColumnType("TEXT") - .HasColumnName("length"); + b.Property<TimeSpan>("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); - b.Property<string>("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); - b.Property<int>("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); - b.Property<string>("Title") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("title"); + b.Property<string>("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); - b.HasKey("Id") - .HasName("PK_ban_template"); + b.HasKey("Id") + .HasName("PK_ban_template"); - b.ToTable("ban_template", (string)null); - }); + b.ToTable("ban_template", (string)null); + }); modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { @@ -567,6 +574,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ServerId") .HasDatabaseName("IX_connection_log_server_id"); + b.HasIndex("Time"); + b.HasIndex("UserId"); b.ToTable("connection_log", (string)null); @@ -607,31 +616,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("job", (string)null); }); - modelBuilder.Entity("Content.Server.Database.Loadout", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("loadout_id"); - - b.Property<string>("LoadoutName") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("loadout_name"); - - b.Property<int>("ProfileId") - .HasColumnType("INTEGER") - .HasColumnName("profile_id"); - - b.HasKey("Id") - .HasName("PK_loadout"); - - b.HasIndex("ProfileId", "LoadoutName") - .IsUnique(); - - b.ToTable("loadout", (string)null); - }); - modelBuilder.Entity("Content.Server.Database.PlayTime", b => { b.Property<int>("Id") @@ -752,21 +736,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasColumnName("age"); - b.Property<string>("Backpack") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("backpack"); - b.Property<string>("CharacterName") .IsRequired() .HasColumnType("TEXT") .HasColumnName("char_name"); - b.Property<string>("Clothing") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("clothing"); - b.Property<string>("EyeColor") .IsRequired() .HasColumnType("TEXT") @@ -849,6 +823,94 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("profile", (string)null); }); + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.Round", b => { b.Property<int>("Id") @@ -860,10 +922,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasColumnName("server_id"); - b.Property<DateTime>("StartDate") - .ValueGeneratedOnAdd() + b.Property<DateTime?>("StartDate") .HasColumnType("TEXT") - .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) .HasColumnName("start_date"); b.HasKey("Id") @@ -1504,28 +1564,65 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.Loadout", b => + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => { b.HasOne("Content.Server.Database.Profile", "Profile") .WithMany("Loadouts") .HasForeignKey("ProfileId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_loadout_profile_profile_id"); + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.Profile", b => + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => { - b.HasOne("Content.Server.Database.Preference", "Preference") - .WithMany("Profiles") - .HasForeignKey("PreferenceId") + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_profile_preference_preference_id"); + .HasConstraintName("FK_role_whitelists_player_player_user_id"); - b.Navigation("Preference"); + b.Navigation("Player"); }); modelBuilder.Entity("Content.Server.Database.Round", b => @@ -1727,6 +1824,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("AdminWatchlistsLastEdited"); b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); }); modelBuilder.Entity("Content.Server.Database.Preference", b => @@ -1745,6 +1844,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Traits"); }); + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + modelBuilder.Entity("Content.Server.Database.Round", b => { b.Navigation("AdminLogs"); diff --git a/Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.Designer.cs new file mode 100644 index 00000000000..56a9fe0a059 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.Designer.cs @@ -0,0 +1,1995 @@ +// <auto-generated /> +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20241111170107_ModernHwid")] + partial class ModernHwid + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property<Guid>("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<sbyte>("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property<string>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property<int>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<TimeSpan>("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property<string>("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property<Guid>("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property<string>("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<int>("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property<byte[]>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.cs b/Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.cs new file mode 100644 index 00000000000..97b5dafd03f --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// <inheritdoc /> + public partial class ModernHwid : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<int>( + name: "hwid_type", + table: "server_role_ban", + type: "INTEGER", + nullable: true, + defaultValue: 0); + + migrationBuilder.AddColumn<int>( + name: "hwid_type", + table: "server_ban", + type: "INTEGER", + nullable: true, + defaultValue: 0); + + migrationBuilder.AddColumn<int>( + name: "last_seen_hwid_type", + table: "player", + type: "INTEGER", + nullable: true, + defaultValue: 0); + + migrationBuilder.AddColumn<int>( + name: "hwid_type", + table: "connection_log", + type: "INTEGER", + nullable: true, + defaultValue: 0); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "hwid_type", + table: "server_role_ban"); + + migrationBuilder.DropColumn( + name: "hwid_type", + table: "server_ban"); + + migrationBuilder.DropColumn( + name: "last_seen_hwid_type", + table: "player"); + + migrationBuilder.DropColumn( + name: "hwid_type", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.Designer.cs new file mode 100644 index 00000000000..bd4e20a464b --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.Designer.cs @@ -0,0 +1,1999 @@ +// <auto-generated /> +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20241111193602_ConnectionTrust")] + partial class ConnectionTrust + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property<Guid>("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<sbyte>("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property<string>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property<int>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<TimeSpan>("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property<string>("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property<float>("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property<Guid>("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property<string>("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<int>("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property<byte[]>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property<string>("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property<int>("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.cs b/Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.cs new file mode 100644 index 00000000000..3a7fd784e16 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// <inheritdoc /> + public partial class ConnectionTrust : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<float>( + name: "trust", + table: "connection_log", + type: "REAL", + nullable: false, + defaultValue: 0f); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "trust", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20241222203134_UpstreamMerge.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20241222203134_UpstreamMerge.Designer.cs new file mode 100644 index 00000000000..8b6c0048392 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20241222203134_UpstreamMerge.Designer.cs @@ -0,0 +1,1957 @@ +// <auto-generated /> +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20241222203134_UpstreamMerge")] + partial class UpstreamMerge + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int?>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property<Guid>("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property<bool>("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<sbyte>("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property<string>("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property<string>("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<int>("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property<int>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<bool>("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<bool>("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property<int>("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property<string>("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property<bool>("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property<Guid?>("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<DateTime?>("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<string>("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property<string>("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<TimeSpan>("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property<string>("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property<string>("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<byte?>("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property<int>("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property<DateTime>("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property<float>("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<string>("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property<string>("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property<int>("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Loadout", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("loadout_id"); + + b.Property<string>("CustomColorTint") + .HasColumnType("TEXT") + .HasColumnName("custom_color_tint"); + + b.Property<string>("CustomDescription") + .HasColumnType("TEXT") + .HasColumnName("custom_description"); + + b.Property<bool?>("CustomHeirloom") + .HasColumnType("INTEGER") + .HasColumnName("custom_heirloom"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT") + .HasColumnName("custom_name"); + + b.Property<string>("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_loadout"); + + b.HasIndex("ProfileId", "LoadoutName") + .IsUnique(); + + b.ToTable("loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property<Guid>("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property<TimeSpan>("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property<string>("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property<DateTime>("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property<string>("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property<DateTime>("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property<string>("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<string>("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property<int>("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<int>("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property<string>("Backpack") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("backpack"); + + b.Property<string>("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property<string>("Clothing") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("clothing"); + + b.Property<string>("CustomSpecieName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("custom_specie_name"); + + b.Property<string>("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property<string>("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property<string>("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property<string>("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property<string>("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property<string>("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property<string>("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property<float>("Height") + .HasColumnType("REAL") + .HasColumnName("height"); + + b.Property<byte[]>("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property<int>("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property<int>("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property<string>("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property<string>("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property<int>("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property<int>("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property<string>("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.Property<float>("Width") + .HasColumnType("REAL") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property<Guid>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<string>("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<DateTime?>("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<bool>("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<int>("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property<int>("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<int>("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property<string>("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property<DateTime>("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property<Guid?>("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property<DateTime?>("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property<bool>("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property<DateTime?>("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property<Guid?>("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property<Guid?>("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property<TimeSpan>("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property<string>("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property<int?>("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property<int>("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property<int>("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property<DateTime>("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property<Guid?>("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property<int>("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property<string>("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property<byte[]>("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property<int>("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property<int>("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Loadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20241222203134_UpstreamMerge.cs b/Content.Server.Database/Migrations/Sqlite/20241222203134_UpstreamMerge.cs new file mode 100644 index 00000000000..8b5d3573763 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20241222203134_UpstreamMerge.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// <inheritdoc /> + public partial class UpstreamMerge : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP TABLE IF EXISTS ProileLoadouts;"); + + migrationBuilder.AddForeignKey( + name: "FK_loadout_profile_profile_id", + table: "loadout", + column: "profile_id", + principalTable: "profile", + principalColumn: "profile_id", + onDelete: ReferentialAction.Cascade); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_loadout_profile_profile_id", + table: "loadout"); + + migrationBuilder.CreateTable( + name: "ProfileLoadout", + columns: table => new + { + ProfileId = table.Column<int>(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.ForeignKey( + name: "FK_loadout_profile_profile_id", + column: x => x.ProfileId, + principalTable: "profile", + principalColumn: "profile_id", + onDelete: ReferentialAction.Cascade); + }); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index 88be39c2de2..3b2b680ddf5 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -526,6 +526,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ban_template", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property<Guid>("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.Property<int>("Id") @@ -542,10 +555,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasColumnName("denied"); - b.Property<byte[]>("HWId") - .HasColumnType("BLOB") - .HasColumnName("hwid"); - b.Property<int>("ServerId") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -556,6 +565,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("time"); + b.Property<float>("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + b.Property<Guid>("UserId") .HasColumnType("TEXT") .HasColumnName("user_id"); @@ -571,6 +584,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ServerId") .HasDatabaseName("IX_connection_log_server_id"); + b.HasIndex("Time"); + b.HasIndex("UserId"); b.ToTable("connection_log", (string)null); @@ -692,15 +707,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("first_seen_time"); + b.Property<DateTime?>("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + b.Property<string>("LastSeenAddress") .IsRequired() .HasColumnType("TEXT") .HasColumnName("last_seen_address"); - b.Property<byte[]>("LastSeenHWId") - .HasColumnType("BLOB") - .HasColumnName("last_seen_hwid"); - b.Property<DateTime>("LastSeenTime") .HasColumnType("TEXT") .HasColumnName("last_seen_time"); @@ -969,10 +984,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("expiration_time"); - b.Property<byte[]>("HWId") - .HasColumnType("BLOB") - .HasColumnName("hwid"); - b.Property<bool>("Hidden") .HasColumnType("INTEGER") .HasColumnName("hidden"); @@ -1097,10 +1108,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("expiration_time"); - b.Property<byte[]>("HWId") - .HasColumnType("BLOB") - .HasColumnName("hwid"); - b.Property<bool>("Hidden") .HasColumnType("INTEGER") .HasColumnName("hidden"); @@ -1532,6 +1539,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasConstraintName("FK_connection_log_server_server_id"); + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + b.Navigation("Server"); }); @@ -1559,6 +1594,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property<int>("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + modelBuilder.Entity("Content.Server.Database.Profile", b => { b.HasOne("Content.Server.Database.Preference", "Preference") @@ -1617,8 +1683,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("RoundId") .HasConstraintName("FK_server_ban_round_round_id"); + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + b.Navigation("CreatedBy"); + b.Navigation("HWId"); + b.Navigation("LastEditedBy"); b.Navigation("Round"); @@ -1666,8 +1760,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("RoundId") .HasConstraintName("FK_server_role_ban_round_round_id"); + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property<int>("ServerRoleBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b1.Property<byte[]>("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property<int>("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + b.Navigation("CreatedBy"); + b.Navigation("HWId"); + b.Navigation("LastEditedBy"); b.Navigation("Round"); diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 3245264e9da..377718bcba5 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Text.Json; @@ -30,6 +32,7 @@ protected ServerDbContext(DbContextOptions options) : base(options) public DbSet<AdminLog> AdminLog { get; set; } = null!; public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!; public DbSet<Whitelist> Whitelist { get; set; } = null!; + public DbSet<Blacklist> Blacklist { get; set; } = null!; public DbSet<ServerBan> Ban { get; set; } = default!; public DbSet<ServerUnban> Unban { get; set; } = default!; public DbSet<ServerBanExemption> BanExemption { get; set; } = default!; @@ -42,8 +45,8 @@ protected ServerDbContext(DbContextOptions options) : base(options) public DbSet<AdminNote> AdminNotes { get; set; } = null!; public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!; public DbSet<AdminMessage> AdminMessages { get; set; } = null!; - public DbSet<BanTemplate> BanTemplate { get; set; } = null!; public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!; + public DbSet<BanTemplate> BanTemplate { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -176,6 +179,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity<ConnectionLog>() .HasIndex(p => p.UserId); + modelBuilder.Entity<ConnectionLog>() + .HasIndex(p => p.Time); + modelBuilder.Entity<ConnectionLog>() .Property(p => p.ServerId) .HasDefaultValue(0); @@ -311,6 +317,47 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(w => w.PlayerUserId) .HasPrincipalKey(p => p.UserId) .OnDelete(DeleteBehavior.Cascade); + + // Changes for modern HWID integration + modelBuilder.Entity<Player>() + .OwnsOne(p => p.LastSeenHWId) + .Property(p => p.Hwid) + .HasColumnName("last_seen_hwid"); + + modelBuilder.Entity<Player>() + .OwnsOne(p => p.LastSeenHWId) + .Property(p => p.Type) + .HasDefaultValue(HwidType.Legacy); + + modelBuilder.Entity<ServerBan>() + .OwnsOne(p => p.HWId) + .Property(p => p.Hwid) + .HasColumnName("hwid"); + + modelBuilder.Entity<ServerBan>() + .OwnsOne(p => p.HWId) + .Property(p => p.Type) + .HasDefaultValue(HwidType.Legacy); + + modelBuilder.Entity<ServerRoleBan>() + .OwnsOne(p => p.HWId) + .Property(p => p.Hwid) + .HasColumnName("hwid"); + + modelBuilder.Entity<ServerRoleBan>() + .OwnsOne(p => p.HWId) + .Property(p => p.Type) + .HasDefaultValue(HwidType.Legacy); + + modelBuilder.Entity<ConnectionLog>() + .OwnsOne(p => p.HWId) + .Property(p => p.Hwid) + .HasColumnName("hwid"); + + modelBuilder.Entity<ConnectionLog>() + .OwnsOne(p => p.HWId) + .Property(p => p.Type) + .HasDefaultValue(HwidType.Legacy); } public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText) @@ -450,12 +497,14 @@ public class Player public string LastSeenUserName { get; set; } = null!; public DateTime LastSeenTime { get; set; } public IPAddress LastSeenAddress { get; set; } = null!; - public byte[]? LastSeenHWId { get; set; } + public TypedHwid? LastSeenHWId { get; set; } // Data that changes with each round public List<Round> Rounds { get; set; } = null!; public List<AdminLogPlayer> AdminLogs { get; set; } = null!; + public DateTime? LastReadRules { get; set; } + public List<AdminNote> AdminNotesReceived { get; set; } = null!; public List<AdminNote> AdminNotesCreated { get; set; } = null!; public List<AdminNote> AdminNotesLastEdited { get; set; } = null!; @@ -481,6 +530,15 @@ public class Whitelist [Required, Key] public Guid UserId { get; set; } } + /// <summary> + /// List of users who are on the "blacklist". This is a list that may be used by Whitelist implementations to deny access to certain users. + /// </summary> + [Table("blacklist")] + public class Blacklist + { + [Required, Key] public Guid UserId { get; set; } + } + public class Admin { [Key] public Guid UserId { get; set; } @@ -588,7 +646,7 @@ public interface IBanCommon<TUnban> where TUnban : IUnbanCommon int Id { get; set; } Guid? PlayerUserId { get; set; } NpgsqlInet? Address { get; set; } - byte[]? HWId { get; set; } + TypedHwid? HWId { get; set; } DateTime BanTime { get; set; } DateTime? ExpirationTime { get; set; } string Reason { get; set; } @@ -627,6 +685,19 @@ public enum ServerBanExemptFlags /// Intended use is for users with shared connections. This should not be used as an alternative to <see cref="Datacenter"/>. /// </remarks> IP = 1 << 1, + + /// <summary> + /// Ban is an IP range that is only applied for first time joins. + /// </summary> + /// <remarks> + /// Intended for use with residential IP ranges that are often used maliciously. + /// </remarks> + BlacklistedRange = 1 << 2, + + /// <summary> + /// Represents having all possible exemption flags. + /// </summary> + All = int.MaxValue, // @formatter:on } @@ -660,7 +731,7 @@ public class ServerBan : IBanCommon<ServerUnban> /// <summary> /// Hardware ID of the banned player. /// </summary> - public byte[]? HWId { get; set; } + public TypedHwid? HWId { get; set; } /// <summary> /// The time when the ban was applied by an administrator. @@ -798,7 +869,7 @@ public class ConnectionLog public DateTime Time { get; set; } public IPAddress Address { get; set; } = null!; - public byte[]? HWId { get; set; } + public TypedHwid? HWId { get; set; } public ConnectionDenyReason? Denied { get; set; } @@ -815,6 +886,8 @@ public class ConnectionLog public List<ServerBanHit> BanHits { get; set; } = null!; public Server Server { get; set; } = null!; + + public float Trust { get; set; } } public enum ConnectionDenyReason : byte @@ -823,6 +896,13 @@ public enum ConnectionDenyReason : byte Whitelist = 1, Full = 2, Panic = 3, + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + * + * If baby jail is removed, please reserve this value for as long as can reasonably be done to prevent causing ambiguity in connection denial reasons. + * Reservation by commenting out the value is likely sufficient for this purpose, but may impact projects which depend on SS14 like SS14.Admin. + */ + BabyJail = 4, } public class ServerBanHit @@ -845,7 +925,7 @@ public sealed class ServerRoleBan : IBanCommon<ServerRoleUnban> public Guid? PlayerUserId { get; set; } [Required] public TimeSpan PlaytimeAtNote { get; set; } public NpgsqlInet? Address { get; set; } - public byte[]? HWId { get; set; } + public TypedHwid? HWId { get; set; } public DateTime BanTime { get; set; } @@ -1106,4 +1186,37 @@ public sealed class BanTemplate /// <seealso cref="ServerBan.Hidden"/> public bool Hidden { get; set; } } + + /// <summary> + /// A hardware ID value together with its <see cref="HwidType"/>. + /// </summary> + /// <seealso cref="ImmutableTypedHwid"/> + [Owned] + public sealed class TypedHwid + { + public byte[] Hwid { get; set; } = default!; + public HwidType Type { get; set; } + + [return: NotNullIfNotNull(nameof(immutable))] + public static implicit operator TypedHwid?(ImmutableTypedHwid? immutable) + { + if (immutable == null) + return null; + + return new TypedHwid + { + Hwid = immutable.Hwid.ToArray(), + Type = immutable.Type, + }; + } + + [return: NotNullIfNotNull(nameof(hwid))] + public static implicit operator ImmutableTypedHwid?(TypedHwid? hwid) + { + if (hwid == null) + return null; + + return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type); + } + } } diff --git a/Content.Server.Database/SnakeCaseNaming.cs b/Content.Server.Database/SnakeCaseNaming.cs index 27ce392cd50..3a67ffb9cd1 100644 --- a/Content.Server.Database/SnakeCaseNaming.cs +++ b/Content.Server.Database/SnakeCaseNaming.cs @@ -82,7 +82,7 @@ public ConventionSet ModifyConventions(ConventionSet conventionSet) } } - public class SnakeCaseConvention : + public partial class SnakeCaseConvention : IEntityTypeAddedConvention, IEntityTypeAnnotationChangedConvention, IPropertyAddedConvention, @@ -99,22 +99,27 @@ public SnakeCaseConvention() {} public static string RewriteName(string name) { - var regex = new Regex("[A-Z]+", RegexOptions.Compiled); - return regex.Replace( - name, - (Match match) => { - if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) { - return match.Value; + return UpperCaseLocator() + .Replace( + name, + (Match match) => { + if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) { + return match.Value; + } + if (match.Value == "HWI") + return (match.Index == 0 ? "" : "_") + "hwi"; + if (match.Index == 0) + return match.Value.ToLower(); + if (match.Length > 1) + return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}"; + + // Do not add a _ if there is already one before this. This happens with owned entities. + if (name[match.Index - 1] == '_') + return match.Value.ToLower(); + + return "_" + match.Value.ToLower(); } - if (match.Value == "HWI") - return (match.Index == 0 ? "" : "_") + "hwi"; - if (match.Index == 0) - return match.Value.ToLower(); - if (match.Length > 1) - return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}"; - return "_" + match.Value.ToLower(); - } - ); + ); } public virtual void ProcessEntityTypeAdded( @@ -332,5 +337,8 @@ private static void RewriteColumnName(IConventionPropertyBuilder propertyBuilder } } } + + [GeneratedRegex("[A-Z]+", RegexOptions.Compiled)] + private static partial Regex UpperCaseLocator(); } } diff --git a/Content.Server.Database/add-migration.ps1 b/Content.Server.Database/add-migration.ps1 index 7f44558eb7b..e8c7443c867 100755 --- a/Content.Server.Database/add-migration.ps1 +++ b/Content.Server.Database/add-migration.ps1 @@ -8,5 +8,5 @@ if ($name -eq "") exit } -dotnet ef migrations add --context SqliteServerDbContext -o Migrations/Sqlite $name -dotnet ef migrations add --context PostgresServerDbContext -o Migrations/Postgres $name +dotnet ef migrations add --context SqliteServerDbContext --output-dir Migrations/Sqlite $name +dotnet ef migrations add --context PostgresServerDbContext --output-dir Migrations/Postgres $name diff --git a/Content.Server.Database/remove-migration.ps1 b/Content.Server.Database/remove-migration.ps1 index 7d3df6fb498..364d7d65f32 100755 --- a/Content.Server.Database/remove-migration.ps1 +++ b/Content.Server.Database/remove-migration.ps1 @@ -8,5 +8,5 @@ if ($name -eq "") exit } -dotnet ef migrations remove --context SqliteServerDbContext -o Migrations/Sqlite $name -dotnet ef migrations remove --context PostgresServerDbContext -o Migrations/Postgres $name +dotnet ef migrations remove --context SqliteServerDbContext $name +dotnet ef migrations remove --context PostgresServerDbContext $name diff --git a/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs b/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs index 6a2e90dd88d..0c44a680ff1 100644 --- a/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs +++ b/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs @@ -88,8 +88,7 @@ private void AttemptDoAfter(EntityUid uid, PsionicComponent component, PsionicHe ev.DoRevive = args.DoRevive; var doAfterArgs = new DoAfterArgs(EntityManager, uid, args.UseDelay, ev, uid, target: args.Target) { - BreakOnUserMove = args.BreakOnUserMove, - BreakOnTargetMove = args.BreakOnTargetMove, + BreakOnMove = args.BreakOnMove, Hidden = _glimmer.Glimmer > args.GlimmerDoAfterVisibilityThreshold * args.ModifiedDampening, }; diff --git a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs index ff32809a5a4..e7efc80f59c 100644 --- a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs +++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs @@ -1,20 +1,20 @@ +using Content.Server.Chat.Managers; +using Content.Server.Ghost; +using Content.Server.Mind; +using Content.Server.NPC.HTN; using Content.Shared.Abilities.Psionics; using Content.Shared.Popups; using Content.Shared.Chat; using Content.Shared.Psionics; using Content.Shared.Random.Helpers; using Content.Shared.StatusEffect; -using Robust.Shared.Random; +using Robust.Shared.Configuration; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Player; -using Content.Server.Chat.Managers; -using Robust.Shared.Configuration; using Content.Shared.CCVar; -using Content.Server.NPC.Systems; -using Content.Server.NPC.HTN; -using Content.Server.Ghost; -using Content.Server.Mind; +using Content.Shared.NPC.Systems; namespace Content.Server.Abilities.Psionics; diff --git a/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs b/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs index d382c1f2318..6fbfbdb08cf 100644 --- a/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs +++ b/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs @@ -9,6 +9,9 @@ using Content.Shared.Mobs; using Robust.Shared.Map; using System.Numerics; +using Content.Shared.NPC.Components; +using NpcFactionSystem = Content.Shared.NPC.Systems.NpcFactionSystem; + namespace Content.Server.Abilities.Psionics; @@ -60,7 +63,7 @@ private void InheritFactions(EntityUid uid, EntityUid familiar, PsionicFamiliarC EnsureComp<NpcFactionMemberComponent>(familiar, out var familiarFactions); foreach (var faction in masterFactions.Factions) { - if (familiarFactions.Factions.Contains(faction)) + if (_factions.IsMember(familiar, faction)) continue; _factions.AddFaction(familiar, faction, true); diff --git a/Content.Server/Access/AddAccessLogCommand.cs b/Content.Server/Access/AddAccessLogCommand.cs index 8b3aebb16b4..f55a9b8f1eb 100644 --- a/Content.Server/Access/AddAccessLogCommand.cs +++ b/Content.Server/Access/AddAccessLogCommand.cs @@ -10,11 +10,7 @@ namespace Content.Server.Access; public sealed class AddAccessLogCommand : ToolshedCommand { [CommandImplementation] - public void AddAccessLog( - [CommandInvocationContext] IInvocationContext ctx, - [CommandArgument] EntityUid input, - [CommandArgument] float seconds, - [CommandArgument] ValueRef<string> accessor) + public void AddAccessLog(IInvocationContext ctx, EntityUid input, float seconds, string accessor) { var accessReader = EnsureComp<AccessReaderComponent>(input); @@ -23,19 +19,14 @@ public void AddAccessLog( ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!"); var accessTime = TimeSpan.FromSeconds(seconds); - var accessName = accessor.Evaluate(ctx)!; - accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessName)); + accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessor)); ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " + $"Time of access: {accessTime}\n " + - $"Accessed by: {accessName}"); + $"Accessed by: {accessor}"); } [CommandImplementation] - public void AddAccessLogPiped( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] float seconds, - [CommandArgument] ValueRef<string> accessor) + public void AddAccessLogPiped(IInvocationContext ctx, [PipedArgument] EntityUid input, float seconds, string accessor) { AddAccessLog(ctx, input, seconds, accessor); } diff --git a/Content.Server/Access/Components/AgentIDCardComponent.cs b/Content.Server/Access/Components/AgentIDCardComponent.cs index 4b92b43ea99..ab1a045ea9d 100644 --- a/Content.Server/Access/Components/AgentIDCardComponent.cs +++ b/Content.Server/Access/Components/AgentIDCardComponent.cs @@ -1,15 +1,7 @@ -using Content.Shared.StatusIcon; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; +namespace Content.Server.Access.Components; -namespace Content.Server.Access.Components -{ - [RegisterComponent] - public sealed partial class AgentIDCardComponent : Component - { - /// <summary> - /// Set of job icons that the agent ID card can show. - /// </summary> - [DataField("icons", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StatusIconPrototype>))] - public HashSet<string> Icons = new(); - } -} +/// <summary> +/// Allows an ID card to copy accesses from other IDs and to change the name, job title and job icon via an interface. +/// </summary> +[RegisterComponent] +public sealed partial class AgentIDCardComponent : Component { } diff --git a/Content.Server/Access/Systems/AccessOverriderSystem.cs b/Content.Server/Access/Systems/AccessOverriderSystem.cs index 4e7b796503f..c7b20513f42 100644 --- a/Content.Server/Access/Systems/AccessOverriderSystem.cs +++ b/Content.Server/Access/Systems/AccessOverriderSystem.cs @@ -57,8 +57,7 @@ private void AfterInteractOn(EntityUid uid, AccessOverriderComponent component, var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.DoAfter, new AccessOverriderDoAfterEvent(), uid, target: args.Target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = true, }; diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs index d5e9dc357dd..e10f31decf6 100644 --- a/Content.Server/Access/Systems/AgentIDCardSystem.cs +++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs @@ -9,6 +9,7 @@ using Robust.Shared.Prototypes; using Content.Shared.Roles; using System.Diagnostics.CodeAnalysis; +using Content.Shared.DeltaV.NanoChat; // DeltaV namespace Content.Server.Access.Systems { @@ -18,6 +19,7 @@ public sealed class AgentIDCardSystem : SharedAgentIdCardSystem [Dependency] private readonly IdCardSystem _cardSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; // DeltaV public override void Initialize() { @@ -28,6 +30,17 @@ public override void Initialize() SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNameChangedMessage>(OnNameChanged); SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobChangedMessage>(OnJobChanged); SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobIconChangedMessage>(OnJobIconChanged); + SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNumberChangedMessage>(OnNumberChanged); // DeltaV + } + + // DeltaV - Add number change handler + private void OnNumberChanged(Entity<AgentIDCardComponent> ent, ref AgentIDCardNumberChangedMessage args) + { + if (!TryComp<NanoChatCardComponent>(ent, out var comp)) + return; + + _nanoChat.SetNumber((ent, comp), args.Number); + Dirty(ent, comp); } private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args) @@ -42,6 +55,34 @@ private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, Afte access.Tags.UnionWith(targetAccess.Tags); var addedLength = access.Tags.Count - beforeLength; + // DeltaV - Copy NanoChat data if available + if (TryComp<NanoChatCardComponent>(args.Target, out var targetNanoChat) && + TryComp<NanoChatCardComponent>(uid, out var agentNanoChat)) + { + // First clear existing data + _nanoChat.Clear((uid, agentNanoChat)); + + // Copy the number + if (_nanoChat.GetNumber((args.Target.Value, targetNanoChat)) is { } number) + _nanoChat.SetNumber((uid, agentNanoChat), number); + + // Copy all recipients and their messages + foreach (var (recipientNumber, recipient) in _nanoChat.GetRecipients((args.Target.Value, targetNanoChat))) + { + _nanoChat.SetRecipient((uid, agentNanoChat), recipientNumber, recipient); + + if (_nanoChat.GetMessagesForRecipient((args.Target.Value, targetNanoChat), recipientNumber) is not + { } messages) + continue; + + foreach (var message in messages) + { + _nanoChat.AddMessage((uid, agentNanoChat), recipientNumber, message); + } + } + } + // End DeltaV + if (addedLength == 0) { _popupSystem.PopupEntity(Loc.GetString("agent-id-no-new", ("card", args.Target)), args.Target.Value, args.User); @@ -67,7 +108,17 @@ private void AfterUIOpen(EntityUid uid, AgentIDCardComponent component, AfterAct if (!TryComp<IdCardComponent>(uid, out var idCard)) return; - var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon ?? "", component.Icons); + // DeltaV - Get current number if it exists + uint? currentNumber = null; + if (TryComp<NanoChatCardComponent>(uid, out var comp)) + currentNumber = comp.Number; + + var state = new AgentIDCardBoundUserInterfaceState( + idCard.FullName ?? "", + idCard.LocalizedJobTitle ?? "", + idCard.JobIcon, + currentNumber); // DeltaV - Pass current number + _uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state); } @@ -90,14 +141,10 @@ private void OnNameChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCard private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardJobIconChangedMessage args) { if (!TryComp<IdCardComponent>(uid, out var idCard)) - { return; - } - if (!_prototypeManager.TryIndex<StatusIconPrototype>(args.JobIconId, out var jobIcon)) - { + if (!_prototypeManager.TryIndex(args.JobIconId, out var jobIcon)) return; - } _cardSystem.TryChangeJobIcon(uid, jobIcon, idCard); @@ -105,11 +152,11 @@ private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDC _cardSystem.TryChangeJobDepartment(uid, job, idCard); } - private bool TryFindJobProtoFromIcon(StatusIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job) + private bool TryFindJobProtoFromIcon(JobIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job) { foreach (var jobPrototype in _prototypeManager.EnumeratePrototypes<JobPrototype>()) { - if(jobPrototype.Icon == jobIcon.ID) + if (jobPrototype.Icon == jobIcon.ID) { job = jobPrototype; return true; diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs index e680b0c6f40..7a0422de9eb 100644 --- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs +++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs @@ -96,7 +96,7 @@ private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component PrivilegedIdIsAuthorized(uid, component), true, targetIdComponent.FullName, - targetIdComponent.JobTitle, + targetIdComponent.LocalizedJobTitle, targetAccessComponent.Tags.ToList(), possibleAccess, jobProto, @@ -129,7 +129,7 @@ private void TryWriteToTargetId(EntityUid uid, _idCard.TryChangeJobTitle(targetId, newJobTitle, player: player); if (_prototype.TryIndex<JobPrototype>(newJobProto, out var job) - && _prototype.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon)) + && _prototype.TryIndex(job.Icon, out var jobIcon)) { _idCard.TryChangeJobIcon(targetId, jobIcon, player: player); _idCard.TryChangeJobDepartment(targetId, job); diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index 9cd9976cea9..aeb4cc163fb 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Popups; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Content.Server.Kitchen.EntitySystems; namespace Content.Server.Access.Systems; @@ -18,18 +19,24 @@ public sealed class IdCardSystem : SharedIdCardSystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly MicrowaveSystem _microwave = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent<IdCardComponent, BeingMicrowavedEvent>(OnMicrowaved); } private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args) { + if (!component.CanMicrowave || !TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken) + return; + if (TryComp<AccessComponent>(uid, out var access)) { float randomPick = _random.NextFloat(); + // if really unlucky, burn card if (randomPick <= 0.15f) { @@ -46,6 +53,14 @@ private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowa EntityManager.QueueDeleteEntity(uid); return; } + + //Explode if the microwave can't handle it + if (!micro.CanMicrowaveIdsSafely) + { + _microwave.Explode((args.Microwave, micro)); + return; + } + // If they're unlucky, brick their ID if (randomPick <= 0.25f) { @@ -70,6 +85,7 @@ private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowa _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}"); + } } } diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs index 696b7a1dcfd..3896d9f8792 100644 --- a/Content.Server/Access/Systems/PresetIdCardSystem.cs +++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs @@ -82,9 +82,7 @@ private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extende _cardSystem.TryChangeJobTitle(uid, job.LocalizedName); _cardSystem.TryChangeJobDepartment(uid, job); - if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon)) - { + if (_prototypeManager.TryIndex(job.Icon, out var jobIcon)) _cardSystem.TryChangeJobIcon(uid, jobIcon); - } } } diff --git a/Content.Server/Actions/ActionOnInteractSystem.cs b/Content.Server/Actions/ActionOnInteractSystem.cs index 28685858592..af9b0b1ddbd 100644 --- a/Content.Server/Actions/ActionOnInteractSystem.cs +++ b/Content.Server/Actions/ActionOnInteractSystem.cs @@ -55,12 +55,6 @@ private void OnActivate(EntityUid uid, ActionOnInteractComponent component, Acti return; var (actId, act) = _random.Pick(options); - if (act.Event != null) - { - act.Event.Performer = args.User; - act.Event.Action = actId; - } - _actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false); args.Handled = true; } @@ -94,8 +88,6 @@ private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, var (entActId, entAct) = _random.Pick(entOptions); if (entAct.Event != null) { - entAct.Event.Performer = args.User; - entAct.Event.Action = entActId; entAct.Event.Target = args.Target.Value; } @@ -105,6 +97,29 @@ private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, } } + // Then EntityWorld target actions + var entWorldOptions = GetValidActions<EntityWorldTargetActionComponent>(actionEnts, args.CanReach); + for (var i = entWorldOptions.Count - 1; i >= 0; i--) + { + var action = entWorldOptions[i]; + if (!_actions.ValidateEntityWorldTarget(args.User, args.Target, args.ClickLocation, action)) + entWorldOptions.RemoveAt(i); + } + + if (entWorldOptions.Count > 0) + { + var (entActId, entAct) = _random.Pick(entWorldOptions); + if (entAct.Event != null) + { + entAct.Event.Entity = args.Target; + entAct.Event.Coords = args.ClickLocation; + } + + _actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false); + args.Handled = true; + return; + } + // else: try world target actions var options = GetValidActions<WorldTargetActionComponent>(component.ActionEntities, args.CanReach); for (var i = options.Count - 1; i >= 0; i--) @@ -120,8 +135,6 @@ private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, var (actId, act) = _random.Pick(options); if (act.Event != null) { - act.Event.Performer = args.User; - act.Event.Action = actId; act.Event.Target = args.ClickLocation; } @@ -129,21 +142,6 @@ private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, args.Handled = true; } - private bool ValidAction(BaseActionComponent action, bool canReach = true) - { - if (!action.Enabled) - return false; - - if (action.Charges.HasValue && action.Charges <= 0) - return false; - - var curTime = _timing.CurTime; - if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime) - return false; - - return canReach || action is BaseTargetActionComponent { CheckCanAccess: false }; - } - private List<(EntityUid Id, T Comp)> GetValidActions<T>(List<EntityUid>? actions, bool canReach = true) where T : BaseActionComponent { var valid = new List<(EntityUid Id, T Comp)>(); @@ -155,7 +153,7 @@ private bool ValidAction(BaseActionComponent action, bool canReach = true) { if (!_actions.TryGetActionData(id, out var baseAction) || baseAction as T is not { } action || - !ValidAction(action, canReach)) + !_actions.ValidAction(action, canReach)) { continue; } diff --git a/Content.Server/Administration/BanList/BanListEui.cs b/Content.Server/Administration/BanList/BanListEui.cs index 8ddc7459d7b..2ca126bf164 100644 --- a/Content.Server/Administration/BanList/BanListEui.cs +++ b/Content.Server/Administration/BanList/BanListEui.cs @@ -54,7 +54,7 @@ private void OnPermsChanged(AdminPermsChangedEventArgs args) private async Task LoadBans(NetUserId userId) { - foreach (var ban in await _db.GetServerBansAsync(null, userId, null)) + foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null)) { SharedServerUnban? unban = null; if (ban.Unban is { } unbanDef) @@ -74,7 +74,7 @@ private async Task LoadBans(NetUserId userId) ? (address.address.ToString(), address.cidrMask) : null; - hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()); + hwid = ban.HWId?.ToString(); } Bans.Add(new SharedServerBan( @@ -95,7 +95,7 @@ private async Task LoadBans(NetUserId userId) private async Task LoadRoleBans(NetUserId userId) { - foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null)) + foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null)) { SharedServerUnban? unban = null; if (ban.Unban is { } unbanDef) @@ -115,7 +115,7 @@ private async Task LoadRoleBans(NetUserId userId) ? (address.address.ToString(), address.cidrMask) : null; - hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()); + hwid = ban.HWId?.ToString(); } RoleBans.Add(new SharedServerRoleBan( ban.Id, diff --git a/Content.Server/Administration/BanPanelEui.cs b/Content.Server/Administration/BanPanelEui.cs index aa6bd8d4bfa..3e336932088 100644 --- a/Content.Server/Administration/BanPanelEui.cs +++ b/Content.Server/Administration/BanPanelEui.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.Net; using System.Net.Sockets; using Content.Server.Administration.Managers; @@ -28,7 +27,7 @@ public sealed class BanPanelEui : BaseEui private NetUserId? PlayerId { get; set; } private string PlayerName { get; set; } = string.Empty; private IPAddress? LastAddress { get; set; } - private ImmutableArray<byte>? LastHwid { get; set; } + private ImmutableTypedHwid? LastHwid { get; set; } private const int Ipv4_CIDR = 32; private const int Ipv6_CIDR = 64; @@ -52,7 +51,7 @@ public override void HandleMessage(EuiMessageBase msg) switch (msg) { case BanPanelEuiStateMsg.CreateBanRequest r: - BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase); + BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase); break; case BanPanelEuiStateMsg.GetPlayerInfoRequest r: ChangePlayer(r.PlayerUsername); @@ -60,7 +59,7 @@ public override void HandleMessage(EuiMessageBase msg) } } - private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase) + private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase) { if (!_admins.HasAdminFlag(Player, AdminFlags.Ban)) { @@ -157,7 +156,7 @@ public async void ChangePlayer(string playerNameOrId) ChangePlayer(located?.UserId, located?.Username ?? string.Empty, located?.LastAddress, located?.LastHWId); } - public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableArray<byte>? lastHwid) + public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableTypedHwid? lastHwid) { PlayerId = playerId; PlayerName = playerName; diff --git a/Content.Server/Administration/Commands/BabyJailCommand.cs b/Content.Server/Administration/Commands/BabyJailCommand.cs new file mode 100644 index 00000000000..45687080d48 --- /dev/null +++ b/Content.Server/Administration/Commands/BabyJailCommand.cs @@ -0,0 +1,139 @@ +using Content.Shared.Administration; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.Console; + +/* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Server)] +public sealed class BabyJailCommand : LocalizedCommands +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override string Command => "babyjail"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + var toggle = Toggle(CCVars.BabyJailEnabled, shell, args, _cfg); + if (toggle == null) + return; + + shell.WriteLine(Loc.GetString(toggle.Value ? "babyjail-command-enabled" : "babyjail-command-disabled")); + } + + public static bool? Toggle(CVarDef<bool> cvar, IConsoleShell shell, string[] args, IConfigurationManager config) + { + if (args.Length > 1) + { + shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1))); + return null; + } + + var enabled = config.GetCVar(cvar); + + switch (args.Length) + { + case 0: + enabled = !enabled; + break; + case 1 when !bool.TryParse(args[0], out enabled): + shell.WriteError(Loc.GetString("shell-argument-must-be-boolean")); + return null; + } + + config.SetCVar(cvar, enabled); + + return enabled; + } +} + + +[AdminCommand(AdminFlags.Server)] +public sealed class BabyJailShowReasonCommand : LocalizedCommands +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override string Command => "babyjail_show_reason"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + var toggle = BabyJailCommand.Toggle(CCVars.BabyJailShowReason, shell, args, _cfg); + if (toggle == null) + return; + + shell.WriteLine(Loc.GetString(toggle.Value + ? "babyjail-command-show-reason-enabled" + : "babyjail-command-show-reason-disabled" + )); + } +} + +[AdminCommand(AdminFlags.Server)] +public sealed class BabyJailMinAccountAgeCommand : LocalizedCommands +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override string Command => "babyjail_max_account_age"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + switch (args.Length) + { + case 0: + { + var current = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge); + shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-is", ("hours", current))); + break; + } + case > 1: + shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1))); + return; + } + + if (!int.TryParse(args[0], out var hours)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + _cfg.SetCVar(CCVars.BabyJailMaxAccountAge, hours); + shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-set", ("hours", hours))); + } +} + +[AdminCommand(AdminFlags.Server)] +public sealed class BabyJailMinOverallHoursCommand : LocalizedCommands +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override string Command => "babyjail_max_overall_hours"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + switch (args.Length) + { + case 0: + { + var current = _cfg.GetCVar(CCVars.BabyJailMaxOverallHours); + shell.WriteLine(Loc.GetString("babyjail-command-max-overall-hours-is", ("hours", current))); + break; + } + case > 1: + shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1))); + return; + } + + if (!int.TryParse(args[0], out var hours)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + _cfg.SetCVar(CCVars.BabyJailMaxOverallHours, hours); + shell.WriteLine(Loc.GetString("babyjail-command-overall-hours-set", ("hours", hours))); + } +} diff --git a/Content.Server/Administration/Commands/BanListCommand.cs b/Content.Server/Administration/Commands/BanListCommand.cs index a5bc97dce3e..2f7093ae1d8 100644 --- a/Content.Server/Administration/Commands/BanListCommand.cs +++ b/Content.Server/Administration/Commands/BanListCommand.cs @@ -38,7 +38,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] if (shell.Player is not { } player) { - var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastHWId, false); + var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); if (bans.Count == 0) { diff --git a/Content.Server/Administration/Commands/ExplosionCommand.cs b/Content.Server/Administration/Commands/ExplosionCommand.cs index 56ed78b2e2e..81daca59c48 100644 --- a/Content.Server/Administration/Commands/ExplosionCommand.cs +++ b/Content.Server/Administration/Commands/ExplosionCommand.cs @@ -8,6 +8,7 @@ using Robust.Shared.Prototypes; using System.Linq; using System.Numerics; +using Robust.Server.GameObjects; namespace Content.Server.Administration.Commands; @@ -105,7 +106,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length > 4) coords = new MapCoordinates(new Vector2(x, y), xform.MapID); else - coords = xform.MapPosition; + coords = entMan.System<TransformSystem>().GetMapCoordinates(shell.Player.AttachedEntity.Value, xform: xform); } ExplosionPrototype? type; diff --git a/Content.Server/Administration/Commands/PanicBunkerCommand.cs b/Content.Server/Administration/Commands/PanicBunkerCommand.cs index de3f3cbaea2..19271edf982 100644 --- a/Content.Server/Administration/Commands/PanicBunkerCommand.cs +++ b/Content.Server/Administration/Commands/PanicBunkerCommand.cs @@ -171,7 +171,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length == 0) { var current = _cfg.GetCVar(CCVars.PanicBunkerMinOverallHours); - shell.WriteLine(Loc.GetString("panicbunker-command-min-overall-hours-is", ("minutes", current))); + shell.WriteLine(Loc.GetString("panicbunker-command-min-overall-hours-is", ("hours", current))); } if (args.Length > 1) diff --git a/Content.Server/Administration/Commands/PlayerPanelCommand.cs b/Content.Server/Administration/Commands/PlayerPanelCommand.cs new file mode 100644 index 00000000000..4a065bd58a0 --- /dev/null +++ b/Content.Server/Administration/Commands/PlayerPanelCommand.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Content.Server.EUI; +using Content.Shared.Administration; +using Robust.Server.Player; +using Robust.Shared.Console; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Admin)] +public sealed class PlayerPanelCommand : LocalizedCommands +{ + [Dependency] private readonly IPlayerLocator _locator = default!; + [Dependency] private readonly EuiManager _euis = default!; + [Dependency] private readonly IPlayerManager _players = default!; + + public override string Command => "playerpanel"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } admin) + { + shell.WriteError(Loc.GetString("cmd-playerpanel-server")); + return; + } + + if (args.Length != 1) + { + shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-arguments")); + return; + } + + var queriedPlayer = await _locator.LookupIdByNameOrIdAsync(args[0]); + + if (queriedPlayer == null) + { + shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-player")); + return; + } + + var ui = new PlayerPanelEui(queriedPlayer); + _euis.OpenEui(ui, admin); + ui.SetPlayerState(); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray(); + + return CompletionResult.FromHintOptions(options, LocalizationManager.GetString("cmd-playerpanel-completion")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Administration/Commands/RoleBanListCommand.cs b/Content.Server/Administration/Commands/RoleBanListCommand.cs index a5904723390..c87a0b3b544 100644 --- a/Content.Server/Administration/Commands/RoleBanListCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanListCommand.cs @@ -42,10 +42,9 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) } var targetUid = located.UserId; - var targetHWid = located.LastHWId; var targetAddress = located.LastAddress; - var bans = await dbMan.GetServerRoleBansAsync(targetAddress, targetUid, targetHWid, includeUnbanned); + var bans = await dbMan.GetServerRoleBansAsync(targetAddress, targetUid, located.LastLegacyHWId, located.LastModernHWIds, includeUnbanned); if (bans.Count == 0) { diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index 2b74a6d5ac5..088cbfeaf4c 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -234,7 +234,6 @@ public void Initialize() _sawmill = _logManager.GetSawmill("admin"); _netMgr.RegisterNetMessage<MsgUpdateAdminStatus>(); - _netMgr.RegisterNetMessage<ShowRulesPopupMessage>(); // Cache permissions for loaded console commands with the requisite attributes. foreach (var (cmdName, cmd) in _consoleHost.AvailableCommands) diff --git a/Content.Server/Administration/Managers/BanManager.Notification.cs b/Content.Server/Administration/Managers/BanManager.Notification.cs new file mode 100644 index 00000000000..e9bfa628841 --- /dev/null +++ b/Content.Server/Administration/Managers/BanManager.Notification.cs @@ -0,0 +1,123 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Content.Server.Database; + +namespace Content.Server.Administration.Managers; + +public sealed partial class BanManager +{ + // Responsible for ban notification handling. + // Ban notifications are sent through the database to notify the entire server group that a new ban has been added, + // so that people will get kicked if they are banned on a different server than the one that placed the ban. + // + // Ban notifications are currently sent by a trigger in the database, automatically. + + /// <summary> + /// The notification channel used to broadcast information about new bans. + /// </summary> + public const string BanNotificationChannel = "ban_notification"; + + // Rate limit to avoid undue load from mass-ban imports. + // Only process 10 bans per 30 second interval. + // + // I had the idea of maybe binning this by postgres transaction ID, + // to avoid any possibility of dropping a normal ban by coincidence. + // Didn't bother implementing this though. + private static readonly TimeSpan BanNotificationRateLimitTime = TimeSpan.FromSeconds(30); + private const int BanNotificationRateLimitCount = 10; + + private readonly object _banNotificationRateLimitStateLock = new(); + private TimeSpan _banNotificationRateLimitStart; + private int _banNotificationRateLimitCount; + + private void OnDatabaseNotification(DatabaseNotification notification) + { + if (notification.Channel != BanNotificationChannel) + return; + + if (notification.Payload == null) + { + _sawmill.Error("Got ban notification with null payload!"); + return; + } + + BanNotificationData data; + try + { + data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload) + ?? throw new JsonException("Content is null"); + } + catch (JsonException e) + { + _sawmill.Error($"Got invalid JSON in ban notification: {e}"); + return; + } + + if (!CheckBanRateLimit()) + { + _sawmill.Verbose("Not processing ban notification due to rate limit"); + return; + } + + _taskManager.RunOnMainThread(() => ProcessBanNotification(data)); + } + + private async void ProcessBanNotification(BanNotificationData data) + { + if ((await _entryManager.ServerEntity).Id == data.ServerId) + { + _sawmill.Verbose("Not processing ban notification: came from this server"); + return; + } + + _sawmill.Verbose($"Processing ban notification for ban {data.BanId}"); + var ban = await _db.GetServerBanAsync(data.BanId); + if (ban == null) + { + _sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?"); + return; + } + + KickMatchingConnectedPlayers(ban, "ban notification"); + } + + private bool CheckBanRateLimit() + { + lock (_banNotificationRateLimitStateLock) + { + var now = _gameTiming.RealTime; + if (_banNotificationRateLimitStart + BanNotificationRateLimitTime < now) + { + // Rate limit period expired, restart it. + _banNotificationRateLimitCount = 1; + _banNotificationRateLimitStart = now; + return true; + } + + _banNotificationRateLimitCount += 1; + return _banNotificationRateLimitCount <= BanNotificationRateLimitCount; + } + } + + /// <summary> + /// Data sent along the notification channel for a single ban notification. + /// </summary> + private sealed class BanNotificationData + { + /// <summary> + /// The ID of the new ban object in the database to check. + /// </summary> + [JsonRequired, JsonPropertyName("ban_id")] + public int BanId { get; init; } + + /// <summary> + /// The id of the server the ban was made on. + /// This is used to avoid double work checking the ban on the originating server. + /// </summary> + /// <remarks> + /// This is optional in case the ban was made outside a server (SS14.Admin) + /// </remarks> + [JsonPropertyName("server_id")] + public int? ServerId { get; init; } + } +} diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs index 68bd8170265..d36bcd29d03 100644 --- a/Content.Server/Administration/Managers/BanManager.cs +++ b/Content.Server/Administration/Managers/BanManager.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; using Content.Server.Chat.Managers; using Content.Server.Database; @@ -12,16 +13,18 @@ using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Roles; using Robust.Server.Player; +using Robust.Shared.Asynchronous; +using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; +using Robust.Shared.Timing; namespace Content.Server.Administration.Managers; -public sealed class BanManager : IBanManager, IPostInjectInit +public sealed partial class BanManager : IBanManager, IPostInjectInit { [Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; @@ -29,43 +32,66 @@ public sealed class BanManager : IBanManager, IPostInjectInit [Dependency] private readonly IEntitySystemManager _systems = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly ILocalizationManager _localizationManager = default!; + [Dependency] private readonly ServerDbEntryManager _entryManager = default!; [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ITaskManager _taskManager = default!; + [Dependency] private readonly UserDbDataManager _userDbData = default!; private ISawmill _sawmill = default!; public const string SawmillId = "admin.bans"; public const string JobPrefix = "Job:"; - private readonly Dictionary<NetUserId, HashSet<ServerRoleBanDef>> _cachedRoleBans = new(); + private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new(); + // Cached ban exemption flags are used to handle + private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new(); public void Initialize() { - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - _netManager.RegisterNetMessage<MsgRoleBans>(); + + _db.SubscribeToNotifications(OnDatabaseNotification); + + _userDbData.AddOnLoadPlayer(CachePlayerData); + _userDbData.AddOnPlayerDisconnect(ClearPlayerData); } - private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + private async Task CachePlayerData(ICommonSession player, CancellationToken cancel) { - if (e.NewStatus != SessionStatus.Connected || _cachedRoleBans.ContainsKey(e.Session.UserId)) - return; + var flags = await _db.GetBanExemption(player.UserId, cancel); - var netChannel = e.Session.Channel; + var netChannel = player.Channel; ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId; - await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, hwId); + var modernHwids = netChannel.UserData.ModernHWIds; + var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false); + + var userRoleBans = new List<ServerRoleBanDef>(); + foreach (var ban in roleBans) + userRoleBans.Add(ban); + cancel.ThrowIfCancellationRequested(); + _cachedBanExemptions[player] = flags; + _cachedRoleBans[player] = userRoleBans; - SendRoleBans(e.Session); + SendRoleBans(player); + } + + private void ClearPlayerData(ICommonSession player) + { + _cachedBanExemptions.Remove(player); } private async Task<bool> AddRoleBan(ServerRoleBanDef banDef) { banDef = await _db.AddServerRoleBanAsync(banDef); - if (banDef.UserId != null) + if (banDef.UserId != null + && _playerManager.TryGetSessionById(banDef.UserId, out var player) + && _cachedRoleBans.TryGetValue(player, out var cachedBans)) { - _cachedRoleBans.GetOrNew(banDef.UserId.Value).Add(banDef); + cachedBans.Add(banDef); } return true; @@ -73,31 +99,21 @@ private async Task<bool> AddRoleBan(ServerRoleBanDef banDef) public HashSet<string>? GetRoleBans(NetUserId playerUserId) { - return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans) + if (!_playerManager.TryGetSessionById(playerUserId, out var session)) + return null; + + return _cachedRoleBans.TryGetValue(session, out var roleBans) ? roleBans.Select(banDef => banDef.Role).ToHashSet() : null; } - private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null) - { - var roleBans = await _db.GetServerRoleBansAsync(address, userId, hwId, false); - - var userRoleBans = new HashSet<ServerRoleBanDef>(); - foreach (var ban in roleBans) - { - userRoleBans.Add(ban); - } - - _cachedRoleBans[userId] = userRoleBans; - } - public void Restart() { // Clear out players that have disconnected. - var toRemove = new List<NetUserId>(); + var toRemove = new ValueList<ICommonSession>(); foreach (var player in _cachedRoleBans.Keys) { - if (!_playerManager.TryGetSessionById(player, out _)) + if (player.Status == SessionStatus.Disconnected) toRemove.Add(player); } @@ -109,12 +125,12 @@ public void Restart() // Check for expired bans foreach (var roleBans in _cachedRoleBans.Values) { - roleBans.RemoveWhere(ban => DateTimeOffset.Now > ban.ExpirationTime); + roleBans.RemoveAll(ban => DateTimeOffset.Now > ban.ExpirationTime); } } #region Server Bans - public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason) + public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason) { DateTimeOffset? expires = null; if (minutes > 0) @@ -148,9 +164,7 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net var addressRangeString = addressRange != null ? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}" : "null"; - var hwidString = hwid != null - ? string.Concat(hwid.Value.Select(x => x.ToString("x2"))) - : "null"; + var hwidString = hwid?.ToString() ?? "null"; var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}"; var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii"; @@ -168,23 +182,50 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net _sawmill.Info(logMessage); _chat.SendAdminAlert(logMessage); - // If we're not banning a player we don't care about disconnecting people - if (target == null) - return; + KickMatchingConnectedPlayers(banDef, "newly placed ban"); + } - // Is the player connected? - if (!_playerManager.TryGetSessionById(target.Value, out var targetPlayer)) - return; - // If they are, kick them - var message = banDef.FormatBanMessage(_cfg, _localizationManager); - targetPlayer.Channel.Disconnect(message); + private void KickMatchingConnectedPlayers(ServerBanDef def, string source) + { + foreach (var player in _playerManager.Sessions) + { + if (BanMatchesPlayer(player, def)) + { + KickForBanDef(player, def); + _sawmill.Info($"Kicked player {player.Name} ({player.UserId}) through {source}"); + } + } + } + + private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) + { + var playerInfo = new BanMatcher.PlayerInfo + { + UserId = player.UserId, + Address = player.Channel.RemoteEndPoint.Address, + HWId = player.Channel.UserData.HWId, + ModernHWIds = player.Channel.UserData.ModernHWIds, + // It's possible for the player to not have cached data loading yet due to coincidental timing. + // If this is the case, we assume they have all flags to avoid false-positives. + ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All), + IsNewPlayer = false, + }; + + return BanMatcher.BanMatches(ban, playerInfo); + } + + private void KickForBanDef(ICommonSession player, ServerBanDef def) + { + var message = def.FormatBanMessage(_cfg, _localizationManager); + player.Channel.Disconnect(message); } + #endregion #region Job Bans // If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin. // Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset. - public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan) + public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan) { if (!_prototypeManager.TryIndex(role, out JobPrototype? _)) { @@ -226,9 +267,9 @@ public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUs var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires)); _chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length))); - if (target != null) + if (target != null && _playerManager.TryGetSessionById(target.Value, out var session)) { - SendRoleBans(target.Value); + SendRoleBans(session); } } @@ -256,10 +297,12 @@ public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, Da await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); - if (ban.UserId is { } player && _cachedRoleBans.TryGetValue(player, out var roleBans)) + if (ban.UserId is { } player + && _playerManager.TryGetSessionById(player, out var session) + && _cachedRoleBans.TryGetValue(session, out var roleBans)) { - roleBans.RemoveWhere(roleBan => roleBan.Id == ban.Id); - SendRoleBans(player); + roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); + SendRoleBans(session); } return $"Pardoned ban with id {banId}"; @@ -267,8 +310,12 @@ public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, Da public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId) { - if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans)) + if (!_playerManager.TryGetSessionById(playerUserId, out var session)) + return null; + + if (!_cachedRoleBans.TryGetValue(session, out var roleBans)) return null; + return roleBans .Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal)) .Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..])) @@ -276,19 +323,9 @@ public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, Da } #endregion - public void SendRoleBans(NetUserId userId) - { - if (!_playerManager.TryGetSessionById(userId, out var player)) - { - return; - } - - SendRoleBans(player); - } - public void SendRoleBans(ICommonSession pSession) { - var roleBans = _cachedRoleBans.GetValueOrDefault(pSession.UserId) ?? new HashSet<ServerRoleBanDef>(); + var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>(); var bans = new MsgRoleBans() { Bans = roleBans.Select(o => o.Role).ToList() diff --git a/Content.Server/Administration/Managers/IBanManager.cs b/Content.Server/Administration/Managers/IBanManager.cs index b60e0a25351..fc192cc3066 100644 --- a/Content.Server/Administration/Managers/IBanManager.cs +++ b/Content.Server/Administration/Managers/IBanManager.cs @@ -24,7 +24,7 @@ public interface IBanManager /// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param> /// <param name="severity">Severity of the resulting ban note</param> /// <param name="reason">Reason for the ban</param> - public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason); + public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason); public HashSet<string>? GetRoleBans(NetUserId playerUserId); public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId); @@ -37,7 +37,7 @@ public interface IBanManager /// <param name="reason">Reason for the ban</param> /// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param> /// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param> - public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan); + public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan); /// <summary> /// Pardons a role ban for the specified target, username or GUID @@ -47,12 +47,6 @@ public interface IBanManager /// <param name="unbanTime">The time at which this role ban was pardoned.</param> public Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime); - /// <summary> - /// Sends role bans to the target - /// </summary> - /// <param name="pSession">Player's user ID</param> - public void SendRoleBans(NetUserId userId); - /// <summary> /// Sends role bans to the target /// </summary> diff --git a/Content.Server/Administration/PlayerLocator.cs b/Content.Server/Administration/PlayerLocator.cs index 64a85f19ad0..25cc7714686 100644 --- a/Content.Server/Administration/PlayerLocator.cs +++ b/Content.Server/Administration/PlayerLocator.cs @@ -5,16 +5,42 @@ using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; +using Content.Server.Connection; using Content.Server.Database; +using Content.Shared.Database; using JetBrains.Annotations; using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Player; namespace Content.Server.Administration { - public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray<byte>? LastHWId, string Username); + /// <summary> + /// Contains data resolved via <see cref="IPlayerLocator"/>. + /// </summary> + /// <param name="UserId">The ID of the located user.</param> + /// <param name="LastAddress">The last known IP address that the user connected with.</param> + /// <param name="LastHWId"> + /// The last known HWID that the user connected with. + /// This should be used for placing new records involving HWIDs, such as bans. + /// For looking up data based on HWID, use combined <see cref="LastLegacyHWId"/> and <see cref="LastModernHWIds"/>. + /// </param> + /// <param name="Username">The last known username for the user connected with.</param> + /// <param name="LastLegacyHWId"> + /// The last known legacy HWID value this user connected with. Only use for old lookups! + /// </param> + /// <param name="LastModernHWIds"> + /// The set of last known modern HWIDs the user connected with. + /// </param> + public sealed record LocatedPlayerData( + NetUserId UserId, + IPAddress? LastAddress, + ImmutableTypedHwid? LastHWId, + string Username, + ImmutableArray<byte>? LastLegacyHWId, + ImmutableArray<ImmutableArray<byte>> LastModernHWIds); /// <summary> /// Utilities for finding user IDs that extend to more than the server database. @@ -67,63 +93,42 @@ public PlayerLocator() { // Check people currently on the server, the easiest case. if (_playerManager.TryGetSessionByUsername(playerName, out var session)) - { - var userId = session.UserId; - var address = session.Channel.RemoteEndPoint.Address; - var hwId = session.Channel.UserData.HWId; - return new LocatedPlayerData(userId, address, hwId, session.Name); - } + return ReturnForSession(session); // Check database for past players. var record = await _db.GetPlayerRecordByUserName(playerName, cancel); if (record != null) - return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName); + return ReturnForPlayerRecord(record); // If all else fails, ask the auth server. var authServer = _configurationManager.GetCVar(CVars.AuthServer); var requestUri = $"{authServer}api/query/name?name={WebUtility.UrlEncode(playerName)}"; using var resp = await _httpClient.GetAsync(requestUri, cancel); - if (resp.StatusCode == HttpStatusCode.NotFound) - return null; - - if (!resp.IsSuccessStatusCode) - { - _sawmill.Error("Auth server returned bad response {StatusCode}!", resp.StatusCode); - return null; - } - - var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel); - - if (responseData == null) - { - _sawmill.Error("Auth server returned null response!"); - return null; - } - - return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName); + return await HandleAuthServerResponse(resp, cancel); } public async Task<LocatedPlayerData?> LookupIdAsync(NetUserId userId, CancellationToken cancel = default) { // Check people currently on the server, the easiest case. if (_playerManager.TryGetSessionById(userId, out var session)) - { - var address = session.Channel.RemoteEndPoint.Address; - var hwId = session.Channel.UserData.HWId; - return new LocatedPlayerData(userId, address, hwId, session.Name); - } + return ReturnForSession(session); // Check database for past players. var record = await _db.GetPlayerRecordByUserId(userId, cancel); if (record != null) - return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName); + return ReturnForPlayerRecord(record); // If all else fails, ask the auth server. var authServer = _configurationManager.GetCVar(CVars.AuthServer); var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}"; using var resp = await _httpClient.GetAsync(requestUri, cancel); + return await HandleAuthServerResponse(resp, cancel); + } + + private async Task<LocatedPlayerData?> HandleAuthServerResponse(HttpResponseMessage resp, CancellationToken cancel) + { if (resp.StatusCode == HttpStatusCode.NotFound) return null; @@ -134,14 +139,40 @@ public PlayerLocator() } var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel); - if (responseData == null) { _sawmill.Error("Auth server returned null response!"); return null; } - return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName); + return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName, null, []); + } + + private static LocatedPlayerData ReturnForSession(ICommonSession session) + { + var userId = session.UserId; + var address = session.Channel.RemoteEndPoint.Address; + var hwId = session.Channel.UserData.GetModernHwid(); + return new LocatedPlayerData( + userId, + address, + hwId, + session.Name, + session.Channel.UserData.HWId, + session.Channel.UserData.ModernHWIds); + } + + private static LocatedPlayerData ReturnForPlayerRecord(PlayerRecord record) + { + var hwid = record.HWId; + + return new LocatedPlayerData( + record.UserId, + record.LastSeenAddress, + hwid, + record.LastSeenUserName, + hwid is { Type: HwidType.Legacy } ? hwid.Hwid : null, + hwid is { Type: HwidType.Modern } ? [hwid.Hwid] : []); } public async Task<LocatedPlayerData?> LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default) diff --git a/Content.Server/Administration/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs new file mode 100644 index 00000000000..6c304888866 --- /dev/null +++ b/Content.Server/Administration/PlayerPanelEui.cs @@ -0,0 +1,210 @@ +using System.Linq; +using Content.Server.Administration.Logs; +using Content.Server.Administration.Managers; +using Content.Server.Administration.Notes; +using Content.Server.Administration.Systems; +using Content.Server.Database; +using Content.Server.EUI; +using Content.Shared.Administration; +using Content.Shared.Database; +using Content.Shared.Eui; +using Robust.Server.Player; +using Robust.Shared.Player; + +namespace Content.Server.Administration; + +public sealed class PlayerPanelEui : BaseEui +{ + [Dependency] private readonly IAdminManager _admins = default!; + [Dependency] private readonly IServerDbManager _db = default!; + [Dependency] private readonly IAdminNotesManager _notesMan = default!; + [Dependency] private readonly IEntityManager _entity = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly EuiManager _eui = default!; + [Dependency] private readonly IAdminLogManager _adminLog = default!; + + private readonly LocatedPlayerData _targetPlayer; + private int? _notes; + private int? _bans; + private int? _roleBans; + private int _sharedConnections; + private bool? _whitelisted; + private TimeSpan _playtime; + private bool _frozen; + private bool _canFreeze; + private bool _canAhelp; + + public PlayerPanelEui(LocatedPlayerData player) + { + IoCManager.InjectDependencies(this); + _targetPlayer = player; + } + + public override void Opened() + { + base.Opened(); + _admins.OnPermsChanged += OnPermsChanged; + } + + public override void Closed() + { + base.Closed(); + _admins.OnPermsChanged -= OnPermsChanged; + } + + public override EuiStateBase GetNewState() + { + return new PlayerPanelEuiState(_targetPlayer.UserId, + _targetPlayer.Username, + _playtime, + _notes, + _bans, + _roleBans, + _sharedConnections, + _whitelisted, + _canFreeze, + _frozen, + _canAhelp); + } + + private void OnPermsChanged(AdminPermsChangedEventArgs args) + { + if (args.Player != Player) + return; + + SetPlayerState(); + } + + public override void HandleMessage(EuiMessageBase msg) + { + base.HandleMessage(msg); + + ICommonSession? session; + + switch (msg) + { + case PlayerPanelFreezeMessage freezeMsg: + if (!_admins.IsAdmin(Player) || + !_entity.TrySystem<AdminFrozenSystem>(out var frozenSystem) || + !_player.TryGetSessionById(_targetPlayer.UserId, out session) || + session.AttachedEntity == null) + return; + + if (_entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity)) + { + _adminLog.Add(LogType.Action,$"{Player:actor} unfroze {_entity.ToPrettyString(session.AttachedEntity):subject}"); + _entity.RemoveComponent<AdminFrozenComponent>(session.AttachedEntity.Value); + SetPlayerState(); + return; + } + + if (freezeMsg.Mute) + { + _adminLog.Add(LogType.Action,$"{Player:actor} froze and muted {_entity.ToPrettyString(session.AttachedEntity):subject}"); + frozenSystem.FreezeAndMute(session.AttachedEntity.Value); + } + else + { + _adminLog.Add(LogType.Action,$"{Player:actor} froze {_entity.ToPrettyString(session.AttachedEntity):subject}"); + _entity.EnsureComponent<AdminFrozenComponent>(session.AttachedEntity.Value); + } + SetPlayerState(); + break; + + case PlayerPanelLogsMessage: + if (!_admins.HasAdminFlag(Player, AdminFlags.Logs)) + return; + + _adminLog.Add(LogType.Action, $"{Player:actor} opened logs on {_targetPlayer.Username:subject}"); + var ui = new AdminLogsEui(); + _eui.OpenEui(ui, Player); + ui.SetLogFilter(search: _targetPlayer.Username); + break; + case PlayerPanelDeleteMessage: + case PlayerPanelRejuvenationMessage: + if (!_admins.HasAdminFlag(Player, AdminFlags.Debug) || + !_player.TryGetSessionById(_targetPlayer.UserId, out session) || + session.AttachedEntity == null) + return; + + if (msg is PlayerPanelRejuvenationMessage) + { + _adminLog.Add(LogType.Action,$"{Player:actor} rejuvenated {_entity.ToPrettyString(session.AttachedEntity):subject}"); + if (!_entity.TrySystem<RejuvenateSystem>(out var rejuvenate)) + return; + + rejuvenate.PerformRejuvenate(session.AttachedEntity.Value); + } + else + { + _adminLog.Add(LogType.Action,$"{Player:actor} deleted {_entity.ToPrettyString(session.AttachedEntity):subject}"); + _entity.DeleteEntity(session.AttachedEntity); + } + break; + } + } + + public async void SetPlayerState() + { + if (!_admins.IsAdmin(Player)) + { + Close(); + return; + } + + _playtime = (await _db.GetPlayTimes(_targetPlayer.UserId)) + .Where(p => p.Tracker == "Overall") + .Select(p => p.TimeSpent) + .FirstOrDefault(); + + if (_notesMan.CanView(Player)) + { + _notes = (await _notesMan.GetAllAdminRemarks(_targetPlayer.UserId)).Count; + } + else + { + _notes = null; + } + + _sharedConnections = _player.Sessions.Count(s => s.Channel.RemoteEndPoint.Address.Equals(_targetPlayer.LastAddress) && s.UserId != _targetPlayer.UserId); + + // Apparently the Bans flag is also used for whitelists + if (_admins.HasAdminFlag(Player, AdminFlags.Ban)) + { + _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId); + // This won't get associated ip or hwid bans but they were not placed on this account anyways + _bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count; + // Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally + // The only way to distinguish whether a role ban is the same is to compare the ban time. + // This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now. + _roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count(); + } + else + { + _whitelisted = null; + _bans = null; + _roleBans = null; + } + + if (_player.TryGetSessionById(_targetPlayer.UserId, out var session)) + { + _canFreeze = session.AttachedEntity != null; + _frozen = _entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity); + } + else + { + _canFreeze = false; + } + + if (_admins.HasAdminFlag(Player, AdminFlags.Adminhelp)) + { + _canAhelp = true; + } + else + { + _canAhelp = false; + } + + StateDirty(); + } +} diff --git a/Content.Server/Administration/Systems/AdminFrozenSystem.cs b/Content.Server/Administration/Systems/AdminFrozenSystem.cs new file mode 100644 index 00000000000..baf7b682b85 --- /dev/null +++ b/Content.Server/Administration/Systems/AdminFrozenSystem.cs @@ -0,0 +1,16 @@ +using Content.Shared.Administration; + +namespace Content.Server.Administration.Systems; + +public sealed class AdminFrozenSystem : SharedAdminFrozenSystem +{ + /// <summary> + /// Freezes and mutes the given entity. + /// </summary> + public void FreezeAndMute(EntityUid uid) + { + var comp = EnsureComp<AdminFrozenComponent>(uid); + comp.Muted = true; + Dirty(uid, comp); + } +} diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index 82bbe6e266b..8d6e2fa3749 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -62,6 +62,7 @@ public sealed class AdminSystem : EntitySystem private readonly HashSet<NetUserId> _roundActivePlayers = new(); public readonly PanicBunkerStatus PanicBunker = new(); + public readonly BabyJailStatus BabyJail = new(); public override void Initialize() { @@ -70,6 +71,7 @@ public override void Initialize() _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; _adminManager.OnPermsChanged += OnAdminPermsChanged; + // Panic Bunker Settings Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true); Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true); Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true); @@ -78,6 +80,17 @@ public override void Initialize() Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true); Subs.CVar(_config, CCVars.PanicBunkerMinOverallHours, OnPanicBunkerMinOverallHoursChanged, true); + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + + // Baby Jail Settings + Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true); + Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true); + Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true); + Subs.CVar(_config, CCVars.BabyJailMaxOverallHours, OnBabyJailMaxOverallHoursChanged, true); + Subs.CVar(_config, CCVars.PanicBunkerMinOverallHours, OnPanicBunkerMinOverallHoursChanged, true); + SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged); SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached); SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached); @@ -249,6 +262,17 @@ private void OnPanicBunkerChanged(bool enabled) SendPanicBunkerStatusAll(); } + private void OnBabyJailChanged(bool enabled) + { + BabyJail.Enabled = enabled; + _chat.SendAdminAlert(Loc.GetString(enabled + ? "admin-ui-baby-jail-enabled-admin-alert" + : "admin-ui-baby-jail-disabled-admin-alert" + )); + + SendBabyJailStatusAll(); + } + private void OnPanicBunkerDisableWithAdminsChanged(bool enabled) { PanicBunker.DisableWithAdmins = enabled; @@ -273,18 +297,36 @@ private void OnShowReasonChanged(bool enabled) SendPanicBunkerStatusAll(); } - private void OnPanicBunkerMinAccountAgeChanged(int minutes) + private void OnBabyJailShowReasonChanged(bool enabled) + { + BabyJail.ShowReason = enabled; + SendBabyJailStatusAll(); + } + + private void OnPanicBunkerMinAccountAgeChanged(int hours) { - PanicBunker.MinAccountAgeHours = minutes / 60; + PanicBunker.MinAccountAgeHours = hours; SendPanicBunkerStatusAll(); } + private void OnBabyJailMaxAccountAgeChanged(int hours) + { + BabyJail.MaxOverallHours = hours; + SendBabyJailStatusAll(); + } + private void OnPanicBunkerMinOverallHoursChanged(int hours) { PanicBunker.MinOverallHours = hours; SendPanicBunkerStatusAll(); } + private void OnBabyJailMaxOverallHoursChanged(int hours) + { + BabyJail.MaxAccountAgeHours = hours; + SendBabyJailStatusAll(); + } + private void UpdatePanicBunker() { var admins = PanicBunker.CountDeadminnedAdmins @@ -292,6 +334,19 @@ private void UpdatePanicBunker() : _adminManager.ActiveAdmins; var hasAdmins = admins.Any(); + // TODO Fix order dependent Cvars + // Please for the sake of my sanity don't make cvars & order dependent. + // Just make a bool field on the system instead of having some cvars automatically modify other cvars. + // + // I.e., this: + // /sudo cvar game.panic_bunker.enabled true + // /sudo cvar game.panic_bunker.disable_with_admins true + // and this: + // /sudo cvar game.panic_bunker.disable_with_admins true + // /sudo cvar game.panic_bunker.enabled true + // + // should have the same effect, but currently setting the disable_with_admins can modify enabled. + if (hasAdmins && PanicBunker.DisableWithAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, false); @@ -313,6 +368,15 @@ private void SendPanicBunkerStatusAll() } } + private void SendBabyJailStatusAll() + { + var ev = new BabyJailChangedEvent(BabyJail); + foreach (var admin in _adminManager.AllAdmins) + { + RaiseNetworkEvent(ev, admin); + } + } + /// <summary> /// Erases a player from the round. /// This removes them and any trace of them from the round, deleting their diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index bda60e9449a..2aaab767c6b 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -22,6 +22,7 @@ using Content.Shared.Administration.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Part; +using Content.Shared.Clumsy; using Content.Shared.Clothing.Components; using Content.Shared.Cluwne; using Content.Shared.Damage; @@ -100,7 +101,7 @@ private void AddSmiteVerbs(GetVerbsEvent<Verb> args) Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")), Act = () => { - var coords = Transform(args.Target).MapPosition; + var coords = _transformSystem.GetMapCoordinates(args.Target); Timer.Spawn(_gameTiming.TickPeriod, () => _explosionSystem.QueueExplosion(coords, ExplosionSystem.DefaultExplosionPrototypeId, 4, 1, 2, maxTileBreak: 0), // it gibs, damage doesn't need to be high. diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 328fa744846..3aa1c20cb02 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -205,6 +205,7 @@ private void AddTricksVerbs(GetVerbsEvent<Verb> args) var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target); recharger.AutoRecharge = true; recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill. + recharger.AutoRechargePause = false; // No delay. }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-infinite-battery-object-description"), @@ -604,6 +605,7 @@ private void AddTricksVerbs(GetVerbsEvent<Verb> args) recharger.AutoRecharge = true; recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill. + recharger.AutoRechargePause = false; // No delay. } }, Impact = LogImpact.Extreme, diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 5bb75b4c99c..1e701794ffc 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -35,6 +35,9 @@ using Robust.Shared.Utility; using System.Linq; using System.Numerics; +using Content.Server.Silicons.Laws; +using Content.Shared.Silicons.Laws.Components; +using Robust.Server.Player; using Robust.Shared.Physics.Components; using static Content.Shared.Configurable.ConfigurationComponent; @@ -67,6 +70,9 @@ public sealed partial class AdminVerbSystem : EntitySystem [Dependency] private readonly StationSystem _stations = default!; [Dependency] private readonly StationSpawningSystem _spawning = default!; [Dependency] private readonly ExamineSystemShared _examine = default!; + [Dependency] private readonly AdminFrozenSystem _freeze = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly SiliconLawSystem _siliconLawSystem = default!; private readonly Dictionary<ICommonSession, List<EditSolutionsEui>> _openSolutionUis = new(); @@ -131,24 +137,57 @@ private void AddAdminVerbs(GetVerbsEvent<Verb> args) args.Verbs.Add(prayerVerb); // Freeze - var frozen = HasComp<AdminFrozenComponent>(args.Target); - args.Verbs.Add(new Verb + var frozen = TryComp<AdminFrozenComponent>(args.Target, out var frozenComp); + var frozenAndMuted = frozenComp?.Muted ?? false; + + if (!frozen) { - Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze. - Text = frozen - ? Loc.GetString("admin-verbs-unfreeze") - : Loc.GetString("admin-verbs-freeze"), - Category = VerbCategory.Admin, - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")), - Act = () => + args.Verbs.Add(new Verb { - if (frozen) - RemComp<AdminFrozenComponent>(args.Target); - else + Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze. + Text = Loc.GetString("admin-verbs-freeze"), + Category = VerbCategory.Admin, + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")), + Act = () => + { EnsureComp<AdminFrozenComponent>(args.Target); - }, - Impact = LogImpact.Medium, - }); + }, + Impact = LogImpact.Medium, + }); + } + + if (!frozenAndMuted) + { + // allow you to additionally mute someone when they are already frozen + args.Verbs.Add(new Verb + { + Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze. + Text = Loc.GetString("admin-verbs-freeze-and-mute"), + Category = VerbCategory.Admin, + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")), + Act = () => + { + _freeze.FreezeAndMute(args.Target); + }, + Impact = LogImpact.Medium, + }); + } + + if (frozen) + { + args.Verbs.Add(new Verb + { + Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze. + Text = Loc.GetString("admin-verbs-unfreeze"), + Category = VerbCategory.Admin, + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")), + Act = () => + { + RemComp<AdminFrozenComponent>(args.Target); + }, + Impact = LogImpact.Medium, + }); + } // Erase args.Verbs.Add(new Verb @@ -227,6 +266,15 @@ private void AddAdminVerbs(GetVerbsEvent<Verb> args) ConfirmationPopup = true, Impact = LogImpact.High, }); + + // PlayerPanel + args.Verbs.Add(new Verb + { + Text = Loc.GetString("admin-player-actions-player-panel"), + Category = VerbCategory.Admin, + Act = () => _console.ExecuteCommand(player, $"playerpanel \"{targetActor.PlayerSession.UserId}\""), + Impact = LogImpact.Low + }); } // Admin Logs @@ -294,6 +342,25 @@ private void AddAdminVerbs(GetVerbsEvent<Verb> args) Impact = LogImpact.Low }); + if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent)) + { + args.Verbs.Add(new Verb() + { + Text = Loc.GetString("silicon-law-ui-verb"), + Category = VerbCategory.Admin, + Act = () => + { + var ui = new SiliconLawEui(_siliconLawSystem, EntityManager, _adminManager); + if (!_playerManager.TryGetSessionByEntity(args.User, out var session)) + { + return; + } + _euiManager.OpenEui(ui, session); + ui.UpdateLaws(lawBoundComponent, args.Target); + }, + Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"), + }); + } } } diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 21b5f6d3016..220e79f3f32 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -173,7 +173,7 @@ private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs } // Check if the user has been banned - var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null); + var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null); if (ban != null) { var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason)); diff --git a/Content.Server/Administration/Systems/SuperBonkSystem.cs b/Content.Server/Administration/Systems/SuperBonkSystem.cs index 5488a8d6f46..5cd62d83572 100644 --- a/Content.Server/Administration/Systems/SuperBonkSystem.cs +++ b/Content.Server/Administration/Systems/SuperBonkSystem.cs @@ -1,27 +1,27 @@ using Content.Server.Administration.Components; using Content.Shared.Climbing.Components; -using Content.Shared.Climbing.Events; -using Content.Shared.Climbing.Systems; -using Content.Shared.Interaction.Components; +using Content.Shared.Clumsy; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; +using Robust.Shared.Audio.Systems; namespace Content.Server.Administration.Systems; -public sealed class SuperBonkSystem: EntitySystem +public sealed class SuperBonkSystem : EntitySystem { [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly BonkSystem _bonkSystem = default!; + [Dependency] private readonly ClumsySystem _clumsySystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown); SubscribeLocalEvent<SuperBonkComponent, MobStateChangedEvent>(OnMobStateChanged); + SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown); } - public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false ) + public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false) { //The other check in the code to stop when the target dies does not work if the target is already dead. @@ -31,7 +31,6 @@ public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDe return; } - var hadClumsy = EnsureComp<ClumsyComponent>(target, out _); var tables = EntityQueryEnumerator<BonkableComponent>(); @@ -79,16 +78,17 @@ public override void Update(float frameTime) private void Bonk(SuperBonkComponent comp) { var uid = comp.Tables.Current.Key; - var bonkComp = comp.Tables.Current.Value; // It would be very weird for something without a transform component to have a bonk component // but just in case because I don't want to crash the server. - if (!HasComp<TransformComponent>(uid)) + if (!HasComp<TransformComponent>(uid) || !TryComp<ClumsyComponent>(comp.Target, out var clumsyComp)) return; _transformSystem.SetCoordinates(comp.Target, Transform(uid).Coordinates); - _bonkSystem.TryBonk(comp.Target, uid, bonkComp); + _clumsySystem.HitHeadClumsy((comp.Target, clumsyComp), uid); + + _audioSystem.PlayPvs(clumsyComp.TableBonkSound, comp.Target); } private void OnMobStateChanged(EntityUid uid, SuperBonkComponent comp, MobStateChangedEvent args) diff --git a/Content.Server/Administration/Toolshed/MarkedCommand.cs b/Content.Server/Administration/Toolshed/MarkedCommand.cs index b9e39cb82da..54d45a352de 100644 --- a/Content.Server/Administration/Toolshed/MarkedCommand.cs +++ b/Content.Server/Administration/Toolshed/MarkedCommand.cs @@ -7,7 +7,7 @@ namespace Content.Server.Administration.Toolshed; public sealed class MarkedCommand : ToolshedCommand { [CommandImplementation] - public IEnumerable<EntityUid> Marked([CommandInvocationContext] IInvocationContext ctx) + public IEnumerable<EntityUid> Marked(IInvocationContext ctx) { var res = (IEnumerable<EntityUid>?)ctx.ReadVar("marked"); res ??= Array.Empty<EntityUid>(); diff --git a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs index 61b0d6213aa..3925badc589 100644 --- a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs +++ b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs @@ -23,7 +23,7 @@ public IEnumerable<EntityUid> Rejuvenate([PipedArgument] IEnumerable<EntityUid> } [CommandImplementation] - public void Rejuvenate([CommandInvocationContext] IInvocationContext ctx) + public void Rejuvenate(IInvocationContext ctx) { _rejuvenate ??= GetSys<RejuvenateSystem>(); if (ExecutingEntity(ctx) is not { } ent) diff --git a/Content.Server/Administration/Toolshed/SolutionCommand.cs b/Content.Server/Administration/Toolshed/SolutionCommand.cs index e81e9aa3235..9ffadaa4ef1 100644 --- a/Content.Server/Administration/Toolshed/SolutionCommand.cs +++ b/Content.Server/Administration/Toolshed/SolutionCommand.cs @@ -7,6 +7,7 @@ using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; using System.Linq; +using Robust.Shared.Prototypes; namespace Content.Server.Administration.Toolshed; @@ -16,48 +17,38 @@ public sealed class SolutionCommand : ToolshedCommand private SolutionContainerSystem? _solutionContainer; [CommandImplementation("get")] - public SolutionRef? Get( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<string> name - ) + public SolutionRef? Get([PipedArgument] EntityUid input, string name) { _solutionContainer ??= GetSys<SolutionContainerSystem>(); - if (_solutionContainer.TryGetSolution(input, name.Evaluate(ctx)!, out var solution)) + if (_solutionContainer.TryGetSolution(input, name, out var solution)) return new SolutionRef(solution.Value); return null; } [CommandImplementation("get")] - public IEnumerable<SolutionRef> Get( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<EntityUid> input, - [CommandArgument] ValueRef<string> name - ) + public IEnumerable<SolutionRef> Get([PipedArgument] IEnumerable<EntityUid> input, string name) { - return input.Select(x => Get(ctx, x, name)).Where(x => x is not null).Cast<SolutionRef>(); + return input.Select(x => Get(x, name)).Where(x => x is not null).Cast<SolutionRef>(); } [CommandImplementation("adjreagent")] public SolutionRef AdjReagent( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] SolutionRef input, - [CommandArgument] Prototype<ReagentPrototype> name, - [CommandArgument] ValueRef<FixedPoint2> amountRef + ProtoId<ReagentPrototype> proto, + FixedPoint2 amount ) { _solutionContainer ??= GetSys<SolutionContainerSystem>(); - var amount = amountRef.Evaluate(ctx); if (amount > 0) { - _solutionContainer.TryAddReagent(input.Solution, name.Value.ID, amount, out _); + _solutionContainer.TryAddReagent(input.Solution, proto, amount, out _); } else if (amount < 0) { - _solutionContainer.RemoveReagent(input.Solution, name.Value.ID, -amount); + _solutionContainer.RemoveReagent(input.Solution, proto, -amount); } return input; @@ -65,12 +56,11 @@ [CommandArgument] ValueRef<FixedPoint2> amountRef [CommandImplementation("adjreagent")] public IEnumerable<SolutionRef> AdjReagent( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] IEnumerable<SolutionRef> input, - [CommandArgument] Prototype<ReagentPrototype> name, - [CommandArgument] ValueRef<FixedPoint2> amountRef + ProtoId<ReagentPrototype> name, + FixedPoint2 amount ) - => input.Select(x => AdjReagent(ctx, x, name, amountRef)); + => input.Select(x => AdjReagent(x, name, amount)); } public readonly record struct SolutionRef(Entity<SolutionComponent> Solution) diff --git a/Content.Server/Administration/Toolshed/TagCommand.cs b/Content.Server/Administration/Toolshed/TagCommand.cs index 1af27797660..ad3b33a9429 100644 --- a/Content.Server/Administration/Toolshed/TagCommand.cs +++ b/Content.Server/Administration/Toolshed/TagCommand.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Shared.Administration; using Content.Shared.Tag; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; @@ -13,94 +14,62 @@ public sealed class TagCommand : ToolshedCommand private TagSystem? _tag; [CommandImplementation("list")] - public IEnumerable<string> List([PipedArgument] IEnumerable<EntityUid> ent) + public IEnumerable<ProtoId<TagPrototype>> List([PipedArgument] IEnumerable<EntityUid> ent) { return ent.SelectMany(x => { if (TryComp<TagComponent>(x, out var tags)) // Note: Cast is required for C# to figure out the type signature. - return (IEnumerable<string>)tags.Tags; - return Array.Empty<string>(); + return (IEnumerable<ProtoId<TagPrototype>>)tags.Tags; + return Array.Empty<ProtoId<TagPrototype>>(); }); } [CommandImplementation("add")] - public EntityUid Add( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref - ) + public EntityUid Add([PipedArgument] EntityUid input, ProtoId<TagPrototype> tag) { _tag ??= GetSys<TagSystem>(); - _tag.AddTag(input, @ref.Evaluate(ctx)!); + _tag.AddTag(input, tag); return input; } [CommandImplementation("add")] - public IEnumerable<EntityUid> Add( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<EntityUid> input, - [CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref - ) - => input.Select(x => Add(ctx, x, @ref)); + public IEnumerable<EntityUid> Add([PipedArgument] IEnumerable<EntityUid> input, ProtoId<TagPrototype> tag) + => input.Select(x => Add(x, tag)); [CommandImplementation("rm")] - public EntityUid Rm( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref - ) + public EntityUid Rm([PipedArgument] EntityUid input, ProtoId<TagPrototype> tag) { _tag ??= GetSys<TagSystem>(); - _tag.RemoveTag(input, @ref.Evaluate(ctx)!); + _tag.RemoveTag(input, tag); return input; } [CommandImplementation("rm")] - public IEnumerable<EntityUid> Rm( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<EntityUid> input, - [CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref - ) - => input.Select(x => Rm(ctx, x, @ref)); + public IEnumerable<EntityUid> Rm([PipedArgument] IEnumerable<EntityUid> input, ProtoId<TagPrototype> tag) + => input.Select(x => Rm(x, tag)); [CommandImplementation("addmany")] - public EntityUid AddMany( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref - ) + public EntityUid AddMany([PipedArgument] EntityUid input, IEnumerable<ProtoId<TagPrototype>> tags) { _tag ??= GetSys<TagSystem>(); - _tag.AddTags(input, @ref.Evaluate(ctx)!); + _tag.AddTags(input, tags); return input; } [CommandImplementation("addmany")] - public IEnumerable<EntityUid> AddMany( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<EntityUid> input, - [CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref - ) - => input.Select(x => AddMany(ctx, x, @ref)); + public IEnumerable<EntityUid> AddMany([PipedArgument] IEnumerable<EntityUid> input, IEnumerable<ProtoId<TagPrototype>> tags) + => input.Select(x => AddMany(x, tags.ToArray())); [CommandImplementation("rmmany")] - public EntityUid RmMany( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref - ) + public EntityUid RmMany([PipedArgument] EntityUid input, IEnumerable<ProtoId<TagPrototype>> tags) { _tag ??= GetSys<TagSystem>(); - _tag.RemoveTags(input, @ref.Evaluate(ctx)!); + _tag.RemoveTags(input, tags); return input; } [CommandImplementation("rmmany")] - public IEnumerable<EntityUid> RmMany( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<EntityUid> input, - [CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref - ) - => input.Select(x => RmMany(ctx, x, @ref)); + public IEnumerable<EntityUid> RmMany([PipedArgument] IEnumerable<EntityUid> input, IEnumerable<ProtoId<TagPrototype>> tags) + => input.Select(x => RmMany(x, tags.ToArray())); } diff --git a/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs b/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs index 4f2108748b8..3dd216c5dce 100644 --- a/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs +++ b/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs @@ -1,6 +1,7 @@ using Content.Server.Power.Components; using Content.Server.Station.Systems; using Content.Shared.AlertLevel; +using Content.Shared.Power; namespace Content.Server.AlertLevel; diff --git a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs index 1b323d66437..98f33b84244 100644 --- a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs +++ b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; using Content.Shared.Mind.Components; +using Content.Shared.Power; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; diff --git a/Content.Server/Animals/Systems/UdderSystem.cs b/Content.Server/Animals/Systems/UdderSystem.cs index ef43c2c89d0..b7856e90423 100644 --- a/Content.Server/Animals/Systems/UdderSystem.cs +++ b/Content.Server/Animals/Systems/UdderSystem.cs @@ -15,7 +15,7 @@ namespace Content.Server.Animals.Systems; /// <summary> -/// Gives ability to produce milkable reagents, produces endless if the +/// Gives ability to produce milkable reagents, produces endless if the /// owner has no HungerComponent /// </summary> internal sealed class UdderSystem : EntitySystem @@ -76,9 +76,8 @@ private void AttemptMilk(Entity<UdderComponent?> udder, EntityUid userUid, Entit var doargs = new DoAfterArgs(EntityManager, userUid, 5, new MilkingDoAfterEvent(), udder, udder, used: containerUid) { - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, - BreakOnTargetMove = true, MovementThreshold = 1.0f, }; diff --git a/Content.Server/Anomaly/AnomalySynchronizerSystem.cs b/Content.Server/Anomaly/AnomalySynchronizerSystem.cs index 434a3fef66b..170676e08ca 100644 --- a/Content.Server/Anomaly/AnomalySynchronizerSystem.cs +++ b/Content.Server/Anomaly/AnomalySynchronizerSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Power; using Robust.Shared.Audio.Systems; using Content.Shared.Verbs; diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs index 056a985cbe2..6eb84b94418 100644 --- a/Content.Server/Anomaly/AnomalySystem.Generator.cs +++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs @@ -13,6 +13,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Map; using System.Numerics; +using Content.Shared.Power; using Robust.Server.GameObjects; namespace Content.Server.Anomaly; diff --git a/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs b/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs index f2a060d6295..bd4718e8e35 100644 --- a/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs @@ -61,7 +61,7 @@ public override void Update(float frameTime) var damage = (int) (elec.MaxElectrocuteDamage * anom.Severity); var duration = elec.MaxElectrocuteDuration * anom.Severity; - foreach (var (ent, comp) in _lookup.GetEntitiesInRange<StatusEffectsComponent>(xform.MapPosition, range)) + foreach (var (ent, comp) in _lookup.GetEntitiesInRange<StatusEffectsComponent>(_transform.GetMapCoordinates(uid, xform), range)) { _electrocution.TryDoElectrocution(ent, uid, damage, duration, true, statusEffects: comp, ignoreInsulation: true); } diff --git a/Content.Server/Anomaly/Effects/InjectionAnomalySystem.cs b/Content.Server/Anomaly/Effects/InjectionAnomalySystem.cs index 731d853280c..1fa0c00bd34 100644 --- a/Content.Server/Anomaly/Effects/InjectionAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/InjectionAnomalySystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Anomaly.Components; using Content.Shared.Chemistry.Components.SolutionManager; using System.Linq; +using Robust.Server.GameObjects; namespace Content.Server.Anomaly.Effects; /// <summary> @@ -16,6 +17,7 @@ public sealed class InjectionAnomalySystem : EntitySystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly TransformSystem _transform = default!; private EntityQuery<InjectableSolutionComponent> _injectableQuery; @@ -45,7 +47,7 @@ private void PulseScalableEffect(Entity<InjectionAnomalyComponent> entity, float //We get all the entity in the radius into which the reagent will be injected. var xformQuery = GetEntityQuery<TransformComponent>(); var xform = xformQuery.GetComponent(entity); - var allEnts = _lookup.GetEntitiesInRange<InjectableSolutionComponent>(xform.MapPosition, injectRadius) + var allEnts = _lookup.GetEntitiesInRange<InjectableSolutionComponent>(_transform.GetMapCoordinates(entity, xform: xform), injectRadius) .Select(x => x.Owner).ToList(); //for each matching entity found diff --git a/Content.Server/Antag/AntagObjectivesSystem.cs b/Content.Server/Antag/AntagObjectivesSystem.cs new file mode 100644 index 00000000000..5aa31f66f67 --- /dev/null +++ b/Content.Server/Antag/AntagObjectivesSystem.cs @@ -0,0 +1,35 @@ +using Content.Server.Antag.Components; +using Content.Server.Objectives; +using Content.Shared.Mind; +using Content.Shared.Objectives.Systems; + +namespace Content.Server.Antag; + +/// <summary> +/// Adds fixed objectives to an antag made with <c>AntagObjectivesComponent</c>. +/// </summary> +public sealed class AntagObjectivesSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mind = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<AntagObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected); + } + + private void OnAntagSelected(Entity<AntagObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args) + { + if (!_mind.TryGetMind(args.Session, out var mindId, out var mind)) + { + Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!"); + return; + } + + foreach (var id in ent.Comp.Objectives) + { + _mind.TryAddObjective(mindId, mind, id); + } + } +} diff --git a/Content.Server/Antag/AntagRandomObjectivesSystem.cs b/Content.Server/Antag/AntagRandomObjectivesSystem.cs new file mode 100644 index 00000000000..c935b8c0648 --- /dev/null +++ b/Content.Server/Antag/AntagRandomObjectivesSystem.cs @@ -0,0 +1,52 @@ +using Content.Server.Antag.Components; +using Content.Server.Objectives; +using Content.Shared.Mind; +using Content.Shared.Objectives.Components; +using Content.Shared.Objectives.Systems; +using Robust.Shared.Random; + +namespace Content.Server.Antag; + +/// <summary> +/// Adds fixed objectives to an antag made with <c>AntagRandomObjectivesComponent</c>. +/// </summary> +public sealed class AntagRandomObjectivesSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly ObjectivesSystem _objectives = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<AntagRandomObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected); + } + + private void OnAntagSelected(Entity<AntagRandomObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args) + { + if (!_mind.TryGetMind(args.Session, out var mindId, out var mind)) + { + Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!"); + return; + } + + var difficulty = 0f; + foreach (var set in ent.Comp.Sets) + { + if (!_random.Prob(set.Prob)) + continue; + + for (var pick = 0; pick < set.MaxPicks && ent.Comp.MaxDifficulty > difficulty; pick++) + { + if (_objectives.GetRandomObjective(mindId, mind, set.Groups) is not {} objective) + continue; + + _mind.AddObjective(mindId, mind, objective); + var adding = Comp<ObjectiveComponent>(objective).Difficulty; + difficulty += adding; + Log.Debug($"Added objective {ToPrettyString(objective):objective} to {ToPrettyString(args.EntityUid):player} with {adding} difficulty"); + } + } + } +} diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs index 77f543cdcf1..8893d9a476d 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.cs +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -147,7 +147,7 @@ public List<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent? } /// <remarks> - /// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/> + /// Helper to get just the mind entities and not names. /// </remarks> public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent) { diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index 8efdc2738b9..a0809e58829 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; +using Content.Server.Objectives; using Content.Server.Preferences.Managers; using Content.Server.Roles; using Content.Server.Roles.Jobs; @@ -17,6 +18,7 @@ using Content.Shared.Ghost; using Content.Shared.Humanoid; using Content.Shared.Players; +using Content.Shared.Whitelist; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Server.Player; @@ -40,6 +42,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection [Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; // arbitrary random number to give late joining some mild interest. public const float LateJoinRandomChance = 0.5f; @@ -51,6 +54,8 @@ public override void Initialize() SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole); + SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo); + SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning); SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned); SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete); @@ -402,20 +407,21 @@ public bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity)) return false; - if (def.Whitelist != null) - { - if (!def.Whitelist.IsValid(entity.Value)) - return false; - } - - if (def.Blacklist != null) - { - if (def.Blacklist.IsValid(entity.Value)) - return false; - } + if (_entityWhitelist.IsWhitelistFail(def.Whitelist, entity.Value) + || _entityWhitelist.IsBlacklistPass(def.Blacklist, entity.Value)) + return false; return true; } + + private void OnObjectivesTextGetInfo(Entity<AntagSelectionComponent> ent, ref ObjectivesTextGetInfoEvent args) + { + if (ent.Comp.AgentName is not {} name) + return; + + args.Minds = ent.Comp.SelectedMinds; + args.AgentName = Loc.GetString(name); + } } /// <summary> diff --git a/Content.Server/Antag/Components/AntagImmuneComponent.cs b/Content.Server/Antag/Components/AntagImmuneComponent.cs new file mode 100644 index 00000000000..3ae8e049f60 --- /dev/null +++ b/Content.Server/Antag/Components/AntagImmuneComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Antag.Components; + +[RegisterComponent] +public partial class AntagImmuneComponent : Component +{ + +} diff --git a/Content.Server/Antag/Components/AntagObjectivesComponent.cs b/Content.Server/Antag/Components/AntagObjectivesComponent.cs new file mode 100644 index 00000000000..357c138f46b --- /dev/null +++ b/Content.Server/Antag/Components/AntagObjectivesComponent.cs @@ -0,0 +1,18 @@ +using Content.Server.Antag; +using Content.Shared.Objectives.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.Antag.Components; + +/// <summary> +/// Gives antags selected by this rule a fixed list of objectives. +/// </summary> +[RegisterComponent, Access(typeof(AntagObjectivesSystem))] +public sealed partial class AntagObjectivesComponent : Component +{ + /// <summary> + /// List of static objectives to give. + /// </summary> + [DataField(required: true)] + public List<EntProtoId<ObjectiveComponent>> Objectives = new(); +} diff --git a/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs b/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs new file mode 100644 index 00000000000..9a551acc499 --- /dev/null +++ b/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs @@ -0,0 +1,52 @@ +using Content.Server.Antag; +using Content.Shared.Random; +using Robust.Shared.Prototypes; + +namespace Content.Server.Antag.Components; + +/// <summary> +/// Gives antags selected by this rule a random list of objectives. +/// </summary> +[RegisterComponent, Access(typeof(AntagRandomObjectivesSystem))] +public sealed partial class AntagRandomObjectivesComponent : Component +{ + /// <summary> + /// Each set of objectives to add. + /// </summary> + [DataField(required: true)] + public List<AntagObjectiveSet> Sets = new(); + + /// <summary> + /// If the total difficulty of the currently given objectives exceeds, no more will be given. + /// </summary> + [DataField(required: true)] + public float MaxDifficulty; +} + +/// <summary> +/// A set of objectives to try picking. +/// Difficulty is checked over all sets, but each set has its own probability and pick count. +/// </summary> +[DataRecord] +public record struct AntagObjectiveSet() +{ + /// <summary> + /// The grouping used by the objective system to pick random objectives. + /// First a group is picked from these, then an objective from that group. + /// </summary> + [DataField(required: true)] + public ProtoId<WeightedRandomPrototype> Groups = string.Empty; + + /// <summary> + /// Probability of this set being used. + /// </summary> + [DataField] + public float Prob = 1f; + + /// <summary> + /// Number of times to try picking objectives from this set. + /// Even if there is enough difficulty remaining, no more will be given after this. + /// </summary> + [DataField] + public int MaxPicks = 20; +} diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Server/Antag/Components/AntagSelectionComponent.cs index 5b6699dab76..cae56c1a7bb 100644 --- a/Content.Server/Antag/Components/AntagSelectionComponent.cs +++ b/Content.Server/Antag/Components/AntagSelectionComponent.cs @@ -42,6 +42,13 @@ public sealed partial class AntagSelectionComponent : Component /// Is not serialized. /// </summary> public HashSet<ICommonSession> SelectedSessions = new(); + + /// <summary> + /// Locale id for the name of the antag. + /// If this is set then the antag is listed in the round-end summary. + /// </summary> + [DataField] + public LocId? AgentName; } [DataDefinition] diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs index b2ad984884b..8d2a7991b76 100644 --- a/Content.Server/Antag/MobReplacementRuleSystem.cs +++ b/Content.Server/Antag/MobReplacementRuleSystem.cs @@ -24,6 +24,8 @@ using Content.Server.GameTicking; using Content.Server.Chat.Systems; using Content.Server.NPC.Systems; +using Content.Shared.NPC.Systems; + namespace Content.Server.Antag; diff --git a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs index 561cad8d7ee..b0bf3895092 100644 --- a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs +++ b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Advertise; using Content.Server.Advertise.Components; using Content.Shared.Arcade; +using Content.Shared.Power; using Robust.Server.GameObjects; using Robust.Shared.Player; diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs index 07a4d044cab..624f4cac3e8 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Advertise; using Content.Server.Advertise.Components; using Content.Shared.Mood; +using Content.Shared.Power; using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; using Robust.Server.GameObjects; using Robust.Shared.Audio; diff --git a/Content.Server/Atmos/Components/MovedByPressureComponent.cs b/Content.Server/Atmos/Components/MovedByPressureComponent.cs deleted file mode 100644 index ca830767bda..00000000000 --- a/Content.Server/Atmos/Components/MovedByPressureComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Content.Server.Atmos.Components -{ - // Unfortunately can't be friends yet due to magboots. - [RegisterComponent] - public sealed partial class MovedByPressureComponent : Component - { - public const float MoveForcePushRatio = 1f; - public const float MoveForceForcePushRatio = 1f; - public const float ProbabilityOffset = 25f; - public const float ProbabilityBasePercent = 10f; - public const float ThrowForce = 100f; - - /// <summary> - /// Accumulates time when yeeted by high pressure deltas. - /// </summary> - [DataField("accumulator")] - public float Accumulator = 0f; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("enabled")] - public bool Enabled { get; set; } = true; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("pressureResistance")] - public float PressureResistance { get; set; } = 1f; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("moveResist")] - public float MoveResist { get; set; } = 100f; - [ViewVariables(VVAccess.ReadWrite)] - public int LastHighPressureMovementAirCycle { get; set; } = 0; - } -} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs index c1b58f7a772..0d622f3067a 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs @@ -106,7 +106,7 @@ private void ExcitedGroupSelfBreakdown( if (tile?.Air == null) continue; - tile.Air.CopyFromMutable(combined); + tile.Air.CopyFrom(combined); InvalidateVisuals(ent, tile); } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs index 461435f0624..4fb0f3fe5ff 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs @@ -1,5 +1,6 @@ using Content.Server.Atmos.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Humanoid; using Content.Shared.Mobs.Components; using Content.Shared.Physics; diff --git a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs index 2f56142aa60..7ac4c653d3f 100644 --- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs @@ -18,6 +18,7 @@ using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Systems; using Content.Shared.Interaction; +using Content.Shared.Power; using Content.Shared.Wires; using Robust.Server.GameObjects; using Robust.Shared.Player; diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs index 2875d4a3d5d..81a3968e6ff 100644 --- a/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs @@ -7,10 +7,12 @@ using Content.Server.Power.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.DeviceNetwork; +using Content.Shared.Power; using Content.Shared.Tag; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Audio; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Atmos.Monitor.Systems; @@ -86,7 +88,7 @@ private void OnPacketRecv(EntityUid uid, AtmosAlarmableComponent component, Devi return; if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd) - || !args.Data.TryGetValue(AlertSource, out HashSet<string>? sourceTags)) + || !args.Data.TryGetValue(AlertSource, out HashSet<ProtoId<TagPrototype>>? sourceTags)) { return; } diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs index c1a5256fdd5..2c9a3587559 100644 --- a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Atmos; using Content.Shared.Atmos.Monitor; using Content.Shared.DeviceNetwork; +using Content.Shared.Power; using Content.Shared.Tag; using Robust.Shared.Prototypes; diff --git a/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs index 3c73a8f64ee..f932ef36208 100644 --- a/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs +++ b/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs @@ -132,10 +132,19 @@ public override void Update(float frameTime) var ev = new AtmosDeviceUpdateEvent(_atmosphereSystem.AtmosTime, null, null); foreach (var device in _joinedDevices) { - DebugTools.Assert(!HasComp<GridAtmosphereComponent>(Transform(device).GridUid)); + var deviceGrid = Transform(device).GridUid; + if (HasComp<GridAtmosphereComponent>(deviceGrid)) + { + RejoinAtmosphere(device); + } RaiseLocalEvent(device, ref ev); device.Comp.LastProcess = time; } } + + public bool IsJoinedOffGrid(Entity<AtmosDeviceComponent> device) + { + return _joinedDevices.Contains(device); + } } } diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs index 01eab560a16..827ba0bda5c 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs @@ -143,7 +143,7 @@ private bool IsHeater(GasThermoMachineComponent comp) private void OnToggleMessage(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineToggleMessage args) { - var powerState = _power.TryTogglePower(uid); + var powerState = _power.TogglePower(uid); _adminLogger.Add(LogType.AtmosPowerChanged, $"{ToPrettyString(args.Actor)} turned {(powerState ? "On" : "Off")} {ToPrettyString(uid)}"); DirtyUI(uid, thermoMachine); } diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs index 7c12cf3f77f..0921a763d5a 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs @@ -18,6 +18,7 @@ using Content.Shared.Audio; using Content.Shared.DeviceNetwork; using Content.Shared.Examine; +using Content.Shared.Power; using Content.Shared.Tools.Systems; using JetBrains.Annotations; using Robust.Server.GameObjects; diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs index b27689ed586..a35cf6c2e30 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Audio; using Content.Shared.DeviceNetwork; +using Content.Shared.Power; using Content.Shared.Tools.Systems; using JetBrains.Annotations; using Robust.Server.GameObjects; diff --git a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs index fbe40deedb1..7dc141f8753 100644 --- a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs +++ b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs @@ -16,6 +16,7 @@ using Content.Server.NodeContainer.EntitySystems; using Content.Shared.Atmos; using Content.Shared.Database; +using Content.Shared.Power; namespace Content.Server.Atmos.Portable { diff --git a/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs b/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs index 70eea2d9b78..36ef59e743b 100644 --- a/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs +++ b/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Atmos.Piping.Portable.Components; using Content.Shared.Atmos.Visuals; +using Content.Shared.Power; using Content.Shared.UserInterface; using Robust.Server.GameObjects; @@ -98,7 +99,7 @@ private void OnToggle(EntityUid uid, SpaceHeaterComponent spaceHeater, SpaceHeat if (!Resolve(uid, ref powerReceiver)) return; - _power.TryTogglePower(uid); + _power.TogglePower(uid); UpdateAppearance(uid); DirtyUI(uid, spaceHeater); diff --git a/Content.Server/Atmos/Rotting/RottingSystem.cs b/Content.Server/Atmos/Rotting/RottingSystem.cs index 40bdf657439..bfff8a91d0d 100644 --- a/Content.Server/Atmos/Rotting/RottingSystem.cs +++ b/Content.Server/Atmos/Rotting/RottingSystem.cs @@ -34,7 +34,7 @@ private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEven if (!TryComp<PerishableComponent>(uid, out var perishable)) return; - var molsToDump = perishable.MolsPerSecondPerUnitMass * physics.FixturesMass * (float) component.TotalRotTime.TotalSeconds; + var molsToDump = perishable.MolsPerSecondPerUnitMass * physics.FixturesMass * (float)component.TotalRotTime.TotalSeconds; var tileMix = _atmosphere.GetTileMixture(uid, excite: true); tileMix?.AdjustMoles(Gas.Ammonia, molsToDump); } @@ -77,7 +77,7 @@ public void ReduceAccumulator(EntityUid uid, TimeSpan time) /// <returns></returns> private float GetRotRate(EntityUid uid) { - if (_container.TryGetContainingContainer(uid, out var container) && + if (_container.TryGetContainingContainer((uid, null, null), out var container) && TryComp<ProRottingContainerComponent>(container.Owner, out var rotContainer)) { return rotContainer.DecayModifier; @@ -147,7 +147,7 @@ public override void Update(float frameTime) continue; // We need a way to get the mass of the mob alone without armor etc in the future // or just remove the mass mechanics altogether because they aren't good. - var molRate = perishable.MolsPerSecondPerUnitMass * (float) rotting.RotUpdateRate.TotalSeconds; + var molRate = perishable.MolsPerSecondPerUnitMass * (float)rotting.RotUpdateRate.TotalSeconds; var tileMix = _atmosphere.GetTileMixture(uid, excite: true); tileMix?.AdjustMoles(Gas.Ammonia, molRate * physics.FixturesMass); } diff --git a/Content.Server/Audio/AmbientSoundSystem.cs b/Content.Server/Audio/AmbientSoundSystem.cs index e78970d1243..1f4abf34f77 100644 --- a/Content.Server/Audio/AmbientSoundSystem.cs +++ b/Content.Server/Audio/AmbientSoundSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Audio; using Content.Shared.Mobs; +using Content.Shared.Power; namespace Content.Server.Audio; diff --git a/Content.Server/Audio/Jukebox/JukeboxSystem.cs b/Content.Server/Audio/Jukebox/JukeboxSystem.cs index cc9235e3d7a..3535f6b2382 100644 --- a/Content.Server/Audio/Jukebox/JukeboxSystem.cs +++ b/Content.Server/Audio/Jukebox/JukeboxSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Audio.Jukebox; +using Content.Shared.Power; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Components; diff --git a/Content.Server/Beam/BeamSystem.cs b/Content.Server/Beam/BeamSystem.cs index 33f2f252d90..ad67f983c27 100644 --- a/Content.Server/Beam/BeamSystem.cs +++ b/Content.Server/Beam/BeamSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Beam; using Content.Shared.Beam.Components; using Content.Shared.Physics; +using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; @@ -16,6 +17,7 @@ namespace Content.Server.Beam; public sealed class BeamSystem : SharedBeamSystem { [Dependency] private readonly FixtureSystem _fixture = default!; + [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; @@ -144,8 +146,8 @@ public void TryCreateBeam(EntityUid user, EntityUid target, string bodyPrototype if (Deleted(user) || Deleted(target)) return; - var userMapPos = Transform(user).MapPosition; - var targetMapPos = Transform(target).MapPosition; + var userMapPos = _transform.GetMapCoordinates(user); + var targetMapPos = _transform.GetMapCoordinates(target); //The distance between the target and the user. var calculatedDistance = targetMapPos.Position - userMapPos.Position; diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index 089ce322366..2335859f0b8 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -13,6 +13,7 @@ using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Mobs.Systems; +using Content.Shared.Power; using Robust.Shared.Timing; using Content.Shared.Silicon.Components; // I shouldn't have to modify this. using Robust.Shared.Utility; diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index 54e51d7e35e..a40bafc8cff 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Body.Components; using Content.Server.Body.Events; using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Chemistry.ReactionEffects; +using Content.Server.EntityEffects.Effects; using Content.Server.Fluids.EntitySystems; using Content.Server.Forensics; using Content.Server.Popups; diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index fdcc76718cf..b79e083bd46 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -23,6 +23,7 @@ public sealed class InternalsSystem : EntitySystem [Dependency] private readonly GasTankSystem _gasTank = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly RespiratorSystem _respirator = default!; private EntityQuery<InternalsComponent> _internalsQuery; @@ -38,15 +39,30 @@ public override void Initialize() SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs); SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter); - SubscribeLocalEvent<StartingGearEquippedEvent>(OnStartingGear); + SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear); } - private void OnStartingGear(ref StartingGearEquippedEvent ev) + private void OnStartingGear(EntityUid uid, InternalsComponent component, ref StartingGearEquippedEvent args) { - if (!_internalsQuery.TryComp(ev.Entity, out var internals) || internals.BreathToolEntity == null) + if (component.BreathToolEntity == null) return; - ToggleInternals(ev.Entity, ev.Entity, force: false, internals); + if (component.GasTankEntity != null) + return; // already connected + + // Can the entity breathe the air it is currently exposed to? + if (_respirator.CanMetabolizeInhaledAir(uid)) + return; + + var tank = FindBestGasTank(uid); + if (tank == null) + return; + + // Could the entity metabolise the air in the linked gas tank? + if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air)) + return; + + ToggleInternals(uid, uid, force: false, component); } private void OnGetInteractionVerbs( @@ -126,9 +142,8 @@ private void StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsCompone _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt) { - BreakOnUserMove = true, BreakOnDamage = true, - BreakOnTargetMove = true, + BreakOnMove = true, MovementThreshold = 0.1f, }); } @@ -244,6 +259,7 @@ private short GetSeverity(InternalsComponent component) public Entity<GasTankComponent>? FindBestGasTank( Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user) { + // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses // Prioritise // 1. back equipped tanks // 2. exo-slot tanks diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs index e83d3c32a25..7e58c24f7e4 100644 --- a/Content.Server/Body/Systems/LungSystem.cs +++ b/Content.Server/Body/Systems/LungSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Body.Components; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Shared.Atmos; +using Content.Shared.Chemistry.Components; using Content.Shared.Clothing; using Content.Shared.Inventory.Events; @@ -77,23 +78,32 @@ public void GasToReagent(EntityUid uid, LungComponent lung) if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution)) return; - foreach (var gas in Enum.GetValues<Gas>()) + GasToReagent(lung.Air, solution); + _solutionContainerSystem.UpdateChemicals(lung.Solution.Value); + } + + private void GasToReagent(GasMixture gas, Solution solution) + { + foreach (var gasId in Enum.GetValues<Gas>()) { - var i = (int) gas; - var moles = lung.Air[i]; + var i = (int) gasId; + var moles = gas[i]; if (moles <= 0) continue; + var reagent = _atmosphereSystem.GasReagents[i]; - if (reagent is null) continue; + if (reagent is null) + continue; var amount = moles * Atmospherics.BreathMolesToReagentMultiplier; solution.AddReagent(reagent, amount); - - // We don't remove the gas from the lung mix, - // that's the responsibility of whatever gas is being metabolized. - // Most things will just want to exhale again. } + } - _solutionContainerSystem.UpdateChemicals(lung.Solution.Value); + public Solution GasToReagent(GasMixture gas) + { + var solution = new Solution(); + GasToReagent(gas, solution); + return solution; } } diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs index a7eec8e3c02..8b0f0ac6f8d 100644 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ b/Content.Server/Body/Systems/MetabolizerSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -196,8 +197,7 @@ private void TryMetabolize(Entity<MetabolizerComponent, OrganComponent?, Solutio var ev = new TryMetabolizeReagent(reagent, proto, quantity); RaiseLocalEvent(actualEntity, ref ev); - var args = new ReagentEffectArgs(actualEntity, ent, solution, proto, mostToRemove, - EntityManager, null, scale * ev.Scale, ev.QuantityMultiplier); + var args = new EntityEffectReagentArgs(actualEntity, EntityManager, ent, solution, mostToRemove, proto, null, scale); // do all effects, if conditions apply foreach (var effect in entry.Effects) diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index 9bf8d47c05f..5ad0fb35acf 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -1,18 +1,25 @@ using Content.Server.Administration.Logs; -using Content.Server.Atmos; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Components; +using Content.Server.Chat.Systems; using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.EntityEffects.EffectConditions; +using Content.Server.EntityEffects.Effects; using Content.Server.Popups; using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Body.Components; -using Content.Shared._Shitmed.Body.Organ; // Shitmed Change +using Content.Shared._Shitmed.Body.Organ; +using Content.Shared.Body.Prototypes; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; // Shitmed Change using Content.Shared.Damage; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.Mobs.Systems; using Content.Shared.Mood; using JetBrains.Annotations; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Body.Systems; @@ -29,7 +36,11 @@ public sealed class RespiratorSystem : EntitySystem [Dependency] private readonly LungSystem _lungSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly ChatSystem _chat = default!; + + private static readonly ProtoId<MetabolismGroupPrototype> GasId = new("Gas"); public override void Initialize() { @@ -114,7 +125,7 @@ public void Inhale(EntityUid uid, BodyComponent? body = null) // Inhale gas var ev = new InhaleLocationEvent(); - RaiseLocalEvent(uid, ref ev, broadcast: false); + RaiseLocalEvent(uid, ref ev); ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true); @@ -169,6 +180,112 @@ public void Exhale(EntityUid uid, BodyComponent? body = null) _atmosSys.Merge(ev.Gas, outGas); } + /// <summary> + /// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic + /// gasses). + /// </summary> + public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + var ev = new InhaleLocationEvent(); + RaiseLocalEvent(ent, ref ev); + + var gas = ev.Gas ?? _atmosSys.GetContainingMixture(ent.Owner); + if (gas == null) + return false; + + return CanMetabolizeGas(ent, gas); + } + + /// <summary> + /// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage + /// (i.e., no toxic gasses). + /// </summary> + public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent); + if (organs.Count == 0) + return false; + + gas = new GasMixture(gas); + var lungRatio = 1.0f / organs.Count; + gas.Multiply(MathF.Min(lungRatio * gas.Volume/Atmospherics.BreathVolume, lungRatio)); + var solution = _lungSystem.GasToReagent(gas); + + float saturation = 0; + foreach (var organ in organs) + { + saturation += GetSaturation(solution, organ.Comp.Owner, out var toxic); + if (toxic) + return false; + } + + return saturation > ent.Comp.UpdateInterval.TotalSeconds; + } + + /// <summary> + /// Get the amount of saturation that would be generated if the lung were to metabolize the given solution. + /// </summary> + /// <remarks> + /// This assumes the metabolism rate is unbounded, which generally should be the case for lungs, otherwise we get + /// back to the old pulmonary edema bug. + /// </remarks> + /// <param name="solution">The reagents to metabolize</param> + /// <param name="lung">The entity doing the metabolizing</param> + /// <param name="toxic">Whether or not any of the reagents would deal damage to the entity</param> + private float GetSaturation(Solution solution, Entity<MetabolizerComponent?> lung, out bool toxic) + { + toxic = false; + if (!Resolve(lung, ref lung.Comp)) + return 0; + + if (lung.Comp.MetabolismGroups == null) + return 0; + + float saturation = 0; + foreach (var (id, quantity) in solution.Contents) + { + var reagent = _protoMan.Index<ReagentPrototype>(id.Prototype); + if (reagent.Metabolisms == null) + continue; + + if (!reagent.Metabolisms.TryGetValue(GasId, out var entry)) + continue; + + foreach (var effect in entry.Effects) + { + if (effect is HealthChange health) + toxic |= CanMetabolize(health) && health.Damage.AnyPositive(); + else if (effect is Oxygenate oxy && CanMetabolize(oxy)) + saturation += oxy.Factor * quantity.Float(); + } + } + + // TODO generalize condition checks + // this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture + // Applying actual reaction effects require a full ReagentEffectArgs struct. + bool CanMetabolize(EntityEffect effect) + { + if (effect.Conditions == null) + return true; + + foreach (var cond in effect.Conditions) + { + if (cond is OrganType organ && !organ.Condition(lung, EntityManager)) + return false; + } + + return true; + } + + return saturation; + } + private void TakeSuffocationDamage(Entity<RespiratorComponent> ent) { if (ent.Comp.SuffocationCycles == 2) diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs index dd3555f143f..d08f5985215 100644 --- a/Content.Server/Botany/SeedPrototype.cs +++ b/Content.Server/Botany/SeedPrototype.cs @@ -1,7 +1,7 @@ using Content.Server.Botany.Components; using Content.Server.Botany.Systems; using Content.Shared.Atmos; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -70,7 +70,7 @@ public partial struct SeedChemQuantity /// When chemicals are added to produce, the potency of the seed is divided with this value. Final chemical amount is the result plus the `Min` value. /// Example: PotencyDivisor of 20 with seed potency of 55 results in 2.75, 55/20 = 2.75. If minimum is 1 then final result will be 3.75 of that chemical, 55/20+1 = 3.75. /// </summary> - [DataField] public int PotencyDivisor; + [DataField("PotencyDivisor")] public int PotencyDivisor; /// <summary> /// Inherent chemical is one that is NOT result of mutation or crossbreeding. These chemicals are removed if species mutation is executed. @@ -80,7 +80,7 @@ public partial struct SeedChemQuantity // TODO reduce the number of friends to a reasonable level. Requires ECS-ing things like plant holder component. [Virtual, DataDefinition] -[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(ReagentEffect), typeof(MutationSystem))] +[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(EntityEffect), typeof(MutationSystem))] public partial class SeedData { #region Tracking diff --git a/Content.Server/Botany/Systems/BotanySwabSystem.cs b/Content.Server/Botany/Systems/BotanySwabSystem.cs index 8f300c94486..f6190bae4ea 100644 --- a/Content.Server/Botany/Systems/BotanySwabSystem.cs +++ b/Content.Server/Botany/Systems/BotanySwabSystem.cs @@ -47,8 +47,7 @@ private void OnAfterInteract(EntityUid uid, BotanySwabComponent swab, AfterInter _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, swab.SwabDelay, new BotanySwabDoAfterEvent(), uid, target: args.Target, used: uid) { Broadcast = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }); } diff --git a/Content.Server/Buckle/Systems/AntiRotOnBuckleSystem.cs b/Content.Server/Buckle/Systems/AntiRotOnBuckleSystem.cs index 4458b020a11..6f356baf073 100644 --- a/Content.Server/Buckle/Systems/AntiRotOnBuckleSystem.cs +++ b/Content.Server/Buckle/Systems/AntiRotOnBuckleSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Power.Components; using Content.Shared.Atmos.Rotting; using Content.Shared.Buckle.Components; +using Content.Shared.Power; namespace Content.Server.Buckle.Systems; diff --git a/Content.Server/Cargo/Components/StationStockMarketComponent.cs b/Content.Server/Cargo/Components/StationStockMarketComponent.cs new file mode 100644 index 00000000000..4ea9bd43133 --- /dev/null +++ b/Content.Server/Cargo/Components/StationStockMarketComponent.cs @@ -0,0 +1,71 @@ +using System.Numerics; +using Content.Server.DeltaV.Cargo.Systems; +using Content.Server.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Timing; + +namespace Content.Server.DeltaV.Cargo.Components; + +[RegisterComponent, AutoGenerateComponentPause] +[Access(typeof(StockMarketSystem), typeof(StockTradingCartridgeSystem))] +public sealed partial class StationStockMarketComponent : Component +{ + /// <summary> + /// The list of companies you can invest in + /// </summary> + [DataField] + public List<StockCompanyStruct> Companies = []; + + /// <summary> + /// The list of shares owned by the station + /// </summary> + [DataField] + public Dictionary<int, int> StockOwnership = new(); + + /// <summary> + /// The interval at which the stock market updates + /// </summary> + [DataField] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(600); // 10 minutes + + /// <summary> + /// The <see cref="IGameTiming.CurTime"/> timespan of next update. + /// </summary> + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextUpdate = TimeSpan.Zero; + + /// <summary> + /// The sound to play after selling or buying stocks + /// </summary> + [DataField] + public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg"); + + /// <summary> + /// The sound to play if the don't have access to buy or sell stocks + /// </summary> + [DataField] + public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg"); + + // These work well as presets but can be changed in the yaml + [DataField] + public List<MarketChange> MarketChanges = + [ + new() { Chance = 0.86f, Range = new Vector2(-0.05f, 0.05f) }, // Minor + new() { Chance = 0.10f, Range = new Vector2(-0.3f, 0.2f) }, // Moderate + new() { Chance = 0.03f, Range = new Vector2(-0.5f, 1.5f) }, // Major + new() { Chance = 0.01f, Range = new Vector2(-0.9f, 4.0f) }, // Catastrophic + ]; +} + +[DataDefinition] +public sealed partial class MarketChange +{ + [DataField(required: true)] + public float Chance; + + [DataField(required: true)] + public Vector2 Range; +} diff --git a/Content.Server/Cargo/StocksCommands.cs b/Content.Server/Cargo/StocksCommands.cs new file mode 100644 index 00000000000..dfe1776f666 --- /dev/null +++ b/Content.Server/Cargo/StocksCommands.cs @@ -0,0 +1,135 @@ +using Content.Server.Administration; +using Content.Server.DeltaV.Cargo.Components; +using Content.Server.DeltaV.Cargo.Systems; +using Content.Shared.Administration; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Shared.Console; + +namespace Content.Server.DeltaV.Cargo; + +[AdminCommand(AdminFlags.Fun)] +public sealed class ChangeStocksPriceCommand : IConsoleCommand +{ + public string Command => "changestocksprice"; + public string Description => Loc.GetString("cmd-changestocksprice-desc"); + public string Help => Loc.GetString("cmd-changestocksprice-help", ("command", Command)); + + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!int.TryParse(args[0], out var companyIndex)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + if (!float.TryParse(args[1], out var newPrice)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + EntityUid? targetStation = null; + if (args.Length > 2) + { + if (!EntityUid.TryParse(args[2], out var station)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + targetStation = station; + } + + var stockMarket = _entitySystemManager.GetEntitySystem<StockMarketSystem>(); + var query = _entityManager.EntityQueryEnumerator<StationStockMarketComponent>(); + + while (query.MoveNext(out var uid, out var comp)) + { + // Skip if we're looking for a specific station and this isn't it + if (targetStation != null && uid != targetStation) + continue; + + if (stockMarket.TryChangeStocksPrice(uid, comp, newPrice, companyIndex)) + { + shell.WriteLine(Loc.GetString("shell-command-success")); + return; + } + + shell.WriteLine(Loc.GetString("cmd-changestocksprice-invalid-company")); + return; + } + + shell.WriteLine(targetStation != null + ? Loc.GetString("cmd-changestocksprice-invalid-station") + : Loc.GetString("cmd-changestocksprice-no-stations")); + } +} + +[AdminCommand(AdminFlags.Fun)] +public sealed class AddStocksCompanyCommand : IConsoleCommand +{ + public string Command => "addstockscompany"; + public string Description => Loc.GetString("cmd-addstockscompany-desc"); + public string Help => Loc.GetString("cmd-addstockscompany-help", ("command", Command)); + + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!float.TryParse(args[1], out var basePrice)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + return; + } + + EntityUid? targetStation = null; + if (args.Length > 2) + { + if (!EntityUid.TryParse(args[2], out var station)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + targetStation = station; + } + + var displayName = args[0]; + var stockMarket = _entitySystemManager.GetEntitySystem<StockMarketSystem>(); + var query = _entityManager.EntityQueryEnumerator<StationStockMarketComponent>(); + + while (query.MoveNext(out var uid, out var comp)) + { + // Skip if we're looking for a specific station and this isn't it + if (targetStation != null && uid != targetStation) + continue; + + if (stockMarket.TryAddCompany(uid, comp, basePrice, displayName)) + { + shell.WriteLine(Loc.GetString("shell-command-success")); + return; + } + + shell.WriteLine(Loc.GetString("cmd-addstockscompany-failure")); + return; + } + + shell.WriteLine(targetStation != null + ? Loc.GetString("cmd-addstockscompany-invalid-station") + : Loc.GetString("cmd-addstockscompany-no-stations")); + } +} diff --git a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs index e132e4f12a3..6723b423c6f 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs @@ -11,6 +11,7 @@ using Content.Shared.Database; using Content.Shared.NameIdentifier; using Content.Shared.Stacks; +using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Server.Containers; using Robust.Shared.Containers; @@ -23,6 +24,7 @@ public sealed partial class CargoSystem { [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!; [ValidatePrototypeId<NameIdentifierGroupPrototype>] private const string BountyNameIdentifierGroup = "Bounty"; @@ -135,7 +137,7 @@ private void OnGetBountyPrice(EntityUid uid, CargoBountyLabelComponent component return; // make sure this label was actually applied to a crate. - if (!_container.TryGetContainingContainer(uid, out var container) || container.ID != LabelSystem.ContainerName) + if (!_container.TryGetContainingContainer((uid, null, null), out var container) || container.ID != LabelSystem.ContainerName) return; if (component.AssociatedStationId is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var database)) @@ -298,6 +300,21 @@ public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEnt return IsBountyComplete(GetBountyEntities(container), entries, out bountyEntities); } + /// <summary> + /// Determines whether the <paramref name="entity"/> meets the criteria for the bounty <paramref name="entry"/>. + /// </summary> + /// <returns>true if <paramref name="entity"/> is a valid item for the bounty entry, otherwise false</returns> + public bool IsValidBountyEntry(EntityUid entity, CargoBountyItemEntry entry) + { + if (!_whitelistSys.IsWhitelistPass(entry.Whitelist, entity)) + return false; + + if (entry.Blacklist != null && _whitelistSys.IsBlacklistPass(entry.Blacklist, entity)) + return false; + + return true; + } + public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBountyItemEntry> entries, out HashSet<EntityUid> bountyEntities) { bountyEntities = new(); @@ -311,7 +328,7 @@ public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBount var temp = new HashSet<EntityUid>(); foreach (var entity in entities) { - if (!entry.Whitelist.IsValid(entity, EntityManager)) + if (!IsValidBountyEntry(entity, entry)) continue; count += _stackQuery.CompOrNull(entity)?.Count ?? 1; diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index ac7aadfef15..db732d0046c 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -9,6 +9,8 @@ using Content.Shared.Cargo.Events; using Content.Shared.Cargo.Prototypes; using Content.Shared.Database; +using Content.Shared.Emag.Components; +using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Robust.Shared.Map; using Robust.Shared.Player; @@ -38,6 +40,7 @@ private void InitializeConsole() SubscribeLocalEvent<CargoOrderConsoleComponent, BoundUIOpenedEvent>(OnOrderUIOpened); SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit); SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing); + SubscribeLocalEvent<CargoOrderConsoleComponent, BankBalanceUpdatedEvent>(OnOrderBalanceUpdated); Reset(); } @@ -193,15 +196,20 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle); _audio.PlayPvs(component.ConfirmSound, uid); - var approverName = idCard.Comp?.FullName ?? Loc.GetString("access-reader-unknown-id"); - var approverJob = idCard.Comp?.JobTitle ?? Loc.GetString("access-reader-unknown-id"); - var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", - ("productName", Loc.GetString(order.ProductName)), - ("orderAmount", order.OrderQuantity), - ("approverName", approverName), - ("approverJob", approverJob), - ("cost", cost)); - _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); + if (!HasComp<EmaggedComponent>(uid)) + { + var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(uid, player); + RaiseLocalEvent(tryGetIdentityShortInfoEvent); + order.SetApproverData(tryGetIdentityShortInfoEvent.Title); + + var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", + ("productName", Loc.GetString(order.ProductName)), + ("orderAmount", order.OrderQuantity), + ("approver", order.Approver ?? string.Empty), + ("cost", cost)); + _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); + } + ConsolePopup(args.Actor, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName))); // Log order approval @@ -317,6 +325,15 @@ private void OnOrderUIOpened(EntityUid uid, CargoOrderConsoleComponent component #endregion + + private void OnOrderBalanceUpdated(Entity<CargoOrderConsoleComponent> ent, ref BankBalanceUpdatedEvent args) + { + if (!_uiSystem.IsUiOpen(ent.Owner, CargoConsoleUiKey.Orders)) + return; + + UpdateOrderState(ent, args.Station); + } + private void UpdateOrderState(EntityUid consoleUid, EntityUid? station) { if (station == null || diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs index 223dd2ac329..a78b6b1b25f 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs @@ -8,6 +8,7 @@ using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.DeviceLinking; +using Content.Shared.Power; using Robust.Shared.Audio; using Robust.Shared.Random; using Robust.Shared.Utility; diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs index 16597fa1bcd..6e0c274570b 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Access.Systems; using Content.Server.Cargo.Components; using Content.Server.DeviceLinking.Systems; using Content.Server.Paper; @@ -33,7 +32,6 @@ public sealed partial class CargoSystem : SharedCargoSystem [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; [Dependency] private readonly DeviceLinkSystem _linker = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly IdCardSystem _idCardSystem = default!; [Dependency] private readonly ItemSlotsSystem _slots = default!; [Dependency] private readonly PaperSystem _paperSystem = default!; [Dependency] private readonly PopupSystem _popup = default!; @@ -53,6 +51,7 @@ public sealed partial class CargoSystem : SharedCargoSystem [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly MapLoaderSystem _mapLoader = default!; + [Dependency] private readonly SharedIdCardSystem _idCardSystem = default!; private EntityQuery<TransformComponent> _xformQuery; private EntityQuery<CargoSellBlacklistComponent> _blacklistQuery; @@ -90,18 +89,18 @@ public override void Update(float frameTime) public void UpdateBankAccount(EntityUid uid, StationBankAccountComponent component, int balanceAdded) { component.Balance += balanceAdded; - var query = EntityQueryEnumerator<CargoOrderConsoleComponent>(); + var query = EntityQueryEnumerator<BankClientComponent, TransformComponent>(); - while (query.MoveNext(out var oUid, out var _)) + var ev = new BankBalanceUpdatedEvent(uid, component.Balance); + while (query.MoveNext(out var client, out var comp, out var xform)) { - if (!_uiSystem.IsUiOpen(oUid, CargoConsoleUiKey.Orders)) - continue; - - var station = _station.GetOwningStation(oUid); + var station = _station.GetOwningStation(client, xform); if (station != uid) continue; - UpdateOrderState(oUid, station); + comp.Balance = component.Balance; + Dirty(client, comp); + RaiseLocalEvent(client, ref ev); } } } diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs index f878eeee75c..d936451d524 100644 --- a/Content.Server/Cargo/Systems/PricingSystem.cs +++ b/Content.Server/Cargo/Systems/PricingSystem.cs @@ -16,6 +16,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Utility; using System.Linq; +using Content.Shared.Research.Prototypes; namespace Content.Server.Cargo.Systems; @@ -158,6 +159,26 @@ private double GetMaterialPrice(PhysicalCompositionComponent component) return price; } + public double GetLatheRecipePrice(LatheRecipePrototype recipe) + { + var price = 0.0; + + if (recipe.Result is { } result) + { + price += GetEstimatedPrice(_prototypeManager.Index(result)); + } + + if (recipe.ResultReagents is { } resultReagents) + { + foreach (var (reagent, amount) in resultReagents) + { + price += (_prototypeManager.Index(reagent).PricePerUnit * amount).Double(); + } + } + + return price; + } + /// <summary> /// Get a rough price for an entityprototype. Does not consider contained entities. /// </summary> diff --git a/Content.Server/Cargo/Systems/StockMarketSystem.cs b/Content.Server/Cargo/Systems/StockMarketSystem.cs new file mode 100644 index 00000000000..20c840728e0 --- /dev/null +++ b/Content.Server/Cargo/Systems/StockMarketSystem.cs @@ -0,0 +1,376 @@ +using Content.Server.Access.Systems; +using Content.Server.Administration.Logs; +using Content.Server.Cargo.Components; +using Content.Server.Cargo.Systems; +using Content.Server.DeltaV.Cargo.Components; +using Content.Server.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.Access.Components; +using Content.Shared.Access.Systems; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.Database; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.DeltaV.Cargo.Systems; + +/// <summary> +/// This handles the stock market updates +/// </summary> +public sealed class StockMarketSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessSystem = default!; + [Dependency] private readonly CargoSystem _cargo = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ILogManager _log = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IdCardSystem _idCardSystem = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private ISawmill _sawmill = default!; + private const float MaxPrice = 262144; // 1/64 of max safe integer + + public override void Initialize() + { + base.Initialize(); + + _sawmill = _log.GetSawmill("admin.stock_market"); + + SubscribeLocalEvent<StockTradingCartridgeComponent, CartridgeMessageEvent>(OnStockTradingMessage); + } + + public override void Update(float frameTime) + { + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator<StationStockMarketComponent>(); + + while (query.MoveNext(out var uid, out var component)) + { + if (curTime < component.NextUpdate) + continue; + + component.NextUpdate = curTime + component.UpdateInterval; + UpdateStockPrices(uid, component); + } + } + + private void OnStockTradingMessage(Entity<StockTradingCartridgeComponent> ent, ref CartridgeMessageEvent args) + { + if (args is not StockTradingUiMessageEvent message) + return; + + var companyIndex = message.CompanyIndex; + var amount = (int)message.Amount; + var station = ent.Comp.Station; + var loader = GetEntity(args.LoaderUid); + var xform = Transform(loader); + + // Ensure station and stock market components are valid + if (station == null || !TryComp<StationStockMarketComponent>(station, out var stockMarket)) + return; + + // Validate company index + if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return; + + if (!TryComp<AccessReaderComponent>(ent.Owner, out var access)) + return; + + // Attempt to retrieve ID card from loader + IdCardComponent? idCard = null; + if (_idCardSystem.TryGetIdCard(loader, out var pdaId)) + idCard = pdaId; + + // Play deny sound and exit if access is not allowed + if (idCard == null || !_accessSystem.IsAllowed(pdaId.Owner, ent.Owner, access)) + { + _audio.PlayEntity( + stockMarket.DenySound, + Filter.Empty().AddInRange(_transform.GetMapCoordinates(loader, xform), 0.05f), + loader, + true, + AudioParams.Default.WithMaxDistance(0.05f) + ); + return; + } + + try + { + var company = stockMarket.Companies[companyIndex]; + + // Attempt to buy or sell stocks based on the action + bool success; + switch (message.Action) + { + case StockTradingUiAction.Buy: + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(loader)} attempting to buy {amount} stocks of {company.LocalizedDisplayName}"); + success = TryBuyStocks(station.Value, stockMarket, companyIndex, amount); + break; + + case StockTradingUiAction.Sell: + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(loader)} attempting to sell {amount} stocks of {company.LocalizedDisplayName}"); + success = TrySellStocks(station.Value, stockMarket, companyIndex, amount); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + // Play confirmation sound if the transaction was successful + _audio.PlayEntity(success ? stockMarket.ConfirmSound : stockMarket.DenySound, loader, args.Actor); + } + finally + { + // Raise the event to update the UI regardless of outcome + var ev = new StockMarketUpdatedEvent(station.Value); + RaiseLocalEvent(ev); + } + } + + private bool TryBuyStocks( + EntityUid station, + StationStockMarketComponent stockMarket, + int companyIndex, + int amount) + { + if (amount <= 0 || companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return false; + + // Check if the station has a bank account + if (!TryComp<StationBankAccountComponent>(station, out var bank)) + return false; + + var company = stockMarket.Companies[companyIndex]; + var totalValue = (int)Math.Round(company.CurrentPrice * amount); + + // See if we can afford it + if (bank.Balance < totalValue) + return false; + + if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned)) + currentOwned = 0; + + // Update the bank account + _cargo.UpdateBankAccount(station, bank, -totalValue); + stockMarket.StockOwnership[companyIndex] = currentOwned + amount; + + // Log the transaction + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"[StockMarket] Bought {amount} stocks of {company.LocalizedDisplayName} at {company.CurrentPrice:F2} credits each (Total: {totalValue})"); + + return true; + } + + private bool TrySellStocks( + EntityUid station, + StationStockMarketComponent stockMarket, + int companyIndex, + int amount) + { + if (amount <= 0 || companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return false; + + // Check if the station has a bank account + if (!TryComp<StationBankAccountComponent>(station, out var bank)) + return false; + + if (!stockMarket.StockOwnership.TryGetValue(companyIndex, out var currentOwned) || currentOwned < amount) + return false; + + var company = stockMarket.Companies[companyIndex]; + var totalValue = (int)Math.Round(company.CurrentPrice * amount); + + // Update stock ownership + var newAmount = currentOwned - amount; + if (newAmount > 0) + stockMarket.StockOwnership[companyIndex] = newAmount; + else + stockMarket.StockOwnership.Remove(companyIndex); + + // Update the bank account + _cargo.UpdateBankAccount(station, bank, totalValue); + + // Log the transaction + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"[StockMarket] Sold {amount} stocks of {company.LocalizedDisplayName} at {company.CurrentPrice:F2} credits each (Total: {totalValue})"); + + return true; + } + + private void UpdateStockPrices(EntityUid station, StationStockMarketComponent stockMarket) + { + for (var i = 0; i < stockMarket.Companies.Count; i++) + { + var company = stockMarket.Companies[i]; + var changeType = DetermineMarketChange(stockMarket.MarketChanges); + var multiplier = CalculatePriceMultiplier(changeType); + + UpdatePriceHistory(company); + + // Update price with multiplier + var oldPrice = company.CurrentPrice; + company.CurrentPrice *= (1 + multiplier); + + // Ensure price doesn't go below minimum threshold + company.CurrentPrice = MathF.Max(company.CurrentPrice, company.BasePrice * 0.1f); + + // Ensure price doesn't go above maximum threshold + company.CurrentPrice = MathF.Min(company.CurrentPrice, MaxPrice); + + stockMarket.Companies[i] = company; + + // Calculate the percentage change + var percentChange = (company.CurrentPrice - oldPrice) / oldPrice * 100; + + // Raise the event + var ev = new StockMarketUpdatedEvent(station); + RaiseLocalEvent(ev); + + // Log it + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"[StockMarket] Company '{company.LocalizedDisplayName}' price updated by {percentChange:+0.00;-0.00}% from {oldPrice:0.00} to {company.CurrentPrice:0.00}"); + } + } + + /// <summary> + /// Attempts to change the price for a specific company + /// </summary> + /// <returns>True if the operation was successful, false otherwise</returns> + public bool TryChangeStocksPrice(EntityUid station, + StationStockMarketComponent stockMarket, + float newPrice, + int companyIndex) + { + // Check if it exceeds the max price + if (newPrice > MaxPrice) + { + _sawmill.Error($"New price cannot be greater than {MaxPrice}."); + return false; + } + + if (companyIndex < 0 || companyIndex >= stockMarket.Companies.Count) + return false; + + var company = stockMarket.Companies[companyIndex]; + UpdatePriceHistory(company); + + company.CurrentPrice = MathF.Max(newPrice, company.BasePrice * 0.1f); + stockMarket.Companies[companyIndex] = company; + + var ev = new StockMarketUpdatedEvent(station); + RaiseLocalEvent(ev); + return true; + } + + /// <summary> + /// Attempts to add a new company to the station + /// </summary> + /// <returns>False if the company already exists, true otherwise</returns> + public bool TryAddCompany(EntityUid station, + StationStockMarketComponent stockMarket, + float basePrice, + string displayName) + { + // Create a new company struct with the specified parameters + var company = new StockCompanyStruct + { + LocalizedDisplayName = displayName, // Assume there's no Loc for it + BasePrice = basePrice, + CurrentPrice = basePrice, + PriceHistory = [], + }; + + stockMarket.Companies.Add(company); + UpdatePriceHistory(company); + + var ev = new StockMarketUpdatedEvent(station); + RaiseLocalEvent(ev); + + return true; + } + + /// <summary> + /// Attempts to add a new company to the station using the StockCompanyStruct + /// </summary> + /// <returns>False if the company already exists, true otherwise</returns> + public bool TryAddCompany(EntityUid station, + StationStockMarketComponent stockMarket, + StockCompanyStruct company) + { + // Add the new company to the dictionary + stockMarket.Companies.Add(company); + + // Make sure it has a price history + UpdatePriceHistory(company); + + var ev = new StockMarketUpdatedEvent(station); + RaiseLocalEvent(ev); + + return true; + } + + private static void UpdatePriceHistory(StockCompanyStruct company) + { + // Create if null + company.PriceHistory ??= []; + + // Make sure it has at least 5 entries + while (company.PriceHistory.Count < 5) + { + company.PriceHistory.Add(company.BasePrice); + } + + // Store previous price in history + company.PriceHistory.Add(company.CurrentPrice); + + if (company.PriceHistory.Count > 5) // Keep last 5 prices + company.PriceHistory.RemoveAt(1); // Always keep the base price + } + + private MarketChange DetermineMarketChange(List<MarketChange> marketChanges) + { + var roll = _random.NextFloat(); + var cumulative = 0f; + + foreach (var change in marketChanges) + { + cumulative += change.Chance; + if (roll <= cumulative) + return change; + } + + return marketChanges[0]; // Default to first (usually minor) change if we somehow exceed 100% + } + + private float CalculatePriceMultiplier(MarketChange change) + { + // Using Box-Muller transform for normal distribution + var u1 = _random.NextFloat(); + var u2 = _random.NextFloat(); + var randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); + + // Scale and shift the result to our desired range + var range = change.Range.Y - change.Range.X; + var mean = (change.Range.Y + change.Range.X) / 2; + var stdDev = range / 6.0f; // 99.7% of values within range + + var result = (float)(mean + (stdDev * randStdNormal)); + return Math.Clamp(result, change.Range.X, change.Range.Y); + } +} +public sealed class StockMarketUpdatedEvent(EntityUid station) : EntityEventArgs +{ + public EntityUid Station = station; +} diff --git a/Content.Server/Carrying/CarryingSystem.cs b/Content.Server/Carrying/CarryingSystem.cs index ca69d2f9299..72cfeff91d5 100644 --- a/Content.Server/Carrying/CarryingSystem.cs +++ b/Content.Server/Carrying/CarryingSystem.cs @@ -139,7 +139,7 @@ private void OnThrow(EntityUid uid, CarryingComponent component, ref BeforeThrow args.ItemUid = virtItem.BlockingEntity; - args.ThrowStrength *= _contests.MassContest(uid, virtItem.BlockingEntity, false, 2f) + args.ThrowSpeed *= _contests.MassContest(uid, virtItem.BlockingEntity, false, 2f) * _contests.StaminaContest(uid, virtItem.BlockingEntity); } @@ -250,8 +250,7 @@ private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableCo var ev = new CarryDoAfterEvent(); var args = new DoAfterArgs(EntityManager, carrier, length, ev, carried, target: carried) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }; diff --git a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs index 7896a7822e2..894961a004c 100644 --- a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs +++ b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs @@ -414,6 +414,7 @@ private void OnUiMessage(EntityUid uid, CartridgeLoaderComponent component, Cart { var cartridgeEvent = args.MessageEvent; cartridgeEvent.LoaderUid = GetNetEntity(uid); + cartridgeEvent.Actor = args.Actor; RelayEvent(component, cartridgeEvent, true); } diff --git a/Content.Server/CartridgeLoader/Cartridges/AstroNavCartridgeComponent.cs b/Content.Server/CartridgeLoader/Cartridges/AstroNavCartridgeComponent.cs new file mode 100644 index 00000000000..fd616a1c803 --- /dev/null +++ b/Content.Server/CartridgeLoader/Cartridges/AstroNavCartridgeComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.GPS; + +namespace Content.Server.CartridgeLoader.Cartridges; + +[RegisterComponent] +public sealed partial class AstroNavCartridgeComponent : Component +{ +} diff --git a/Content.Server/CartridgeLoader/Cartridges/AstroNavCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/AstroNavCartridgeSystem.cs new file mode 100644 index 00000000000..60d14789fa0 --- /dev/null +++ b/Content.Server/CartridgeLoader/Cartridges/AstroNavCartridgeSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.GPS.Components; + +namespace Content.Server.CartridgeLoader.Cartridges; + +public sealed class AstroNavCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<AstroNavCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded); + SubscribeLocalEvent<AstroNavCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved); + } + + private void OnCartridgeAdded(Entity<AstroNavCartridgeComponent> ent, ref CartridgeAddedEvent args) + { + EnsureComp<HandheldGPSComponent>(args.Loader); + } + + private void OnCartridgeRemoved(Entity<AstroNavCartridgeComponent> ent, ref CartridgeRemovedEvent args) + { + // only remove when the program itself is removed + if (!_cartridgeLoaderSystem.HasProgram<AstroNavCartridgeComponent>(args.Loader)) + { + RemComp<HandheldGPSComponent>(args.Loader); + } + } +} diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs index cfa92dd67f7..048fa777fc9 100644 --- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs +++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV using Robust.Shared.Audio; namespace Content.Server.CartridgeLoader.Cartridges; @@ -18,4 +19,10 @@ public sealed partial class LogProbeCartridgeComponent : Component /// </summary> [DataField, ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg"); + + /// <summary> + /// DeltaV: The last scanned NanoChat data, if any + /// </summary> + [DataField] + public NanoChatData? ScannedNanoChatData; } diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs index f5ccea95900..725901620d0 100644 --- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs +++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs @@ -2,13 +2,14 @@ using Content.Shared.Audio; using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.NanoChat; // DeltaV using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Random; namespace Content.Server.CartridgeLoader.Cartridges; -public sealed class LogProbeCartridgeSystem : EntitySystem +public sealed partial class LogProbeCartridgeSystem : EntitySystem // DeltaV - Made partial { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!; @@ -18,6 +19,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem public override void Initialize() { base.Initialize(); + InitializeNanoChat(); // DeltaV SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady); SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract); } @@ -33,6 +35,15 @@ private void AfterInteract(Entity<LogProbeCartridgeComponent> ent, ref Cartridge if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target) return; + // DeltaV begin - Add NanoChat card scanning + if (TryComp<NanoChatCardComponent>(target, out var nanoChatCard)) + { + ScanNanoChatCard(ent, args, target, nanoChatCard); + args.InteractEvent.Handled = true; + return; + } + // DeltaV end + if (!TryComp(target, out AccessReaderComponent? accessReaderComponent)) return; @@ -41,6 +52,7 @@ private void AfterInteract(Entity<LogProbeCartridgeComponent> ent, ref Cartridge _popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User); ent.Comp.PulledAccessLogs.Clear(); + ent.Comp.ScannedNanoChatData = null; // DeltaV - Clear any previous NanoChat data foreach (var accessRecord in accessReaderComponent.AccessLog) { @@ -65,7 +77,7 @@ private void OnUiReady(Entity<LogProbeCartridgeComponent> ent, ref CartridgeUiRe private void UpdateUiState(Entity<LogProbeCartridgeComponent> ent, EntityUid loaderUid) { - var state = new LogProbeUiState(ent.Comp.PulledAccessLogs); + var state = new LogProbeUiState(ent.Comp.PulledAccessLogs, ent.Comp.ScannedNanoChatData); // DeltaV - NanoChat support _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state); } } diff --git a/Content.Server/CartridgeLoader/Cartridges/MedTekCartridgeComponent.cs b/Content.Server/CartridgeLoader/Cartridges/MedTekCartridgeComponent.cs new file mode 100644 index 00000000000..1a49b4d1f77 --- /dev/null +++ b/Content.Server/CartridgeLoader/Cartridges/MedTekCartridgeComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.CartridgeLoader.Cartridges; + +[RegisterComponent] +public sealed partial class MedTekCartridgeComponent : Component +{ +} diff --git a/Content.Server/CartridgeLoader/Cartridges/MedTekCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/MedTekCartridgeSystem.cs new file mode 100644 index 00000000000..4d1b71dad04 --- /dev/null +++ b/Content.Server/CartridgeLoader/Cartridges/MedTekCartridgeSystem.cs @@ -0,0 +1,31 @@ +using Content.Server.Medical.Components; +using Content.Shared.CartridgeLoader; + +namespace Content.Server.CartridgeLoader.Cartridges; + +public sealed class MedTekCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<MedTekCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded); + SubscribeLocalEvent<MedTekCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved); + } + + private void OnCartridgeAdded(Entity<MedTekCartridgeComponent> ent, ref CartridgeAddedEvent args) + { + var healthAnalyzer = EnsureComp<HealthAnalyzerComponent>(args.Loader); + } + + private void OnCartridgeRemoved(Entity<MedTekCartridgeComponent> ent, ref CartridgeRemovedEvent args) + { + // only remove when the program itself is removed + if (!_cartridgeLoaderSystem.HasProgram<MedTekCartridgeComponent>(args.Loader)) + { + RemComp<HealthAnalyzerComponent>(args.Loader); + } + } +} diff --git a/Content.Server/Chapel/SacrificialAltarSystem.cs b/Content.Server/Chapel/SacrificialAltarSystem.cs index e7144e1185b..f674f263f7a 100644 --- a/Content.Server/Chapel/SacrificialAltarSystem.cs +++ b/Content.Server/Chapel/SacrificialAltarSystem.cs @@ -119,8 +119,7 @@ protected override void AttemptSacrifice(Entity<SacrificialAltarComponent> ent, var args = new DoAfterArgs(EntityManager, user, ent.Comp.SacrificeTime, ev, target: target, eventTarget: ent) { BreakOnDamage = true, - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, BreakOnWeightlessMove = true, NeedHand = true }; diff --git a/Content.Server/Charges/Components/AutoRechargeComponent.cs b/Content.Server/Charges/Components/AutoRechargeComponent.cs index 9dcf555ea93..165b181dcbc 100644 --- a/Content.Server/Charges/Components/AutoRechargeComponent.cs +++ b/Content.Server/Charges/Components/AutoRechargeComponent.cs @@ -7,6 +7,7 @@ namespace Content.Server.Charges.Components; /// Something with limited charges that can be recharged automatically. /// Requires LimitedChargesComponent to function. /// </summary> +// TODO: no reason this cant be predicted and server system deleted [RegisterComponent, AutoGenerateComponentPause] [Access(typeof(ChargesSystem))] public sealed partial class AutoRechargeComponent : Component diff --git a/Content.Server/Charges/Systems/ChargesSystem.cs b/Content.Server/Charges/Systems/ChargesSystem.cs index 03e192e680e..974928ee4bb 100644 --- a/Content.Server/Charges/Systems/ChargesSystem.cs +++ b/Content.Server/Charges/Systems/ChargesSystem.cs @@ -37,15 +37,17 @@ protected override void OnExamine(EntityUid uid, LimitedChargesComponent comp, E args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining))); } - public override void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null) + public override void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null) { - if (!Resolve(uid, ref comp, false)) + if (!Query.Resolve(uid, ref comp, false)) return; var startRecharge = comp.Charges == comp.MaxCharges; - base.UseCharge(uid, comp); - // start the recharge time after first use at full charge - if (startRecharge && TryComp<AutoRechargeComponent>(uid, out var recharge)) + base.AddCharges(uid, change, comp); + + // if a charge was just used from full, start the recharge timer + // TODO: probably make this an event instead of having le server system that just does this + if (change < 0 && startRecharge && TryComp<AutoRechargeComponent>(uid, out var recharge)) recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration; } } diff --git a/Content.Server/Chat/Commands/SuicideCommand.cs b/Content.Server/Chat/Commands/SuicideCommand.cs index c967ba78d7f..7f6e88e8b54 100644 --- a/Content.Server/Chat/Commands/SuicideCommand.cs +++ b/Content.Server/Chat/Commands/SuicideCommand.cs @@ -1,5 +1,8 @@ using Content.Server.GameTicking; +using Content.Server.Popups; +using Content.Server.Popups; using Content.Shared.Administration; +using Content.Shared.Chat; using Content.Shared.Mind; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -9,6 +12,8 @@ namespace Content.Server.Chat.Commands [AnyCommand] internal sealed class SuicideCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _entityManager = default!; + public string Command => "suicide"; public string Description => Loc.GetString("suicide-command-description"); @@ -19,36 +24,39 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) { if (shell.Player is not { } player) { - shell.WriteLine(Loc.GetString("shell-cannot-run-command-from-server")); + shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server")); return; } if (player.Status != SessionStatus.InGame || player.AttachedEntity == null) return; - var minds = IoCManager.Resolve<IEntityManager>().System<SharedMindSystem>(); + var minds = _entityManager.System<SharedMindSystem>(); + // This check also proves mind not-null for at the end when the mob is ghosted. - if (!minds.TryGetMind(player, out var mindId, out var mind) || - mind.OwnedEntity is not { Valid: true } victim) + if (!minds.TryGetMind(player, out var mindId, out var mindComp) || + mindComp.OwnedEntity is not { Valid: true } victim) { - shell.WriteLine("You don't have a mind!"); + shell.WriteLine(Loc.GetString("suicide-command-no-mind")); return; } - var gameTicker = EntitySystem.Get<GameTicker>(); - var suicideSystem = EntitySystem.Get<SuicideSystem>(); - if (suicideSystem.Suicide(victim)) + var suicideSystem = _entityManager.System<SuicideSystem>(); + + if (_entityManager.HasComponent<AdminFrozenComponent>(victim)) { - // Prevent the player from returning to the body. - // Note that mind cannot be null because otherwise victim would be null. - gameTicker.OnGhostAttempt(mindId, false, mind: mind); + var deniedMessage = Loc.GetString("suicide-command-denied"); + shell.WriteLine(deniedMessage); + _entityManager.System<PopupSystem>() + .PopupEntity(deniedMessage, victim, victim); return; } - if (gameTicker.OnGhostAttempt(mindId, true, mind: mind)) + if (suicideSystem.Suicide(victim)) return; - shell.WriteLine("You can't ghost right now."); + shell.WriteLine(Loc.GetString("ghost-command-denied")); + shell.WriteLine(Loc.GetString("ghost-command-denied")); } } } diff --git a/Content.Server/Chat/SuicideSystem.cs b/Content.Server/Chat/SuicideSystem.cs index 1cc478696ca..884292b0fa7 100644 --- a/Content.Server/Chat/SuicideSystem.cs +++ b/Content.Server/Chat/SuicideSystem.cs @@ -1,143 +1,154 @@ -using Content.Server.Administration.Logs; -using Content.Server.Popups; +using Content.Server.GameTicking; using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; using Content.Shared.Database; using Content.Shared.Hands.Components; using Content.Shared.Interaction.Events; using Content.Shared.Item; +using Content.Shared.Mind; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Content.Shared.Tag; using Robust.Shared.Player; -using Robust.Shared.Prototypes; +using Content.Shared.Administration.Logs; +using Content.Shared.Chat; +using Content.Shared.Mind.Components; -namespace Content.Server.Chat +namespace Content.Server.Chat; + +public sealed class SuicideSystem : EntitySystem { - public sealed class SuicideSystem : EntitySystem + [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly SharedSuicideSystem _suicide = default!; + + public override void Initialize() { - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - - public bool Suicide(EntityUid victim) - { - // Checks to see if the CannotSuicide tag exits, ghosts instead. - if (_tagSystem.HasTag(victim, "CannotSuicide")) - return false; - - // Checks to see if the player is dead. - if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState)) - return false; + base.Initialize(); - _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide"); + SubscribeLocalEvent<DamageableComponent, SuicideEvent>(OnDamageableSuicide); + SubscribeLocalEvent<MobStateComponent, SuicideEvent>(OnEnvironmentalSuicide); + SubscribeLocalEvent<MindContainerComponent, SuicideGhostEvent>(OnSuicideGhost); + } - var suicideEvent = new SuicideEvent(victim); + /// <summary> + /// Calling this function will attempt to kill the user by suiciding on objects in the surrounding area + /// or by applying a lethal amount of damage to the user with the default method. + /// Used when writing /suicide + /// </summary> + public bool Suicide(EntityUid victim) + { + // Can't suicide if we're already dead + if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState)) + return false; - //Check to see if there were any systems blocking this suicide - if (SuicideAttemptBlocked(victim, suicideEvent)) - return false; + var suicideGhostEvent = new SuicideGhostEvent(victim); + RaiseLocalEvent(victim, suicideGhostEvent); - return false; // DeltaV - Prevent Suicide. We allow the event to go out anyways in case anything relies on the event for a message or whatever. + // Suicide is considered a fail if the user wasn't able to ghost + // Suiciding with the CannotSuicide tag will ghost the player but not kill the body + if (!suicideGhostEvent.Handled || _tagSystem.HasTag(victim, "CannotSuicide")) + return false; - bool environmentSuicide = false; - // If you are critical, you wouldn't be able to use your surroundings to suicide, so you do the default suicide - if (!_mobState.IsCritical(victim, mobState)) - { - environmentSuicide = EnvironmentSuicideHandler(victim, suicideEvent); - } + _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide"); + var suicideEvent = new SuicideEvent(victim); + RaiseLocalEvent(victim, suicideEvent); - if (suicideEvent.AttemptBlocked) - return false; + _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided."); + return true; + } - DefaultSuicideHandler(victim, suicideEvent); + /// <summary> + /// Event subscription created to handle the ghosting aspect relating to suicides + /// Mainly useful when you can raise an event in Shared and can't call Suicide() directly + /// </summary> + private void OnSuicideGhost(Entity<MindContainerComponent> victim, ref SuicideGhostEvent args) + { + if (args.Handled) + return; - ApplyDeath(victim, suicideEvent.Kind!.Value); - _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided{(environmentSuicide ? " (environment)" : "")}"); - return true; - } + if (victim.Comp.Mind == null) + return; - /// <summary> - /// If not handled, does the default suicide, which is biting your own tongue - /// </summary> - private void DefaultSuicideHandler(EntityUid victim, SuicideEvent suicideEvent) - { - if (suicideEvent.Handled) - return; + if (!TryComp<MindComponent>(victim.Comp.Mind, out var mindComponent)) + return; - var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim)); - _popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true); + // CannotSuicide tag will allow the user to ghost, but also return to their mind + // This is kind of weird, not sure what it applies to? + if (_tagSystem.HasTag(victim, "CannotSuicide")) + args.CanReturnToBody = true; - var selfMessage = Loc.GetString("suicide-command-default-text-self"); - _popup.PopupEntity(selfMessage, victim, victim); - suicideEvent.SetHandled(SuicideKind.Bloodloss); - } + if (_gameTicker.OnGhostAttempt(victim.Comp.Mind.Value, args.CanReturnToBody, mind: mindComponent)) + args.Handled = true; + } - /// <summary> - /// Checks to see if there are any other systems that prevent suicide - /// </summary> - /// <returns>Returns true if there was a blocked attempt</returns> - private bool SuicideAttemptBlocked(EntityUid victim, SuicideEvent suicideEvent) - { - RaiseLocalEvent(victim, suicideEvent, true); + /// <summary> + /// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide + /// </summary> + private void OnEnvironmentalSuicide(Entity<MobStateComponent> victim, ref SuicideEvent args) + { + if (args.Handled || _mobState.IsCritical(victim)) + return; - if (suicideEvent.AttemptBlocked) - return true; + var suicideByEnvironmentEvent = new SuicideByEnvironmentEvent(victim); - return false; + // Try to suicide by raising an event on the held item + if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent) + && handsComponent.ActiveHandEntity is { } item) + { + RaiseLocalEvent(item, suicideByEnvironmentEvent); + if (suicideByEnvironmentEvent.Handled) + { + args.Handled = suicideByEnvironmentEvent.Handled; + return; + } } - /// <summary> - /// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide - /// </summary> - private bool EnvironmentSuicideHandler(EntityUid victim, SuicideEvent suicideEvent) + // Try to suicide by nearby entities, like Microwaves or Crematoriums, by raising an event on it + // Returns upon being handled by any entity + var itemQuery = GetEntityQuery<ItemComponent>(); + foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static)) { - var itemQuery = GetEntityQuery<ItemComponent>(); - - // Suicide by held item - if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent) - && handsComponent.ActiveHandEntity is { } item) - { - RaiseLocalEvent(item, suicideEvent, false); + // Skip any nearby items that can be picked up, we already checked the active held item above + if (itemQuery.HasComponent(entity)) + continue; - if (suicideEvent.Handled) - return true; - } + RaiseLocalEvent(entity, suicideByEnvironmentEvent); + if (!suicideByEnvironmentEvent.Handled) + continue; - // Suicide by nearby entity (ex: Microwave) - foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static)) - { - // Skip any nearby items that can be picked up, we already checked the active held item above - if (itemQuery.HasComponent(entity)) - continue; + args.Handled = suicideByEnvironmentEvent.Handled; + return; + } + } - RaiseLocalEvent(entity, suicideEvent); + /// <summary> + /// Default suicide behavior for any kind of entity that can take damage + /// </summary> + private void OnDamageableSuicide(Entity<DamageableComponent> victim, ref SuicideEvent args) + { + if (args.Handled) + return; - if (suicideEvent.Handled) - return true; - } + var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim)); + _popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true); - return false; - } + var selfMessage = Loc.GetString("suicide-command-default-text-self"); + _popup.PopupEntity(selfMessage, victim, victim); - private void ApplyDeath(EntityUid target, SuicideKind kind) + if (args.DamageSpecifier != null) { - if (kind == SuicideKind.Special) - return; - - if (!_prototypeManager.TryIndex<DamageTypePrototype>(kind.ToString(), out var damagePrototype)) - { - const SuicideKind fallback = SuicideKind.Blunt; - Log.Error($"{nameof(SuicideSystem)} could not find the damage type prototype associated with {kind}. Falling back to {fallback}"); - damagePrototype = _prototypeManager.Index<DamageTypePrototype>(fallback.ToString()); - } - const int lethalAmountOfDamage = 200; // TODO: Would be nice to get this number from somewhere else - _damageableSystem.TryChangeDamage(target, new(damagePrototype, lethalAmountOfDamage), true, origin: target); + _suicide.ApplyLethalDamage(victim, args.DamageSpecifier); + args.Handled = true; + return; } + + args.DamageType ??= "Bloodloss"; + _suicide.ApplyLethalDamage(victim, args.DamageType); + args.Handled = true; } } diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Server/Chat/Systems/ChatSystem.Emote.cs index 3ee94072ca2..7e8ec4cf89f 100644 --- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs +++ b/Content.Server/Chat/Systems/ChatSystem.Emote.cs @@ -51,19 +51,21 @@ private void CacheEmotes() /// <param name="emoteId">The id of emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param> /// <param name="hideLog">Whether or not this message should appear in the adminlog window</param> /// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param> - /// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerSpeechEvent"/>. If this is set, the event will not get raised.</param> + /// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param> + /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param> public void TryEmoteWithChat( EntityUid source, string emoteId, ChatTransmitRange range = ChatTransmitRange.Normal, bool hideLog = false, string? nameOverride = null, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + bool forceEmote = false ) { if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto)) return; - TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker); + TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote); } /// <summary> @@ -74,22 +76,19 @@ public void TryEmoteWithChat( /// <param name="hideLog">Whether or not this message should appear in the adminlog window</param> /// <param name="hideChat">Whether or not this message should appear in the chat window</param> /// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param> - /// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerSpeechEvent"/>. If this is set, the event will not get raised.</param> + /// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param> + /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param> public void TryEmoteWithChat( EntityUid source, EmotePrototype emote, ChatTransmitRange range = ChatTransmitRange.Normal, bool hideLog = false, string? nameOverride = null, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + bool forceEmote = false ) { - if (_whitelistSystem.IsWhitelistFailOrNull(emote.Whitelist, source) || _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)) - return; - - if (!emote.Available && - TryComp<SpeechComponent>(source, out var speech) && - !speech.AllowedEmotes.Contains(emote.ID)) + if (!forceEmote && !AllowedToUseEmote(source, emote)) return; // check if proto has valid message for chat @@ -159,7 +158,11 @@ public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string _audio.PlayPvs(sound, uid, param); return true; } - + /// <summary> + /// Checks if a valid emote was typed, to play sounds and etc and invokes an event. + /// </summary> + /// <param name="uid"></param> + /// <param name="textInput"></param> private void TryEmoteChatInput(EntityUid uid, string textInput) { var actionLower = textInput.ToLower(); @@ -170,8 +173,35 @@ private void TryEmoteChatInput(EntityUid uid, string textInput) if (!_wordEmoteDict.TryGetValue(actionLower, out var emote)) return; + if (!AllowedToUseEmote(uid, emote)) + return; + InvokeEmoteEvent(uid, emote); } + /// <summary> + /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity. + /// </summary> + /// <param name="source">The entity that is speaking</param> + /// <param name="emote">The emote being used</param> + /// <returns></returns> + private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote) + { + // If emote is in AllowedEmotes, it will bypass whitelist and blacklist + if (TryComp<SpeechComponent>(source, out var speech) && + speech.AllowedEmotes.Contains(emote.ID)) + return true; + + // Check the whitelist and blacklist + if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || + _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)) + return false; + + // Check if the emote is available for all + if (!emote.Available) + return false; + + return true; + } private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto) { diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index f3de9a4f95a..c8cf93d5a29 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -24,7 +24,6 @@ using Content.Shared.Players; using Content.Shared.Players.RateLimiting; using Content.Shared.Radio; -using Content.Shared.Speech; using Content.Shared.Whitelist; using Robust.Server.Player; using Robust.Shared.Audio; @@ -424,7 +423,7 @@ private void SendEntitySpeak( } else { - var nameEv = new TransformSpeakerSpeechEvent(source, Name(source)); + var nameEv = new TransformSpeakerNameEvent(source, Name(source)); RaiseLocalEvent(source, nameEv); name = nameEv.VoiceName ?? Name(source); // Check for a speech verb override @@ -496,9 +495,9 @@ private void SendEntityWhisper( } else { - var nameEv = new TransformSpeakerSpeechEvent(source, Name(source)); + var nameEv = new TransformSpeakerNameEvent(source, Name(source)); RaiseLocalEvent(source, nameEv); - name = nameEv.VoiceName ?? Name(source); + name = nameEv.VoiceName; } name = FormattedMessage.EscapeText(name); diff --git a/Content.Server/Chat/V2/Commands/DeleteChatMessageCommand.cs b/Content.Server/Chat/V2/Commands/DeleteChatMessageCommand.cs index 1f9203d299f..2aeb8998292 100644 --- a/Content.Server/Chat/V2/Commands/DeleteChatMessageCommand.cs +++ b/Content.Server/Chat/V2/Commands/DeleteChatMessageCommand.cs @@ -14,7 +14,7 @@ public sealed class DeleteChatMessageCommand : ToolshedCommand [Dependency] private readonly IEntitySystemManager _manager = default!; [CommandImplementation("id")] - public void DeleteChatMessage([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] uint messageId) + public void DeleteChatMessage(IInvocationContext ctx, uint messageId) { if (!_manager.GetEntitySystem<ChatRepositorySystem>().Delete(messageId)) { diff --git a/Content.Server/Chat/V2/Commands/NukeChatMessagesCommand.cs b/Content.Server/Chat/V2/Commands/NukeChatMessagesCommand.cs index 3d8b69dd765..07b4cd97968 100644 --- a/Content.Server/Chat/V2/Commands/NukeChatMessagesCommand.cs +++ b/Content.Server/Chat/V2/Commands/NukeChatMessagesCommand.cs @@ -14,7 +14,7 @@ public sealed class NukeChatMessagesCommand : ToolshedCommand [Dependency] private readonly IEntitySystemManager _manager = default!; [CommandImplementation("usernames")] - public void Command([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] string usernamesCsv) + public void Command(IInvocationContext ctx, string usernamesCsv) { var usernames = usernamesCsv.Split(','); diff --git a/Content.Server/Chemistry/Components/TransformableContainerComponent.cs b/Content.Server/Chemistry/Components/TransformableContainerComponent.cs index 5ea9a244878..db6c9c53976 100644 --- a/Content.Server/Chemistry/Components/TransformableContainerComponent.cs +++ b/Content.Server/Chemistry/Components/TransformableContainerComponent.cs @@ -14,14 +14,6 @@ namespace Content.Server.Chemistry.Components; [RegisterComponent, Access(typeof(TransformableContainerSystem))] public sealed partial class TransformableContainerComponent : Component { - /// <summary> - /// This is the initial metadata name for the container. - /// It will revert to this when emptied. - /// It defaults to the name of the parent entity unless overwritten. - /// </summary> - [DataField("initialName")] - public string? InitialName; - /// <summary> /// This is the initial metadata description for the container. /// It will revert to this when emptied. diff --git a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs index 56cc0f96709..29a03485db4 100644 --- a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs +++ b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs @@ -1,7 +1,8 @@ using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Chemistry; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Forensics; @@ -17,6 +18,7 @@ using Robust.Shared.GameStates; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Shared.Chemistry.Reagent; using Robust.Server.Audio; namespace Content.Server.Chemistry.EntitySystems; @@ -88,14 +90,44 @@ public bool TryDoInject(Entity<HyposprayComponent> entity, EntityUid target, Ent string? msgFormat = null; - if (target == user) - msgFormat = "hypospray-component-inject-self-message"; - else if (EligibleEntity(user, EntityManager, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance)) + // Self event + var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target); + RaiseLocalEvent(user, selfEvent); + + if (selfEvent.Cancelled) + { + _popup.PopupEntity(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); + return false; + } + + target = selfEvent.TargetGettingInjected; + + if (!EligibleEntity(target, EntityManager, component)) + return false; + + // Target event + var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target); + RaiseLocalEvent(target, targetEvent); + + if (targetEvent.Cancelled) { - msgFormat = "hypospray-component-inject-self-clumsy-message"; - target = user; + _popup.PopupEntity(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); + return false; } + target = targetEvent.TargetGettingInjected; + + if (!EligibleEntity(target, EntityManager, component)) + return false; + + // The target event gets priority for the overriden message. + if (targetEvent.InjectMessageOverride != null) + msgFormat = targetEvent.InjectMessageOverride; + else if (selfEvent.InjectMessageOverride != null) + msgFormat = selfEvent.InjectMessageOverride; + else if (target == user) + msgFormat = "hypospray-component-inject-self-message"; + if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0) { _popup.PopupEntity(Loc.GetString("hypospray-component-empty-message"), target, user); diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs index 9dd1d429b7c..77c8620ea49 100644 --- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs @@ -205,9 +205,11 @@ private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, DoAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) { - BreakOnUserMove = true, + BreakOnMove = true, + BreakOnWeightlessMove = false, BreakOnDamage = true, - BreakOnTargetMove = true, + NeedHand = true, + BreakOnHandChange = true, MovementThreshold = 0.1f, }); } diff --git a/Content.Server/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs index a942d34e7a8..45a85010b19 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Power; namespace Content.Server.Chemistry.EntitySystems; diff --git a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs index 1ef589ab5cb..a9aaa211c77 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Placeable; +using Content.Shared.Power; namespace Content.Server.Chemistry.EntitySystems; diff --git a/Content.Server/Chemistry/EntitySystems/TransformableContainerSystem.cs b/Content.Server/Chemistry/EntitySystems/TransformableContainerSystem.cs index c375d97b8c3..32bd912b220 100644 --- a/Content.Server/Chemistry/EntitySystems/TransformableContainerSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/TransformableContainerSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Chemistry.Containers.EntitySystems; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; +using Content.Shared.NameModifier.EntitySystems; using Robust.Shared.Prototypes; namespace Content.Server.Chemistry.EntitySystems; @@ -11,6 +12,7 @@ public sealed class TransformableContainerSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SolutionContainerSystem _solutionsSystem = default!; [Dependency] private readonly MetaDataSystem _metadataSystem = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; public override void Initialize() { @@ -18,15 +20,12 @@ public override void Initialize() SubscribeLocalEvent<TransformableContainerComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<TransformableContainerComponent, SolutionContainerChangedEvent>(OnSolutionChange); + SubscribeLocalEvent<TransformableContainerComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } - private void OnMapInit(Entity<TransformableContainerComponent> entity, ref MapInitEvent args) + private void OnMapInit(Entity<TransformableContainerComponent> entity, ref MapInitEvent args) { var meta = MetaData(entity.Owner); - if (string.IsNullOrEmpty(entity.Comp.InitialName)) - { - entity.Comp.InitialName = meta.EntityName; - } if (string.IsNullOrEmpty(entity.Comp.InitialDescription)) { entity.Comp.InitialDescription = meta.EntityDescription; @@ -58,12 +57,20 @@ private void OnSolutionChange(Entity<TransformableContainerComponent> entity, re && _prototypeManager.TryIndex(reagentId.Value.Prototype, out ReagentPrototype? proto)) { var metadata = MetaData(entity.Owner); - var val = Loc.GetString("transformable-container-component-glass", ("name", proto.LocalizedName)); - _metadataSystem.SetEntityName(entity.Owner, val, metadata); _metadataSystem.SetEntityDescription(entity.Owner, proto.LocalizedDescription, metadata); entity.Comp.CurrentReagent = proto; entity.Comp.Transformed = true; } + + _nameMod.RefreshNameModifiers(entity.Owner); + } + + private void OnRefreshNameModifiers(Entity<TransformableContainerComponent> entity, ref RefreshNameModifiersEvent args) + { + if (entity.Comp.CurrentReagent is { } currentReagent) + { + args.AddModifier("transformable-container-component-glass", priority: -1, ("reagent", currentReagent.LocalizedName)); + } } private void CancelTransformation(Entity<TransformableContainerComponent> entity) @@ -73,10 +80,8 @@ private void CancelTransformation(Entity<TransformableContainerComponent> entity var metadata = MetaData(entity); - if (!string.IsNullOrEmpty(entity.Comp.InitialName)) - { - _metadataSystem.SetEntityName(entity.Owner, entity.Comp.InitialName, metadata); - } + _nameMod.RefreshNameModifiers(entity.Owner); + if (!string.IsNullOrEmpty(entity.Comp.InitialDescription)) { _metadataSystem.SetEntityDescription(entity.Owner, entity.Comp.InitialDescription, metadata); diff --git a/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs deleted file mode 100644 index 024558f8de3..00000000000 --- a/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Content.Server.Fluids.EntitySystems; -using Content.Shared.Audio; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Coordinates.Helpers; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Maps; -using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Chemistry.ReactionEffects -{ - /// <summary> - /// Basically smoke and foam reactions. - /// </summary> - [UsedImplicitly] - [DataDefinition] - public sealed partial class AreaReactionEffect : ReagentEffect - { - /// <summary> - /// How many seconds will the effect stay, counting after fully spreading. - /// </summary> - [DataField("duration")] private float _duration = 10; - - /// <summary> - /// How many units of reaction for 1 smoke entity. - /// </summary> - [DataField] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5); - - /// <summary> - /// The entity prototype that will be spawned as the effect. - /// </summary> - [DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] - private string _prototypeId = default!; - - /// <summary> - /// Sound that will get played when this reaction effect occurs. - /// </summary> - [DataField("sound", required: true)] private SoundSpecifier _sound = default!; - - public override bool ShouldLog => true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-missing"); - - public override LogImpact LogImpact => LogImpact.High; - - public override void Effect(ReagentEffectArgs args) - { - if (args.Source == null) - return; - - var spreadAmount = (int) Math.Max(0, Math.Ceiling((args.Quantity / OverflowThreshold).Float())); - var splitSolution = args.Source.SplitSolution(args.Source.Volume); - var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity); - var mapManager = IoCManager.Resolve<IMapManager>(); - - if (!mapManager.TryFindGridAt(transform.MapPosition, out _, out var grid) || - !grid.TryGetTileRef(transform.Coordinates, out var tileRef) || - tileRef.Tile.IsSpace()) - { - return; - } - - var coords = grid.MapToGrid(transform.MapPosition); - var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid()); - - var smoke = args.EntityManager.System<SmokeSystem>(); - smoke.StartSmoke(ent, splitSolution, _duration, spreadAmount); - - var audio = args.EntityManager.System<SharedAudioSystem>(); - audio.PlayPvs(_sound, args.SolutionEntity, AudioHelpers.WithVariation(0.125f)); - } - } -} diff --git a/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs deleted file mode 100644 index 8a558a3ffdc..00000000000 --- a/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Content.Server.Explosion.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using Content.Shared.Explosion; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using System.Text.Json.Serialization; - -namespace Content.Server.Chemistry.ReactionEffects -{ - [DataDefinition] - public sealed partial class ExplosionReactionEffect : ReagentEffect - { - /// <summary> - /// The type of explosion. Determines damage types and tile break chance scaling. - /// </summary> - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))] - [JsonIgnore] - public string ExplosionType = default!; - - /// <summary> - /// The max intensity the explosion can have at a given tile. Places an upper limit of damage and tile break - /// chance. - /// </summary> - [DataField] - [JsonIgnore] - public float MaxIntensity = 5; - - /// <summary> - /// How quickly intensity drops off as you move away from the epicenter - /// </summary> - [DataField] - [JsonIgnore] - public float IntensitySlope = 1; - - /// <summary> - /// The maximum total intensity that this chemical reaction can achieve. Basically here to prevent people - /// from creating a nuke by collecting enough potassium and water. - /// </summary> - /// <remarks> - /// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles. - /// </remarks> - [DataField] - [JsonIgnore] - public float MaxTotalIntensity = 100; - - /// <summary> - /// The intensity of the explosion per unit reaction. - /// </summary> - [DataField] - [JsonIgnore] - public float IntensityPerUnit = 1; - - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-explosion-reaction-effect", ("chance", Probability)); - public override LogImpact LogImpact => LogImpact.High; - - public override void Effect(ReagentEffectArgs args) - { - var intensity = MathF.Min((float) args.Quantity * IntensityPerUnit, MaxTotalIntensity); - - EntitySystem.Get<ExplosionSystem>().QueueExplosion( - args.SolutionEntity, - ExplosionType, - intensity, - IntensitySlope, - MaxIntensity); - } - } -} diff --git a/Content.Server/Chemistry/ReactionEffects/SolutionTemperatureEffects.cs b/Content.Server/Chemistry/ReactionEffects/SolutionTemperatureEffects.cs deleted file mode 100644 index ec58754883d..00000000000 --- a/Content.Server/Chemistry/ReactionEffects/SolutionTemperatureEffects.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReactionEffects -{ - /// <summary> - /// Sets the temperature of the solution involved with the reaction to a new value. - /// </summary> - [DataDefinition] - public sealed partial class SetSolutionTemperatureEffect : ReagentEffect - { - /// <summary> - /// The temperature to set the solution to. - /// </summary> - [DataField("temperature", required: true)] private float _temperature; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-set-solution-temperature-effect", - ("chance", Probability), ("temperature", _temperature)); - - public override void Effect(ReagentEffectArgs args) - { - var solution = args.Source; - if (solution == null) - return; - - solution.Temperature = _temperature; - } - } - - /// <summary> - /// Adjusts the temperature of the solution involved in the reaction. - /// </summary> - [DataDefinition] - public sealed partial class AdjustSolutionTemperatureEffect : ReagentEffect - { - /// <summary> - /// The change in temperature. - /// </summary> - [DataField("delta", required: true)] private float _delta; - - /// <summary> - /// The minimum temperature this effect can reach. - /// </summary> - [DataField("minTemp")] private float _minTemp = 0.0f; - - /// <summary> - /// The maximum temperature this effect can reach. - /// </summary> - [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; - - /// <summary> - /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. - /// </summary> - [DataField("scaled")] private bool _scaled; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", - ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); - - public override void Effect(ReagentEffectArgs args) - { - var solution = args.Source; - if (solution == null || solution.Volume == 0) - return; - - var deltaT = _scaled ? _delta * (float) args.Quantity : _delta; - solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); - } - } - - /// <summary> - /// Adjusts the thermal energy of the solution involved in the reaction. - /// </summary> - public sealed partial class AdjustSolutionThermalEnergyEffect : ReagentEffect - { - /// <summary> - /// The change in energy. - /// </summary> - [DataField("delta", required: true)] private float _delta; - - /// <summary> - /// The minimum temperature this effect can reach. - /// </summary> - [DataField("minTemp")] private float _minTemp = 0.0f; - - /// <summary> - /// The maximum temperature this effect can reach. - /// </summary> - [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; - - /// <summary> - /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. - /// </summary> - [DataField("scaled")] private bool _scaled; - - public override void Effect(ReagentEffectArgs args) - { - var solution = args.Source; - if (solution == null || solution.Volume == 0) - return; - - if (_delta > 0 && solution.Temperature >= _maxTemp) - return; - if (_delta < 0 && solution.Temperature <= _minTemp) - return; - - var heatCap = solution.GetHeatCapacity(null); - var deltaT = _scaled - ? _delta / heatCap * (float) args.Quantity - : _delta / heatCap; - - solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", - ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); - } - -} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/BodyTemperature.cs b/Content.Server/Chemistry/ReagentEffectConditions/BodyTemperature.cs deleted file mode 100644 index 9c47bb58ab8..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/BodyTemperature.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.Temperature.Components; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - /// <summary> - /// Requires the solution entity to be above or below a certain temperature. - /// Used for things like cryoxadone and pyroxadone. - /// </summary> - public sealed partial class Temperature : ReagentEffectCondition - { - [DataField] - public float Min = 0; - - [DataField] - public float Max = float.PositiveInfinity; - public override bool Condition(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out TemperatureComponent? temp)) - { - if (temp.CurrentTemperature > Min && temp.CurrentTemperature < Max) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-body-temperature", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/MobStateCondition.cs b/Content.Server/Chemistry/ReagentEffectConditions/MobStateCondition.cs deleted file mode 100644 index 377eb7d9065..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/MobStateCondition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - public sealed partial class MobStateCondition : ReagentEffectCondition - { - [DataField] - public MobState Mobstate = MobState.Alive; - - public override bool Condition(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out MobStateComponent? mobState)) - { - if (mobState.CurrentState == Mobstate) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate)); - } - } -} - diff --git a/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs b/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs deleted file mode 100644 index 4ae13b6a6e4..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Content.Server.Body.Components; -using Content.Shared.Body.Prototypes; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - /// <summary> - /// Requires that the metabolizing organ is or is not tagged with a certain MetabolizerType - /// </summary> - public sealed partial class OrganType : ReagentEffectCondition - { - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<MetabolizerTypePrototype>))] - public string Type = default!; - - /// <summary> - /// Does this condition pass when the organ has the type, or when it doesn't have the type? - /// </summary> - [DataField] - public bool ShouldHave = true; - - public override bool Condition(ReagentEffectArgs args) - { - if (args.OrganEntity == null) - return false; - - if (args.EntityManager.TryGetComponent<MetabolizerComponent>(args.OrganEntity.Value, out var metabolizer) - && metabolizer.MetabolizerTypes != null - && metabolizer.MetabolizerTypes.Contains(Type)) - return ShouldHave; - return !ShouldHave; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-organ-type", - ("name", prototype.Index<MetabolizerTypePrototype>(Type).LocalizedName), - ("shouldhave", ShouldHave)); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/ReagentThreshold.cs b/Content.Server/Chemistry/ReagentEffectConditions/ReagentThreshold.cs deleted file mode 100644 index 061af2b3aef..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/ReagentThreshold.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - /// <summary> - /// Used for implementing reagent effects that require a certain amount of reagent before it should be applied. - /// For instance, overdoses. - /// - /// This can also trigger on -other- reagents, not just the one metabolizing. By default, it uses the - /// one being metabolized. - /// </summary> - public sealed partial class ReagentThreshold : ReagentEffectCondition - { - [DataField] - public FixedPoint2 Min = FixedPoint2.Zero; - - [DataField] - public FixedPoint2 Max = FixedPoint2.MaxValue; - - // TODO use ReagentId - [DataField] - public string? Reagent; - - public override bool Condition(ReagentEffectArgs args) - { - var reagent = Reagent ?? args.Reagent?.ID; - if (reagent == null) - return true; // No condition to apply. - - var quant = FixedPoint2.Zero; - if (args.Source != null) - quant = args.Source.GetTotalPrototypeQuantity(reagent) * args.QuantityMultiplier; - - return quant >= Min && quant <= Max; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - ReagentPrototype? reagentProto = null; - if (Reagent is not null) - prototype.TryIndex(Reagent, out reagentProto); - - return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold", - ("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")), - ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), - ("min", Min.Float())); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/SolutionTemperature.cs b/Content.Server/Chemistry/ReagentEffectConditions/SolutionTemperature.cs deleted file mode 100644 index 4387f2ba93b..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/SolutionTemperature.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - /// <summary> - /// Requires the solution to be above or below a certain temperature. - /// Used for things like explosives. - /// </summary> - public sealed partial class SolutionTemperature : ReagentEffectCondition - { - [DataField] - public float Min = 0.0f; - - [DataField] - public float Max = float.PositiveInfinity; - public override bool Condition(ReagentEffectArgs args) - { - if (args.Source == null) - return false; - if (args.Source.Temperature < Min) - return false; - if (args.Source.Temperature > Max) - return false; - - return true; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-solution-temperature", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/TotalDamage.cs b/Content.Server/Chemistry/ReagentEffectConditions/TotalDamage.cs deleted file mode 100644 index 49630ef9ab0..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/TotalDamage.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Damage; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - public sealed partial class TotalDamage : ReagentEffectCondition - { - [DataField] - public FixedPoint2 Max = FixedPoint2.MaxValue; - - [DataField] - public FixedPoint2 Min = FixedPoint2.Zero; - - public override bool Condition(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out DamageableComponent? damage)) - { - var total = damage.TotalDamage; - if (total > Min && total < Max) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-total-damage", - ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), - ("min", Min.Float())); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs b/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs deleted file mode 100644 index 1dd12e632a7..00000000000 --- a/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffectConditions -{ - public sealed partial class Hunger : ReagentEffectCondition - { - [DataField] - public float Max = float.PositiveInfinity; - - [DataField] - public float Min = 0; - - public override bool Condition(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger)) - { - var total = hunger.CurrentHunger; - if (total > Min && total < Max) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-total-hunger", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/AddToSolutionReaction.cs b/Content.Server/Chemistry/ReagentEffects/AddToSolutionReaction.cs deleted file mode 100644 index 6a43739b0ee..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/AddToSolutionReaction.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects -{ - [UsedImplicitly] - public sealed partial class AddToSolutionReaction : ReagentEffect - { - [DataField("solution")] - private string _solution = "reagents"; - - public override void Effect(ReagentEffectArgs args) - { - if (args.Reagent == null) - return; - - // TODO see if this is correct - var solutionContainerSystem = args.EntityManager.System<SolutionContainerSystem>(); - if (!solutionContainerSystem.TryGetSolution(args.SolutionEntity, _solution, out var solutionContainer)) - return; - - if (solutionContainerSystem.TryAddReagent(solutionContainer.Value, args.Reagent.ID, args.Quantity, out var accepted)) - args.Source?.RemoveReagent(args.Reagent.ID, accepted); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs b/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs deleted file mode 100644 index 402b30a069b..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/ChemCleanBloodstream.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReactionEffects -{ - /// <summary> - /// Basically smoke and foam reactions. - /// </summary> - [UsedImplicitly] - public sealed partial class ChemCleanBloodstream : ReagentEffect - { - [DataField] - public float CleanseRate = 3.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-chem-clean-bloodstream", ("chance", Probability)); - - public override void Effect(ReagentEffectArgs args) - { - if (args.Source == null || args.Reagent == null) - return; - - var cleanseRate = CleanseRate; - - cleanseRate *= args.Scale; - - var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>(); - bloodstreamSys.FlushChemicals(args.SolutionEntity, args.Reagent.ID, cleanseRate); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/ChemHealEyeDamage.cs b/Content.Server/Chemistry/ReagentEffects/ChemHealEyeDamage.cs deleted file mode 100644 index 206b8ed04dc..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/ChemHealEyeDamage.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Eye.Blinding; -using Content.Shared.Eye.Blinding.Systems; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects -{ - /// <summary> - /// Heal or apply eye damage - /// </summary> - [UsedImplicitly] - public sealed partial class ChemHealEyeDamage : ReagentEffect - { - /// <summary> - /// How much eye damage to add. - /// </summary> - [DataField] - public int Amount = -1; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-cure-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); - - public override void Effect(ReagentEffectArgs args) - { - if (args.Scale != 1f) // huh? - return; - - args.EntityManager.EntitySysManager.GetEntitySystem<BlindableSystem>().AdjustEyeDamage(args.SolutionEntity, Amount); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/Electrocute.cs b/Content.Server/Chemistry/ReagentEffects/Electrocute.cs deleted file mode 100644 index 272a0fce42a..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/Electrocute.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Server.Electrocution; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects; - -public sealed partial class Electrocute : ReagentEffect -{ - [DataField] public int ElectrocuteTime = 2; - - [DataField] public int ElectrocuteDamageScale = 5; - - /// <remarks> - /// true - refresh electrocute time, false - accumulate electrocute time - /// </remarks> - [DataField] public bool Refresh = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime)); - - public override bool ShouldLog => true; - - public override void Effect(ReagentEffectArgs args) - { - args.EntityManager.System<ElectrocutionSystem>().TryDoElectrocution(args.SolutionEntity, null, - Math.Max((args.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); - - if (args.Reagent != null) - args.Source?.RemoveReagent(args.Reagent.ID, args.Quantity); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/ExtinguishReaction.cs b/Content.Server/Chemistry/ReagentEffects/ExtinguishReaction.cs deleted file mode 100644 index e9029089988..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/ExtinguishReaction.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects -{ - [UsedImplicitly] - public sealed partial class ExtinguishReaction : ReagentEffect - { - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-extinguish-reaction", ("chance", Probability)); - - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out FlammableComponent? flammable)) return; - - var flammableSystem = EntitySystem.Get<FlammableSystem>(); - flammableSystem.Extinguish(args.SolutionEntity, flammable); - flammableSystem.AdjustFireStacks(args.SolutionEntity, -1.5f * (float) args.Quantity, flammable); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/FlammableReaction.cs b/Content.Server/Chemistry/ReagentEffects/FlammableReaction.cs deleted file mode 100644 index 5b36967f6a8..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/FlammableReaction.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects -{ - [UsedImplicitly] - public sealed partial class FlammableReaction : ReagentEffect - { - [DataField] - public float Multiplier = 0.05f; - - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-flammable-reaction", ("chance", Probability)); - - public override LogImpact LogImpact => LogImpact.Medium; - - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out FlammableComponent? flammable)) - return; - - args.EntityManager.System<FlammableSystem>().AdjustFireStacks(args.SolutionEntity, args.Quantity.Float() * Multiplier, flammable); - - if (args.Reagent != null) - args.Source?.RemoveReagent(args.Reagent.ID, args.Quantity); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/Ignite.cs b/Content.Server/Chemistry/ReagentEffects/Ignite.cs deleted file mode 100644 index 1fc90f63e31..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/Ignite.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects; - -/// <summary> -/// Ignites a mob. -/// </summary> -public sealed partial class Ignite : ReagentEffect -{ - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-ignite", ("chance", Probability)); - - public override LogImpact LogImpact => LogImpact.Medium; - - public override void Effect(ReagentEffectArgs args) - { - var flamSys = EntitySystem.Get<FlammableSystem>(); - flamSys.Ignite(args.SolutionEntity, args.OrganEntity ?? args.SolutionEntity); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs b/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs deleted file mode 100644 index 7b966ea4788..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects; - -public sealed partial class ModifyBleedAmount : ReagentEffect -{ - [DataField] - public bool Scaled = false; - - [DataField] - public float Amount = -1.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-modify-bleed-amount", ("chance", Probability), - ("deltasign", MathF.Sign(Amount))); - - public override void Effect(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent<BloodstreamComponent>(args.SolutionEntity, out var blood)) - { - var sys = EntitySystem.Get<BloodstreamSystem>(); - var amt = Scaled ? Amount * args.Quantity.Float() : Amount; - amt *= args.Scale; - - sys.TryModifyBleedAmount(args.SolutionEntity, amt, blood); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs b/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs deleted file mode 100644 index 748aa71083e..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects; - -public sealed partial class ModifyBloodLevel : ReagentEffect -{ - [DataField] - public bool Scaled = false; - - [DataField] - public FixedPoint2 Amount = 1.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-modify-blood-level", ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float()))); - - public override void Effect(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent<BloodstreamComponent>(args.SolutionEntity, out var blood)) - { - var sys = EntitySystem.Get<BloodstreamSystem>(); - var amt = Scaled ? Amount * args.Quantity : Amount; - amt *= args.Scale; - - sys.TryModifyBloodLevel(args.SolutionEntity, amt, blood); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs b/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs deleted file mode 100644 index e7466fbc85d..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/ModifyLungGas.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Content.Server.Body.Components; -using Content.Shared.Atmos; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects; - -public sealed partial class ModifyLungGas : ReagentEffect -{ - [DataField("ratios", required: true)] - private Dictionary<Gas, float> _ratios = default!; - - // JUSTIFICATION: This is internal magic that players never directly interact with. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; - - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent<LungComponent>(args.OrganEntity, out var lung)) - return; - - foreach (var (gas, ratio) in _ratios) - { - var quantity = ratio * args.Quantity.Float() / Atmospherics.BreathMolesToReagentMultiplier; - if (quantity < 0) - quantity = Math.Max(quantity, -lung.Air[(int)gas]); - lung.Air.AdjustMoles(gas, quantity); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/MovespeedModifier.cs b/Content.Server/Chemistry/ReagentEffects/MovespeedModifier.cs deleted file mode 100644 index 0301742c5af..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/MovespeedModifier.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Movement.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Server.Chemistry.ReagentEffects -{ - /// <summary> - /// Default metabolism for stimulants and tranqs. Attempts to find a MovementSpeedModifier on the target, - /// adding one if not there and to change the movespeed - /// </summary> - public sealed partial class MovespeedModifier : ReagentEffect - { - /// <summary> - /// How much the entities' walk speed is multiplied by. - /// </summary> - [DataField] - public float WalkSpeedModifier { get; set; } = 1; - - /// <summary> - /// How much the entities' run speed is multiplied by. - /// </summary> - [DataField] - public float SprintSpeedModifier { get; set; } = 1; - - /// <summary> - /// How long the modifier applies (in seconds) when metabolized. - /// </summary> - [DataField] - public float StatusLifetime = 2f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-movespeed-modifier", - ("chance", Probability), - ("walkspeed", WalkSpeedModifier), - ("time", StatusLifetime)); - } - - /// <summary> - /// Remove reagent at set rate, changes the movespeed modifiers and adds a MovespeedModifierMetabolismComponent if not already there. - /// </summary> - public override void Effect(ReagentEffectArgs args) - { - var status = args.EntityManager.EnsureComponent<MovespeedModifierMetabolismComponent>(args.SolutionEntity); - - // Only refresh movement if we need to. - var modified = !status.WalkSpeedModifier.Equals(WalkSpeedModifier) || - !status.SprintSpeedModifier.Equals(SprintSpeedModifier); - - status.WalkSpeedModifier = WalkSpeedModifier; - status.SprintSpeedModifier = SprintSpeedModifier; - - // only going to scale application time - var statusLifetime = StatusLifetime; - statusLifetime *= args.Scale; - - IncreaseTimer(status, statusLifetime); - - if (modified) - EntitySystem.Get<MovementSpeedModifierSystem>().RefreshMovementSpeedModifiers(args.SolutionEntity); - - } - public void IncreaseTimer(MovespeedModifierMetabolismComponent status, float time) - { - var gameTiming = IoCManager.Resolve<IGameTiming>(); - - var offsetTime = Math.Max(status.ModifierTimer.TotalSeconds, gameTiming.CurTime.TotalSeconds); - - status.ModifierTimer = TimeSpan.FromSeconds(offsetTime + time); - status.Dirty(); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs b/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs deleted file mode 100644 index 34a2454139e..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/Oxygenate.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects; - -public sealed partial class Oxygenate : ReagentEffect -{ - [DataField] - public float Factor = 1f; - - // JUSTIFICATION: This is internal magic that players never directly interact with. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; - - public override void Effect(ReagentEffectArgs args) - { - if (args.EntityManager.TryGetComponent<RespiratorComponent>(args.SolutionEntity, out var resp)) - { - var respSys = EntitySystem.Get<RespiratorSystem>(); - respSys.UpdateSaturation(args.SolutionEntity, args.Quantity.Float() * Factor, resp); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustAttribute.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustAttribute.cs deleted file mode 100644 index f43b4828f93..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustAttribute.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Content.Server.Botany.Components; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Diagnostics.CodeAnalysis; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [ImplicitDataDefinitionForInheritors] - public abstract partial class PlantAdjustAttribute : ReagentEffect - { - [DataField] - public float Amount { get; protected set; } = 1; - - [DataField] - public float Prob { get; protected set; } = 1; // = (80); - - /// <summary> - /// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default. - /// </summary> - /// <param name="plantHolder">The entity holding the plant</param> - /// <param name="plantHolderComponent">The plant holder component</param> - /// <param name="entityManager">The entity manager</param> - /// <param name="mustHaveAlivePlant">Whether to check if it has an alive plant or not</param> - /// <returns></returns> - public bool CanMetabolize(EntityUid plantHolder, [NotNullWhen(true)] out PlantHolderComponent? plantHolderComponent, - IEntityManager entityManager, - bool mustHaveAlivePlant = true) - { - plantHolderComponent = null; - - if (!entityManager.TryGetComponent(plantHolder, out plantHolderComponent) - || mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead)) - return false; - - if (Prob >= 1f) - return true; - - // Dependencies are never injected for reagents if you intend to do that for this. - return !(Prob <= 0f) && IoCManager.Resolve<IRobustRandom>().Prob(Prob); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustHealth.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustHealth.cs deleted file mode 100644 index f03464469eb..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustHealth.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.Botany.Systems; -using Content.Shared.Chemistry.Reagent; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - public sealed partial class PlantAdjustHealth : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - var plantHolder = args.EntityManager.System<PlantHolderSystem>(); - - plantHolderComp.Health += Amount; - plantHolder.CheckHealth(args.SolutionEntity, plantHolderComp); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustMutationLevel.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustMutationLevel.cs deleted file mode 100644 index 8c8d04765a2..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustMutationLevel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Content.Shared.Chemistry.Reagent; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - public sealed partial class PlantAdjustMutationLevel : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - plantHolderComp.MutationLevel += Amount * plantHolderComp.MutationMod; - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustMutationMod.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustMutationMod.cs deleted file mode 100644 index af4a00a044e..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustMutationMod.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAdjustMutationMod : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - plantHolderComp.MutationMod += Amount; - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustNutrition.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustNutrition.cs deleted file mode 100644 index 900121412e8..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustNutrition.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Botany.Systems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAdjustNutrition : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false)) - return; - - var plantHolder = args.EntityManager.System<PlantHolderSystem>(); - - plantHolder.AdjustNutrient(args.SolutionEntity, Amount, plantHolderComp); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustPests.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustPests.cs deleted file mode 100644 index 27729b4b2ca..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustPests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAdjustPests : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - plantHolderComp.PestLevel += Amount; - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustToxins.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustToxins.cs deleted file mode 100644 index 35130238ad5..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustToxins.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAdjustToxins : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - plantHolderComp.Toxins += Amount; - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustWater.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustWater.cs deleted file mode 100644 index 71b4670dc65..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustWater.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Botany.Systems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAdjustWater : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false)) - return; - - var plantHolder = args.EntityManager.System<PlantHolderSystem>(); - - plantHolder.AdjustWater(args.SolutionEntity, Amount, plantHolderComp); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustWeeds.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustWeeds.cs deleted file mode 100644 index 6184b95adb8..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAdjustWeeds.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAdjustWeeds : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - plantHolderComp.WeedLevel += Amount; - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAffectGrowth.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAffectGrowth.cs deleted file mode 100644 index 54ec628fdc9..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantAffectGrowth.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Botany.Systems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - public sealed partial class PlantAffectGrowth : PlantAdjustAttribute - { - public override void Effect(ReagentEffectArgs args) - { - if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager)) - return; - - var plantHolder = args.EntityManager.System<PlantHolderSystem>(); - - plantHolder.AffectGrowth(args.SolutionEntity, (int) Amount, plantHolderComp); - } - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantCryoxadone.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantCryoxadone.cs deleted file mode 100644 index 83a5f56e592..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantCryoxadone.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Server.Botany.Components; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - [DataDefinition] - public sealed partial class PlantCryoxadone : ReagentEffect - { - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out PlantHolderComponent? plantHolderComp) - || plantHolderComp.Seed == null || plantHolderComp.Dead) - return; - - var deviation = 0; - var seed = plantHolderComp.Seed; - var random = IoCManager.Resolve<IRobustRandom>(); - if (plantHolderComp.Age > seed.Maturation) - deviation = (int) Math.Max(seed.Maturation - 1, plantHolderComp.Age - random.Next(7, 10)); - else - deviation = (int) (seed.Maturation / seed.GrowthStages); - plantHolderComp.Age -= deviation; - plantHolderComp.SkipAging++; - plantHolderComp.ForceUpdate = true; - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantDiethylamine.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantDiethylamine.cs deleted file mode 100644 index 23cb436d2f1..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantDiethylamine.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Server.Botany.Components; -using Content.Server.Botany.Systems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - [DataDefinition] - public sealed partial class PlantDiethylamine : ReagentEffect - { - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out PlantHolderComponent? plantHolderComp) - || plantHolderComp.Seed == null || plantHolderComp.Dead || - plantHolderComp.Seed.Immutable) - return; - - - var plantHolder = args.EntityManager.System<PlantHolderSystem>(); - - var random = IoCManager.Resolve<IRobustRandom>(); - - if (random.Prob(0.1f)) - { - plantHolder.EnsureUniqueSeed(args.SolutionEntity, plantHolderComp); - plantHolderComp.Seed.Lifespan++; - } - - if (random.Prob(0.1f)) - { - plantHolder.EnsureUniqueSeed(args.SolutionEntity, plantHolderComp); - plantHolderComp.Seed.Endurance++; - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantPhalanximine.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantPhalanximine.cs deleted file mode 100644 index c0640d7fc04..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/PlantPhalanximine.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Server.Botany.Components; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - [DataDefinition] - public sealed partial class PlantPhalanximine : ReagentEffect - { - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out PlantHolderComponent? plantHolderComp) - || plantHolderComp.Seed == null || plantHolderComp.Dead || - plantHolderComp.Seed.Immutable) - return; - - plantHolderComp.Seed.Viable = true; - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/RobustHarvest.cs b/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/RobustHarvest.cs deleted file mode 100644 index 990a5a5003f..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/PlantMetabolism/RobustHarvest.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Server.Botany.Components; -using Content.Server.Botany.Systems; -using Content.Shared.Chemistry.Reagent; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism -{ - [UsedImplicitly] - [DataDefinition] - public sealed partial class RobustHarvest : ReagentEffect - { - [DataField] - public int PotencyLimit = 50; - - [DataField] - public int PotencyIncrease = 3; - - [DataField] - public int PotencySeedlessThreshold = 30; - - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out PlantHolderComponent? plantHolderComp) - || plantHolderComp.Seed == null || plantHolderComp.Dead || - plantHolderComp.Seed.Immutable) - return; - - - var plantHolder = args.EntityManager.System<PlantHolderSystem>(); - var random = IoCManager.Resolve<IRobustRandom>(); - - if (plantHolderComp.Seed.Potency < PotencyLimit) - { - plantHolder.EnsureUniqueSeed(args.SolutionEntity, plantHolderComp); - plantHolderComp.Seed.Potency = Math.Min(plantHolderComp.Seed.Potency + PotencyIncrease, PotencyLimit); - - if (plantHolderComp.Seed.Potency > PotencySeedlessThreshold) - { - plantHolderComp.Seed.Seedless = true; - } - } - else if (plantHolderComp.Seed.Yield > 1 && random.Prob(0.1f)) - { - // Too much of a good thing reduces yield - plantHolder.EnsureUniqueSeed(args.SolutionEntity, plantHolderComp); - plantHolderComp.Seed.Yield--; - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs b/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs deleted file mode 100644 index 1208e74367b..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects -{ - /// <summary> - /// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, - /// and to update it's thirst values. - /// </summary> - public sealed partial class SatiateThirst : ReagentEffect - { - private const float DefaultHydrationFactor = 3.0f; - - /// How much thirst is satiated each metabolism tick. Not currently tied to - /// rate or anything. - [DataField("factor")] - public float HydrationFactor { get; set; } = DefaultHydrationFactor; - - /// Satiate thirst if a ThirstComponent can be found - public override void Effect(ReagentEffectArgs args) - { - var uid = args.SolutionEntity; - if (args.EntityManager.TryGetComponent(uid, out ThirstComponent? thirst)) - EntitySystem.Get<ThirstSystem>().ModifyThirst(uid, thirst, HydrationFactor); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", HydrationFactor / DefaultHydrationFactor)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/StatusEffects/GenericStatusEffect.cs b/Content.Server/Chemistry/ReagentEffects/StatusEffects/GenericStatusEffect.cs deleted file mode 100644 index 66454b25fd6..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/StatusEffects/GenericStatusEffect.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.StatusEffect; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects.StatusEffects -{ - /// <summary> - /// Adds a generic status effect to the entity, - /// not worrying about things like how to affect the time it lasts for - /// or component fields or anything. Just adds a component to an entity - /// for a given time. Easy. - /// </summary> - /// <remarks> - /// Can be used for things like adding accents or something. I don't know. Go wild. - /// </remarks> - [UsedImplicitly] - public sealed partial class GenericStatusEffect : ReagentEffect - { - [DataField(required: true)] - public string Key = default!; - - [DataField] - public string Component = String.Empty; - - [DataField] - public float Time = 2.0f; - - /// <remarks> - /// true - refresh status effect time, false - accumulate status effect time - /// </remarks> - [DataField] - public bool Refresh = true; - - /// <summary> - /// Should this effect add the status effect, remove time from it, or set its cooldown? - /// </summary> - [DataField] - public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; - - public override void Effect(ReagentEffectArgs args) - { - var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem<StatusEffectsSystem>(); - - var time = Time; - time *= args.Scale; - - if (Type == StatusEffectMetabolismType.Add && Component != String.Empty) - { - statusSys.TryAddStatusEffect(args.SolutionEntity, Key, TimeSpan.FromSeconds(time), Refresh, Component); - } - else if (Type == StatusEffectMetabolismType.Remove) - { - statusSys.TryRemoveTime(args.SolutionEntity, Key, TimeSpan.FromSeconds(time)); - } - else if (Type == StatusEffectMetabolismType.Set) - { - statusSys.TrySetTime(args.SolutionEntity, Key, TimeSpan.FromSeconds(time)); - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString( - "reagent-effect-guidebook-status-effect", - ("chance", Probability), - ("type", Type), - ("time", Time), - ("key", $"reagent-effect-status-effect-{Key}")); - } - - public enum StatusEffectMetabolismType - { - Add, - Remove, - Set - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/StatusEffects/Jitter.cs b/Content.Server/Chemistry/ReagentEffects/StatusEffects/Jitter.cs deleted file mode 100644 index 7ee70957b7a..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/StatusEffects/Jitter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Jittering; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects.StatusEffects -{ - /// <summary> - /// Adds the jitter status effect to a mob. - /// This doesn't use generic status effects because it needs to - /// take in some parameters that JitterSystem needs. - /// </summary> - public sealed partial class Jitter : ReagentEffect - { - [DataField] - public float Amplitude = 10.0f; - - [DataField] - public float Frequency = 4.0f; - - [DataField] - public float Time = 2.0f; - - /// <remarks> - /// true - refresh jitter time, false - accumulate jitter time - /// </remarks> - [DataField] - public bool Refresh = true; - - public override void Effect(ReagentEffectArgs args) - { - var time = Time; - time *= args.Scale; - - args.EntityManager.EntitySysManager.GetEntitySystem<SharedJitteringSystem>() - .DoJitter(args.SolutionEntity, TimeSpan.FromSeconds(time), Refresh, Amplitude, Frequency); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-jittering", ("chance", Probability)); - } -} diff --git a/Content.Server/Chemistry/ReagentEffects/WashCreamPieReaction.cs b/Content.Server/Chemistry/ReagentEffects/WashCreamPieReaction.cs deleted file mode 100644 index 9bf6792eca2..00000000000 --- a/Content.Server/Chemistry/ReagentEffects/WashCreamPieReaction.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Server.Nutrition.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chemistry.ReagentEffects -{ - [UsedImplicitly] - public sealed partial class WashCreamPieReaction : ReagentEffect - { - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-wash-cream-pie-reaction", ("chance", Probability)); - - public override void Effect(ReagentEffectArgs args) - { - if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out CreamPiedComponent? creamPied)) return; - - EntitySystem.Get<CreamPieSystem>().SetCreamPied(args.SolutionEntity, creamPied, false); - } - } -} diff --git a/Content.Server/Chemistry/TileReactions/CreateEntityTileReaction.cs b/Content.Server/Chemistry/TileReactions/CreateEntityTileReaction.cs index 9cb1ba201d5..d3b57e76303 100644 --- a/Content.Server/Chemistry/TileReactions/CreateEntityTileReaction.cs +++ b/Content.Server/Chemistry/TileReactions/CreateEntityTileReaction.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chemistry.Reaction; +using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; using Content.Shared.Maps; @@ -47,7 +47,8 @@ public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 int acc = 0; foreach (var ent in tile.GetEntitiesInTile()) { - if (Whitelist.IsValid(ent)) + var whitelistSystem = entMan.System<EntityWhitelistSystem>(); + if (whitelistSystem.IsWhitelistPass(Whitelist, ent)) acc += 1; if (acc >= MaxOnTile) diff --git a/Content.Server/Cloning/CloningConsoleSystem.cs b/Content.Server/Cloning/CloningConsoleSystem.cs index c0685024aae..7a54cce31f7 100644 --- a/Content.Server/Cloning/CloningConsoleSystem.cs +++ b/Content.Server/Cloning/CloningConsoleSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Mind; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Power; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Server.Player; diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 72104bc381f..c107bbab7d4 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -46,6 +46,8 @@ using Content.Server.Power.Components; using Content.Shared.Drunk; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Power; + namespace Content.Server.Cloning; @@ -214,7 +216,6 @@ public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponen return true; var mob = FetchAndSpawnMob(uid, clonePod, pref, speciesPrototype, humanoid, bodyToClone, geneticDamage); - var ev = new CloningEvent(bodyToClone, mob); RaiseLocalEvent(bodyToClone, ref ev); diff --git a/Content.Server/Clothing/MagbootsSystem.cs b/Content.Server/Clothing/MagbootsSystem.cs deleted file mode 100644 index 3838ad168d1..00000000000 --- a/Content.Server/Clothing/MagbootsSystem.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Shared.Alert; -using Content.Shared.Clothing; - -namespace Content.Server.Clothing; - -public sealed class MagbootsSystem : SharedMagbootsSystem -{ - [Dependency] private readonly AlertsSystem _alerts = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<MagbootsComponent, ClothingGotEquippedEvent>(OnGotEquipped); - SubscribeLocalEvent<MagbootsComponent, ClothingGotUnequippedEvent>(OnGotUnequipped); - } - - protected override void UpdateMagbootEffects(EntityUid parent, EntityUid uid, bool state, MagbootsComponent? component) - { - if (!Resolve(uid, ref component)) - return; - state = state && component.On; - - if (TryComp(parent, out MovedByPressureComponent? movedByPressure)) - { - movedByPressure.Enabled = !state; - } - - if (state) - { - _alerts.ShowAlert(parent, component.MagbootsAlert); - } - else - { - _alerts.ClearAlert(parent, component.MagbootsAlert); - } - } - - private void OnGotUnequipped(EntityUid uid, MagbootsComponent component, ref ClothingGotUnequippedEvent args) - { - UpdateMagbootEffects(args.Wearer, uid, false, component); - } - - private void OnGotEquipped(EntityUid uid, MagbootsComponent component, ref ClothingGotEquippedEvent args) - { - UpdateMagbootEffects(args.Wearer, uid, true, component); - } -} diff --git a/Content.Server/Clothing/Systems/CursedMaskSystem.cs b/Content.Server/Clothing/Systems/CursedMaskSystem.cs new file mode 100644 index 00000000000..2045ff5ccd6 --- /dev/null +++ b/Content.Server/Clothing/Systems/CursedMaskSystem.cs @@ -0,0 +1,92 @@ +using Content.Server.Administration.Logs; +using Content.Server.GameTicking; +using Content.Server.Mind; +using Content.Server.NPC; +using Content.Server.NPC.HTN; +using Content.Server.NPC.Systems; +using Content.Server.Popups; +using Content.Shared.Clothing; +using Content.Shared.Clothing.Components; +using Content.Shared.Database; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; +using Content.Shared.Players; +using Content.Shared.Popups; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Clothing.Systems; + +/// <inheritdoc/> +public sealed class CursedMaskSystem : SharedCursedMaskSystem +{ + [Dependency] private readonly IAdminLogManager _adminLog = default!; + [Dependency] private readonly GameTicker _ticker = default!; + [Dependency] private readonly HTNSystem _htn = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly NPCSystem _npc = default!; + [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + // We can't store this info on the component easily + private static readonly ProtoId<HTNCompoundPrototype> TakeoverRootTask = "SimpleHostileCompound"; + + protected override void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer) + { + if (ent.Comp.CurrentState != CursedMaskExpression.Anger) + return; + + if (TryComp<ActorComponent>(wearer, out var actor) && actor.PlayerSession.GetMind() is { } mind) + { + var session = actor.PlayerSession; + if (!_ticker.OnGhostAttempt(mind, false)) + return; + + ent.Comp.StolenMind = mind; + + _popup.PopupEntity(Loc.GetString("cursed-mask-takeover-popup"), wearer, session, PopupType.LargeCaution); + _adminLog.Add(LogType.Action, + LogImpact.Extreme, + $"{ToPrettyString(wearer):player} had their body taken over and turned into an enemy through the cursed mask {ToPrettyString(ent):entity}"); + } + + var npcFaction = EnsureComp<NpcFactionMemberComponent>(wearer); + ent.Comp.OldFactions = npcFaction.Factions; + _npcFaction.ClearFactions((wearer, npcFaction), false); + _npcFaction.AddFaction((wearer, npcFaction), ent.Comp.CursedMaskFaction); + + ent.Comp.HasNpc = !EnsureComp<HTNComponent>(wearer, out var htn); + htn.RootTask = new HTNCompoundTask { Task = TakeoverRootTask }; + htn.Blackboard.SetValue(NPCBlackboard.Owner, wearer); + _npc.WakeNPC(wearer, htn); + _htn.Replan(htn); + } + + protected override void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args) + { + // If we are taking off the cursed mask + if (ent.Comp.CurrentState == CursedMaskExpression.Anger) + { + if (ent.Comp.HasNpc) + RemComp<HTNComponent>(args.Wearer); + + var npcFaction = EnsureComp<NpcFactionMemberComponent>(args.Wearer); + _npcFaction.RemoveFaction((args.Wearer, npcFaction), ent.Comp.CursedMaskFaction, false); + _npcFaction.AddFactions((args.Wearer, npcFaction), ent.Comp.OldFactions); + + ent.Comp.HasNpc = false; + ent.Comp.OldFactions.Clear(); + + if (Exists(ent.Comp.StolenMind)) + { + _mind.TransferTo(ent.Comp.StolenMind.Value, args.Wearer); + _adminLog.Add(LogType.Action, + LogImpact.Extreme, + $"{ToPrettyString(args.Wearer):player} was restored to their body after the removal of {ToPrettyString(ent):entity}."); + ent.Comp.StolenMind = null; + } + } + + RandomizeCursedMask(ent, args.Wearer); + } +} diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs index bd7b7a66201..ab280e1cd73 100644 --- a/Content.Server/Cluwne/CluwneSystem.cs +++ b/Content.Server/Cluwne/CluwneSystem.cs @@ -15,8 +15,9 @@ using Content.Shared.Chat; using Content.Shared.Cluwne; using Content.Shared.Interaction.Components; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Content.Shared.NameModifier.EntitySystems; +using Content.Shared.Clumsy; namespace Content.Server.Cluwne; @@ -31,6 +32,7 @@ public sealed class CluwneSystem : EntitySystem [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly AutoEmoteSystem _autoEmote = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; public override void Initialize() { @@ -40,6 +42,7 @@ public override void Initialize() SubscribeLocalEvent<CluwneComponent, MobStateChangedEvent>(OnMobState); SubscribeLocalEvent<CluwneComponent, EmoteEvent>(OnEmote, before: new[] { typeof(VocalSystem), typeof(BodyEmotesSystem) }); + SubscribeLocalEvent<CluwneComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } /// <summary> @@ -48,19 +51,19 @@ public override void Initialize() private void OnMobState(EntityUid uid, CluwneComponent component, MobStateChangedEvent args) { if (args.NewMobState == MobState.Dead) - { + { RemComp<CluwneComponent>(uid); RemComp<ClumsyComponent>(uid); RemComp<AutoEmoteComponent>(uid); var damageSpec = new DamageSpecifier(_prototypeManager.Index<DamageGroupPrototype>("Genetic"), 300); _damageableSystem.TryChangeDamage(uid, damageSpec); - } + } } public EmoteSoundsPrototype? EmoteSounds; /// <summary> - /// OnStartup gives the cluwne outfit, ensures clumsy, gives name prefix and makes sure emote sounds are laugh. + /// OnStartup gives the cluwne outfit, ensures clumsy, and makes sure emote sounds are laugh. /// </summary> private void OnComponentStartup(EntityUid uid, CluwneComponent component, ComponentStartup args) { @@ -68,9 +71,6 @@ private void OnComponentStartup(EntityUid uid, CluwneComponent component, Compon return; _prototypeManager.TryIndex(component.EmoteSoundsId, out EmoteSounds); - var meta = MetaData(uid); - var name = meta.EntityName; - EnsureComp<AutoEmoteComponent>(uid); _autoEmote.AddEmote(uid, "CluwneGiggle"); EnsureComp<ClumsyComponent>(uid); @@ -78,7 +78,7 @@ private void OnComponentStartup(EntityUid uid, CluwneComponent component, Compon _popupSystem.PopupEntity(Loc.GetString("cluwne-transform", ("target", uid)), uid, PopupType.LargeCaution); _audio.PlayPvs(component.SpawnSound, uid); - _metaData.SetEntityName(uid, Loc.GetString("cluwne-name-prefix", ("target", name)), meta); + _nameMod.RefreshNameModifiers(uid); SetOutfitCommand.SetOutfit(uid, "CluwneGear", EntityManager); } @@ -105,4 +105,12 @@ private void OnEmote(EntityUid uid, CluwneComponent component, ref EmoteEvent ar _chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal); } } + + /// <summary> + /// Applies "Cluwnified" prefix + /// </summary> + private void OnRefreshNameModifiers(Entity<CluwneComponent> entity, ref RefreshNameModifiersEvent args) + { + args.AddModifier("cluwne-name-prefix"); + } } diff --git a/Content.Server/Cocoon/CocoonerSystem.cs b/Content.Server/Cocoon/CocoonerSystem.cs index 676eb9808b2..12fb8b52c24 100644 --- a/Content.Server/Cocoon/CocoonerSystem.cs +++ b/Content.Server/Cocoon/CocoonerSystem.cs @@ -135,8 +135,7 @@ private void StartCocooning(EntityUid uid, CocoonerComponent component, EntityUi var args = new DoAfterArgs(EntityManager, uid, delay, new CocoonDoAfterEvent(), uid, target: target) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true }; _doAfter.TryStartDoAfter(args); @@ -151,8 +150,7 @@ private void StartUnCocooning(EntityUid uid, CocoonerComponent component, Entity var args = new DoAfterArgs(EntityManager, uid, delay, new UnCocoonDoAfterEvent(), uid, target: target) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true }; _doAfter.TryStartDoAfter(args); diff --git a/Content.Server/Communications/CommsHackerSystem.cs b/Content.Server/Communications/CommsHackerSystem.cs index bbe64a7987a..4d149ca1ad4 100644 --- a/Content.Server/Communications/CommsHackerSystem.cs +++ b/Content.Server/Communications/CommsHackerSystem.cs @@ -48,7 +48,7 @@ private void OnBeforeInteractHand(EntityUid uid, CommsHackerComponent comp, Befo var doAfterArgs = new DoAfterArgs(EntityManager, uid, comp.Delay, new TerrorDoAfterEvent(), target: target, used: uid, eventTarget: uid) { BreakOnDamage = true, - BreakOnUserMove = true, + BreakOnMove = true, MovementThreshold = 0.5f, CancelDuplicate = false }; diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index 0cdcb3a47f8..c3e5b912549 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -1,15 +1,11 @@ -using System.Globalization; -using Content.Server.Access.Systems; using Content.Server.Administration.Logs; using Content.Server.AlertLevel; using Content.Server.Chat.Systems; -using Content.Server.DeviceNetwork; using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.Interaction; using Content.Server.Popups; using Content.Server.RoundEnd; -using Content.Server.Screens; using Content.Server.Screens.Components; using Content.Server.Shuttles.Systems; using Content.Server.Station.Systems; @@ -21,6 +17,7 @@ using Content.Shared.Database; using Content.Shared.DeviceNetwork; using Content.Shared.Emag.Components; +using Content.Shared.IdentityManagement; using Content.Shared.Popups; using Robust.Server.GameObjects; using Robust.Shared.Configuration; @@ -38,7 +35,6 @@ public sealed class CommunicationsConsoleSystem : EntitySystem [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; - [Dependency] private readonly IdCardSystem _idCardSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; [Dependency] private readonly StationSystem _stationSystem = default!; @@ -257,10 +253,9 @@ private void OnAnnounceMessage(EntityUid uid, CommunicationsConsoleComponent com return; } - if (_idCardSystem.TryFindIdCard(mob, out var id)) - { - author = $"{id.Comp.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id.Comp.JobTitle ?? string.Empty)})".Trim(); - } + var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(uid, mob); + RaiseLocalEvent(tryGetIdentityShortInfoEvent); + author = tryGetIdentityShortInfoEvent.Title; } comp.AnnouncementCooldownRemaining = comp.Delay; diff --git a/Content.Server/Configurable/ConfigurationSystem.cs b/Content.Server/Configurable/ConfigurationSystem.cs index 5f5f1ef7d16..bf89c3f7ed1 100644 --- a/Content.Server/Configurable/ConfigurationSystem.cs +++ b/Content.Server/Configurable/ConfigurationSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Configurable; using Content.Shared.Interaction; using Content.Shared.Tools.Components; +using Content.Shared.Tools.Systems; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.Player; @@ -11,6 +12,7 @@ namespace Content.Server.Configurable; public sealed class ConfigurationSystem : EntitySystem { [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; public override void Initialize() { @@ -28,7 +30,7 @@ private void OnInteractUsing(EntityUid uid, ConfigurationComponent component, In if (args.Handled) return; - if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.Contains(component.QualityNeeded)) + if (!_toolSystem.HasQuality(args.Used, component.QualityNeeded)) return; args.Handled = _uiSystem.TryOpenUi(uid, ConfigurationUiKey.Key, args.User); @@ -68,7 +70,7 @@ private void OnUpdate(EntityUid uid, ConfigurationComponent component, Configura private void OnInsert(EntityUid uid, ConfigurationComponent component, ContainerIsInsertingAttemptEvent args) { - if (!TryComp(args.EntityUid, out ToolComponent? tool) || !tool.Qualities.Contains(component.QualityNeeded)) + if (!_toolSystem.HasQuality(args.EntityUid, component.QualityNeeded)) return; args.Cancel(); diff --git a/Content.Server/Connection/ConnectionManager.Whitelist.cs b/Content.Server/Connection/ConnectionManager.Whitelist.cs new file mode 100644 index 00000000000..212c87e17a6 --- /dev/null +++ b/Content.Server/Connection/ConnectionManager.Whitelist.cs @@ -0,0 +1,221 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Connection.Whitelist; +using Content.Server.Connection.Whitelist.Conditions; +using Content.Server.Database; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Content.Shared.Players.PlayTimeTracking; +using Robust.Shared.Network; + +namespace Content.Server.Connection; + +/// <summary> +/// Handles whitelist conditions for incoming connections. +/// </summary> +public sealed partial class ConnectionManager +{ + private PlayerConnectionWhitelistPrototype[]? _whitelists; + + public void PostInit() + { + _cfg.OnValueChanged(CCVars.WhitelistPrototypeList, UpdateWhitelists, true); + } + + private void UpdateWhitelists(string s) + { + var list = new List<PlayerConnectionWhitelistPrototype>(); + foreach (var id in s.Split(',')) + { + if (_prototypeManager.TryIndex(id, out PlayerConnectionWhitelistPrototype? prototype)) + { + list.Add(prototype); + } + else + { + _sawmill.Fatal($"Whitelist prototype {id} does not exist. Denying all connections."); + _whitelists = null; // Invalidate the list, causes deny on all connections. + return; + } + } + + _whitelists = list.ToArray(); + } + + private bool IsValid(PlayerConnectionWhitelistPrototype whitelist, int playerCount) + { + return playerCount >= whitelist.MinimumPlayers && playerCount <= whitelist.MaximumPlayers; + } + + public async Task<(bool isWhitelisted, string? denyMessage)> IsWhitelisted(PlayerConnectionWhitelistPrototype whitelist, NetUserData data, ISawmill sawmill) + { + var cacheRemarks = await _db.GetAllAdminRemarks(data.UserId); + var cachePlaytime = await _db.GetPlayTimes(data.UserId); + + foreach (var condition in whitelist.Conditions) + { + bool matched; + string denyMessage; + switch (condition) + { + case ConditionAlwaysMatch: + matched = true; + denyMessage = Loc.GetString("whitelist-always-deny"); + break; + case ConditionManualWhitelistMembership: + matched = await CheckConditionManualWhitelist(data); + denyMessage = Loc.GetString("whitelist-manual"); + break; + case ConditionManualBlacklistMembership: + matched = await CheckConditionManualBlacklist(data); + denyMessage = Loc.GetString("whitelist-blacklisted"); + break; + case ConditionNotesDateRange conditionNotes: + matched = CheckConditionNotesDateRange(conditionNotes, cacheRemarks); + denyMessage = Loc.GetString("whitelist-notes"); + break; + case ConditionPlayerCount conditionPlayerCount: + matched = CheckConditionPlayerCount(conditionPlayerCount); + denyMessage = Loc.GetString("whitelist-player-count"); + break; + case ConditionPlaytime conditionPlaytime: + matched = CheckConditionPlaytime(conditionPlaytime, cachePlaytime); + denyMessage = Loc.GetString("whitelist-playtime", ("minutes", conditionPlaytime.MinimumPlaytime)); + break; + case ConditionNotesPlaytimeRange conditionNotesPlaytimeRange: + matched = CheckConditionNotesPlaytimeRange(conditionNotesPlaytimeRange, cacheRemarks, cachePlaytime); + denyMessage = Loc.GetString("whitelist-notes"); + break; + default: + throw new NotImplementedException($"Whitelist condition {condition.GetType().Name} not implemented"); + } + + sawmill.Verbose($"User {data.UserName} whitelist condition {condition.GetType().Name} result: {matched}"); + sawmill.Verbose($"Action: {condition.Action.ToString()}"); + switch (condition.Action) + { + case ConditionAction.Allow: + if (matched) + { + sawmill.Verbose($"User {data.UserName} passed whitelist condition {condition.GetType().Name} and it's a breaking condition"); + return (true, denyMessage); + } + break; + case ConditionAction.Deny: + if (matched) + { + sawmill.Verbose($"User {data.UserName} failed whitelist condition {condition.GetType().Name}"); + return (false, denyMessage); + } + break; + default: + sawmill.Verbose($"User {data.UserName} failed whitelist condition {condition.GetType().Name} but it's not a breaking condition"); + break; + } + } + sawmill.Verbose($"User {data.UserName} passed all whitelist conditions"); + return (true, null); + } + + #region Condition Checking + + private async Task<bool> CheckConditionManualWhitelist(NetUserData data) + { + return !(await _db.GetWhitelistStatusAsync(data.UserId)); + } + + private async Task<bool> CheckConditionManualBlacklist(NetUserData data) + { + return await _db.GetBlacklistStatusAsync(data.UserId); + } + + private bool CheckConditionNotesDateRange(ConditionNotesDateRange conditionNotes, List<IAdminRemarksRecord> remarks) + { + var range = DateTime.UtcNow.AddDays(-conditionNotes.Range); + + return CheckRemarks(remarks, + conditionNotes.IncludeExpired, + conditionNotes.IncludeSecret, + conditionNotes.MinimumSeverity, + conditionNotes.MinimumNotes, + adminRemarksRecord => adminRemarksRecord.CreatedAt > range); + } + + private bool CheckConditionPlayerCount(ConditionPlayerCount conditionPlayerCount) + { + var count = _plyMgr.PlayerCount; + return count >= conditionPlayerCount.MinimumPlayers && count <= conditionPlayerCount.MaximumPlayers; + } + + private bool CheckConditionPlaytime(ConditionPlaytime conditionPlaytime, List<PlayTime> playtime) + { + var tracker = playtime.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall); + if (tracker is null) + { + return false; + } + + return tracker.TimeSpent.TotalMinutes >= conditionPlaytime.MinimumPlaytime; + } + + private bool CheckConditionNotesPlaytimeRange( + ConditionNotesPlaytimeRange conditionNotesPlaytimeRange, + List<IAdminRemarksRecord> remarks, + List<PlayTime> playtime) + { + var overallTracker = playtime.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall); + if (overallTracker is null) + { + return false; + } + + return CheckRemarks(remarks, + conditionNotesPlaytimeRange.IncludeExpired, + conditionNotesPlaytimeRange.IncludeSecret, + conditionNotesPlaytimeRange.MinimumSeverity, + conditionNotesPlaytimeRange.MinimumNotes, + adminRemarksRecord => adminRemarksRecord.PlaytimeAtNote >= overallTracker.TimeSpent - TimeSpan.FromMinutes(conditionNotesPlaytimeRange.Range)); + } + + private bool CheckRemarks(List<IAdminRemarksRecord> remarks, bool includeExpired, bool includeSecret, NoteSeverity minimumSeverity, int MinimumNotes, Func<IAdminRemarksRecord, bool> additionalCheck) + { + var utcNow = DateTime.UtcNow; + + var notes = remarks.Count(r => r is AdminNoteRecord note && note.Severity >= minimumSeverity && (includeSecret || !note.Secret) && (includeExpired || note.ExpirationTime == null || note.ExpirationTime > utcNow)); + if (notes < MinimumNotes) + { + return false; + } + + foreach (var adminRemarksRecord in remarks) + { + // If we're not including expired notes, skip them + if (!includeExpired && (adminRemarksRecord.ExpirationTime == null || adminRemarksRecord.ExpirationTime <= utcNow)) + continue; + + // In order to get the severity of the remark, we need to see if it's an AdminNoteRecord. + if (adminRemarksRecord is not AdminNoteRecord adminNoteRecord) + continue; + + // We want to filter out secret notes if we're not including them. + if (!includeSecret && adminNoteRecord.Secret) + continue; + + // At this point, we need to remove the note if it's not within the severity range. + if (adminNoteRecord.Severity < minimumSeverity) + continue; + + // Perform the additional check specific to each method + if (!additionalCheck(adminRemarksRecord)) + continue; + + // If we've made it this far, we have a match + return true; + } + + // No matches + return false; + } + + #endregion +} diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index 64e93c5af19..1f36ba63ddc 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -1,7 +1,8 @@ using System.Collections.Immutable; -using System.Runtime.InteropServices; -using System.Text.Json.Nodes; +using System.Linq; using System.Threading.Tasks; +using System.Runtime.InteropServices; +using Content.Server.Chat.Managers; using Content.Server.Database; using Content.Server.GameTicking; using Content.Server.Preferences.Managers; @@ -10,15 +11,22 @@ using Content.Shared.Players.PlayTimeTracking; using Robust.Server.Player; using Robust.Shared.Configuration; +using Robust.Shared.Enums; using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Player; using Robust.Shared.Timing; +/* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ namespace Content.Server.Connection { public interface IConnectionManager { void Initialize(); + void PostInit(); Task<bool> HasPrivilegedJoin(NetUserId userId); @@ -38,20 +46,22 @@ public interface IConnectionManager /// <summary> /// Handles various duties like guest username assignment, bans, connection logs, etc... /// </summary> - public sealed class ConnectionManager : IConnectionManager + public sealed partial class ConnectionManager : IConnectionManager { - [Dependency] private readonly IServerDbManager _dbManager = default!; [Dependency] private readonly IPlayerManager _plyMgr = default!; [Dependency] private readonly IServerNetManager _netMgr = default!; [Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; - private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = []; private ISawmill _sawmill = default!; + private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = []; + private List<NetUserId> _connectedWhitelistedPlayers = new(); // DeltaV - Soft whitelist improvements @@ -63,6 +73,8 @@ public void Initialize() _netMgr.Connected += OnConnected; // DeltaV - Soft whitelist improvements _netMgr.Disconnect += OnDisconnected; // DeltaV - Soft whitelist improvements _netMgr.AssignUserIdCallback = AssignUserIdCallback; + _plyMgr.PlayerStatusChanged += PlayerStatusChanged; + _plyMgr.PlayerStatusChanged += PlayerStatusChanged; // Approval-based IP bans disabled because they don't play well with Happy Eyeballs. // _netMgr.HandleApprovalCallback = HandleApproval; } @@ -106,27 +118,74 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) var serverId = (await _serverDbEntry.ServerEntity).Id; + var hwid = e.UserData.GetModernHwid(); + var trust = e.UserData.Trust; + if (deny != null) { var (reason, msg, banHits) = deny.Value; - var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, reason, serverId); + var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, reason, serverId); if (banHits is { Count: > 0 }) await _db.AddServerBanHitsAsync(id, banHits); - e.Deny(msg); + var properties = new Dictionary<string, object>(); + if (reason == ConnectionDenyReason.Full) + properties["delay"] = _cfg.GetCVar(CCVars.GameServerFullReconnectDelay); + + e.Deny(new NetDenyReason(msg, properties)); } else { - await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, null, serverId); + await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, null, serverId); if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType)) return; - await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, e.UserData.HWId); + await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, hwid); } } + private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs args) + { + if (args.NewStatus == SessionStatus.Connected) + { + AdminAlertIfSharedConnection(args.Session); + } + } + + private void AdminAlertIfSharedConnection(ICommonSession newSession) + { + var playerThreshold = _cfg.GetCVar(CCVars.AdminAlertMinPlayersSharingConnection); + if (playerThreshold < 0) + return; + + var addr = newSession.Channel.RemoteEndPoint.Address; + + var otherConnectionsFromAddress = _plyMgr.Sessions.Where(session => + session.Status is SessionStatus.Connected or SessionStatus.InGame + && session.Channel.RemoteEndPoint.Address.Equals(addr) + && session.UserId != newSession.UserId) + .ToList(); + + var otherConnectionCount = otherConnectionsFromAddress.Count; + if (otherConnectionCount + 1 < playerThreshold) // Add one for the total, not just others, using the address + return; + + var username = newSession.Name; + var otherUsernames = string.Join(", ", + otherConnectionsFromAddress.Select(session => session.Name)); + + _chatManager.SendAdminAlert(Loc.GetString("admin-alert-shared-connection", + ("player", username), + ("otherCount", otherConnectionCount), + ("otherList", otherUsernames))); + } + + /* + * TODO: Jesus H Christ what is this utter mess of a function + * TODO: Break this apart into is constituent steps. + */ private async Task<(ConnectionDenyReason, string, List<ServerBanDef>? bansHit)?> ShouldDeny( NetConnectingArgs e) { @@ -141,7 +200,9 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) hwId = null; } - var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false); + var modernHwid = e.UserData.ModernHWIds; + + var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); if (bans.Count > 0) { var firstBan = bans[0]; @@ -155,17 +216,17 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) return null; } - var adminData = await _dbManager.GetAdminDataForAsync(e.UserId); + var adminData = await _db.GetAdminDataForAsync(e.UserId); if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null) { var showReason = _cfg.GetCVar(CCVars.PanicBunkerShowReason); var customReason = _cfg.GetCVar(CCVars.PanicBunkerCustomReason); - var minMinutesAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge); - var record = await _dbManager.GetPlayerRecordByUserId(userId); + var minHoursAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge); + var record = await _db.GetPlayerRecordByUserId(userId); var validAccountAge = record != null && - record.FirstSeenTime.CompareTo(DateTimeOffset.Now - TimeSpan.FromMinutes(minMinutesAge)) <= 0; + record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromHours(minHoursAge)) <= 0; var bypassAllowed = _cfg.GetCVar(CCVars.BypassBunkerWhitelist) && await _db.GetWhitelistStatusAsync(userId); // Use the custom reason if it exists & they don't have the minimum account age @@ -178,7 +239,7 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) { return (ConnectionDenyReason.Panic, Loc.GetString("panic-bunker-account-denied-reason", - ("reason", Loc.GetString("panic-bunker-account-reason-account", ("minutes", minMinutesAge)))), null); + ("reason", Loc.GetString("panic-bunker-account-reason-account", ("hours", minHoursAge)))), null); } var minOverallHours = _cfg.GetCVar(CCVars.PanicBunkerMinOverallHours); @@ -204,46 +265,68 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) } } + if (_cfg.GetCVar(CCVars.BabyJailEnabled) && adminData == null) + { + var result = await IsInvalidConnectionDueToBabyJail(userId, e); - var isPrivileged = await HasPrivilegedJoin(userId); - var isQueueEnabled = _cfg.GetCVar(CCVars.QueueEnabled); + if (result.IsInvalid) + return (ConnectionDenyReason.BabyJail, result.Reason, null); + } - if (_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !isPrivileged && !isQueueEnabled) + var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) && + ticker.PlayerGameStatuses.TryGetValue(userId, out var status) && + status == PlayerGameStatus.JoinedGame; + var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null; + if ((_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame) + { return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null); - + } // DeltaV - Replace existing softwhitelist implementation - if (false) //_cfg.GetCVar(CCVars.WhitelistEnabled)) + if (false)//if (_cfg.GetCVar(CCVars.WhitelistEnabled) && adminData is null) { - var min = _cfg.GetCVar(CCVars.WhitelistMinPlayers); - var max = _cfg.GetCVar(CCVars.WhitelistMaxPlayers); - var playerCountValid = _plyMgr.PlayerCount >= min && _plyMgr.PlayerCount < max; + if (_whitelists is null) + { + _sawmill.Error("Whitelist enabled but no whitelists loaded."); + // Misconfigured, deny everyone. + return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-misconfigured"), null); + } - if (playerCountValid && await _db.GetWhitelistStatusAsync(userId) == false - && adminData is null) + foreach (var whitelist in _whitelists) { - var msg = Loc.GetString(_cfg.GetCVar(CCVars.WhitelistReason)); - // was the whitelist playercount changed? - if (min > 0 || max < int.MaxValue) - msg += "\n" + Loc.GetString("whitelist-playercount-invalid", ("min", min), ("max", max)); - return (ConnectionDenyReason.Whitelist, msg, null); + if (!IsValid(whitelist, _plyMgr.PlayerCount)) + { + // Not valid for current player count. + continue; + } + + var whitelistStatus = await IsWhitelisted(whitelist, e.UserData, _sawmill); + if (!whitelistStatus.isWhitelisted) + { + // Not whitelisted. + return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-fail-prefix", ("msg", whitelistStatus.denyMessage!)), null); + } + + // Whitelisted, don't check any more. + break; } } // DeltaV - Soft whitelist improvements + // TODO: replace this with a whitelist config prototype with a connected whitelisted players condition if (_cfg.GetCVar(CCVars.WhitelistEnabled)) { var connectedPlayers = _plyMgr.PlayerCount; var connectedWhitelist = _connectedWhitelistedPlayers.Count; - var slots = _cfg.GetCVar(CCVars.WhitelistMinPlayers); + var slots = 25; var noSlotsOpen = slots > 0 && slots < connectedPlayers - connectedWhitelist; if (noSlotsOpen && await _db.GetWhitelistStatusAsync(userId) == false && adminData is null) { - var msg = Loc.GetString(_cfg.GetCVar(CCVars.WhitelistReason)); + var msg = Loc.GetString("whitelist-not-whitelisted-peri"); if (slots > 0) msg += "\n" + Loc.GetString("whitelist-playercount-invalid", ("min", slots), ("max", _cfg.GetCVar(CCVars.SoftMaxPlayers))); @@ -255,6 +338,72 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) return null; } + private async Task<(bool IsInvalid, string Reason)> IsInvalidConnectionDueToBabyJail(NetUserId userId, NetConnectingArgs e) + { + // If you're whitelisted then bypass this whole thing + if (await _db.GetWhitelistStatusAsync(userId)) + return (false, ""); + + // Initial cvar retrieval + var showReason = _cfg.GetCVar(CCVars.BabyJailShowReason); + var reason = _cfg.GetCVar(CCVars.BabyJailCustomReason); + var maxAccountAgeHours = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge); + var maxPlaytimeHours = _cfg.GetCVar(CCVars.BabyJailMaxOverallHours); + + // Wait some time to lookup data + var record = await _db.GetPlayerRecordByUserId(userId); + + // No player record = new account or the DB is having a skill issue + if (record == null) + return (false, ""); + + var isAccountAgeInvalid = record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromHours(maxAccountAgeHours)) <= 0; + + if (isAccountAgeInvalid) + { + _sawmill.Debug($"Baby jail will deny {userId} for account age {record.FirstSeenTime}"); // Remove on or after 2024-09 + } + + if (isAccountAgeInvalid && showReason) + { + var locAccountReason = reason != string.Empty + ? reason + : Loc.GetString("baby-jail-account-denied-reason", + ("reason", + Loc.GetString( + "baby-jail-account-reason-account", + ("hours", maxAccountAgeHours)))); + + return (true, locAccountReason); + } + + var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall); + var isTotalPlaytimeInvalid = overallTime != null && overallTime.TimeSpent.TotalHours >= maxAccountAgeHours; + + if (isTotalPlaytimeInvalid) + { + _sawmill.Debug($"Baby jail will deny {userId} for playtime {overallTime!.TimeSpent}"); // Remove on or after 2024-09 + } + + if (isTotalPlaytimeInvalid && showReason) + { + var locPlaytimeReason = reason != string.Empty + ? reason + : Loc.GetString("baby-jail-account-denied-reason", + ("reason", + Loc.GetString( + "baby-jail-account-reason-overall", + ("hours", maxPlaytimeHours)))); + + return (true, locPlaytimeReason); + } + + if (!showReason && isTotalPlaytimeInvalid || isAccountAgeInvalid) + return (true, Loc.GetString("baby-jail-account-denied")); + + return (false, ""); + } + private bool HasTemporaryBypass(NetUserId user) { return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime; @@ -306,10 +455,10 @@ private async void OnDisconnected(object? sender, NetChannelArgs e) public async Task<bool> HasPrivilegedJoin(NetUserId userId) { - var isAdmin = await _dbManager.GetAdminDataForAsync(userId) != null; + var isAdmin = await _db.GetAdminDataForAsync(userId) != null; var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) && - ticker.PlayerGameStatuses.TryGetValue(userId, out var status) && - status == PlayerGameStatus.JoinedGame; + ticker.PlayerGameStatuses.TryGetValue(userId, out var status) && + status == PlayerGameStatus.JoinedGame; return isAdmin || wasInGame; } } diff --git a/Content.Server/Connection/UserDataExt.cs b/Content.Server/Connection/UserDataExt.cs new file mode 100644 index 00000000000..a409f79a75d --- /dev/null +++ b/Content.Server/Connection/UserDataExt.cs @@ -0,0 +1,24 @@ +using Content.Shared.Database; +using Robust.Shared.Network; + +namespace Content.Server.Connection; + +/// <summary> +/// Helper functions for working with <see cref="NetUserData"/>. +/// </summary> +public static class UserDataExt +{ + /// <summary> + /// Get the preferred HWID that should be used for new records related to a player. + /// </summary> + /// <remarks> + /// Players can have zero or more HWIDs, but for logging things like connection logs we generally + /// only want a single one. This method returns a nullable method. + /// </remarks> + public static ImmutableTypedHwid? GetModernHwid(this NetUserData userData) + { + return userData.ModernHWIds.Length == 0 + ? null + : new ImmutableTypedHwid(userData.ModernHWIds[0], HwidType.Modern); + } +} diff --git a/Content.Server/Connection/Whitelist/BlacklistCommands.cs b/Content.Server/Connection/Whitelist/BlacklistCommands.cs new file mode 100644 index 00000000000..e79fb7ee22e --- /dev/null +++ b/Content.Server/Connection/Whitelist/BlacklistCommands.cs @@ -0,0 +1,117 @@ +using Content.Server.Administration; +using Content.Server.Database; +using Content.Shared.Administration; +using Robust.Shared.Console; + +namespace Content.Server.Connection.Whitelist; + +[AdminCommand(AdminFlags.Ban)] +public sealed class AddBlacklistCommand : LocalizedCommands +{ + [Dependency] private readonly IPlayerLocator _playerLocator = default!; + [Dependency] private readonly IServerDbManager _db = default!; + + public override string Command => "blacklistadd"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length == 0) + { + shell.WriteError(Loc.GetString("shell-need-minimum-one-argument")); + shell.WriteLine(Help); + return; + } + + if (args.Length > 1) + { + shell.WriteError(Loc.GetString("shell-need-exactly-one-argument")); + shell.WriteLine(Help); + return; + } + + var name = args[0]; + var data = await _playerLocator.LookupIdByNameAsync(name); + + if (data == null) + { + shell.WriteError(Loc.GetString("cmd-blacklistadd-not-found", ("username", args[0]))); + return; + } + var guid = data.UserId; + var isBlacklisted = await _db.GetBlacklistStatusAsync(guid); + if (isBlacklisted) + { + shell.WriteLine(Loc.GetString("cmd-blacklistadd-existing", ("username", data.Username))); + return; + } + + await _db.AddToBlacklistAsync(guid); + shell.WriteLine(Loc.GetString("cmd-blacklistadd-added", ("username", data.Username))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHint(Loc.GetString("cmd-blacklistadd-arg-player")); + } + + return CompletionResult.Empty; + } +} + +[AdminCommand(AdminFlags.Ban)] +public sealed class RemoveBlacklistCommand : LocalizedCommands +{ + [Dependency] private readonly IPlayerLocator _playerLocator = default!; + [Dependency] private readonly IServerDbManager _db = default!; + + public override string Command => "blacklistremove"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length == 0) + { + shell.WriteError(Loc.GetString("shell-need-minimum-one-argument")); + shell.WriteLine(Help); + return; + } + + if (args.Length > 1) + { + shell.WriteError(Loc.GetString("shell-need-exactly-one-argument")); + shell.WriteLine(Help); + return; + } + + var name = args[0]; + var data = await _playerLocator.LookupIdByNameAsync(name); + + if (data == null) + { + shell.WriteError(Loc.GetString("cmd-blacklistremove-not-found", ("username", args[0]))); + return; + } + + var guid = data.UserId; + var isBlacklisted = await _db.GetBlacklistStatusAsync(guid); + if (!isBlacklisted) + { + shell.WriteLine(Loc.GetString("cmd-blacklistremove-existing", ("username", data.Username))); + return; + } + + await _db.RemoveFromBlacklistAsync(guid); + shell.WriteLine(Loc.GetString("cmd-blacklistremove-removed", ("username", data.Username))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHint(Loc.GetString("cmd-blacklistremove-arg-player")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionAlwaysMatch.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionAlwaysMatch.cs new file mode 100644 index 00000000000..9fe5db40120 --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionAlwaysMatch.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that always matches +/// </summary> +public sealed partial class ConditionAlwaysMatch : WhitelistCondition +{ + +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionManualBlacklistMembership.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionManualBlacklistMembership.cs new file mode 100644 index 00000000000..9d67129e71f --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionManualBlacklistMembership.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Content.Server.Database; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that matches if the player is in the manual blacklist. +/// </summary> +public sealed partial class ConditionManualBlacklistMembership : WhitelistCondition +{ +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionManualWhitelistMembership.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionManualWhitelistMembership.cs new file mode 100644 index 00000000000..a31835f9c2f --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionManualWhitelistMembership.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Content.Server.Database; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that matches if the player is in the manual whitelist. +/// </summary> +public sealed partial class ConditionManualWhitelistMembership : WhitelistCondition +{ +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionNotesDateRange.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionNotesDateRange.cs new file mode 100644 index 00000000000..1e34bc63734 --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionNotesDateRange.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Database; +using Content.Shared.Database; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that matches if the player has notes within a certain date range. +/// </summary> +public sealed partial class ConditionNotesDateRange : WhitelistCondition +{ + [DataField] + public bool IncludeExpired = false; + + [DataField] + public NoteSeverity MinimumSeverity = NoteSeverity.Minor; + + /// <summary> + /// The minimum number of notes required. + /// </summary> + [DataField] + public int MinimumNotes = 1; + + /// <summary> + /// Range in days to check for notes. + /// </summary> + [DataField] + public int Range = int.MaxValue; + + [DataField] + public bool IncludeSecret = false; +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionNotesPlaytimeRange.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionNotesPlaytimeRange.cs new file mode 100644 index 00000000000..d2d22a8cd0d --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionNotesPlaytimeRange.cs @@ -0,0 +1,30 @@ +using Content.Shared.Database; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that matches if the player has notes within a certain playtime range. +/// </summary> +public sealed partial class ConditionNotesPlaytimeRange : WhitelistCondition +{ + [DataField] + public bool IncludeExpired = false; + + [DataField] + public NoteSeverity MinimumSeverity = NoteSeverity.Minor; + + /// <summary> + /// The minimum number of notes required. + /// </summary> + [DataField] + public int MinimumNotes = 1; + + /// <summary> + /// The range in minutes to check for notes. + /// </summary> + [DataField] + public int Range = int.MaxValue; + + [DataField] + public bool IncludeSecret = false; +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionPlayerCount.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionPlayerCount.cs new file mode 100644 index 00000000000..432ad32c4ac --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionPlayerCount.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Robust.Server.Player; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that matches if the player count is within a certain range. +/// </summary> +public sealed partial class ConditionPlayerCount : WhitelistCondition +{ + [DataField] + public int MinimumPlayers = 0; + [DataField] + public int MaximumPlayers = int.MaxValue; +} diff --git a/Content.Server/Connection/Whitelist/Conditions/ConditionPlaytime.cs b/Content.Server/Connection/Whitelist/Conditions/ConditionPlaytime.cs new file mode 100644 index 00000000000..7b45181e120 --- /dev/null +++ b/Content.Server/Connection/Whitelist/Conditions/ConditionPlaytime.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Content.Server.Database; +using Content.Shared.Players.PlayTimeTracking; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist.Conditions; + +/// <summary> +/// Condition that matches if the player has played for a certain amount of time. +/// </summary> +public sealed partial class ConditionPlaytime : WhitelistCondition +{ + [DataField] + public int MinimumPlaytime = 0; // In minutes +} diff --git a/Content.Server/Connection/Whitelist/WhitelistCondition.cs b/Content.Server/Connection/Whitelist/WhitelistCondition.cs new file mode 100644 index 00000000000..b68fd529968 --- /dev/null +++ b/Content.Server/Connection/Whitelist/WhitelistCondition.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Robust.Shared.Network; + +namespace Content.Server.Connection.Whitelist; + +/// <summary> +/// This class is used to determine if a player should be allowed to join the server. +/// It is used in <see cref="PlayerConnectionWhitelistPrototype"/> +/// </summary> +[ImplicitDataDefinitionForInheritors] +[MeansImplicitUse] +public abstract partial class WhitelistCondition +{ + /// <summary> + /// What action should be taken if this condition is met? + /// Defaults to <see cref="ConditionAction.Next"/>. + /// </summary> + [DataField] + public ConditionAction Action { get; set; } = ConditionAction.Next; +} + +/// <summary> +/// Determines what action should be taken if a condition is met. +/// </summary> +public enum ConditionAction +{ + /// <summary> + /// The player is allowed to join, and the next conditions will be skipped. + /// </summary> + Allow, + /// <summary> + /// The player is denied to join, and the next conditions will be skipped. + /// </summary> + Deny, + /// <summary> + /// The next condition should be checked. + /// </summary> + Next +} diff --git a/Content.Server/Connection/Whitelist/WhitelistPrototype.cs b/Content.Server/Connection/Whitelist/WhitelistPrototype.cs new file mode 100644 index 00000000000..2b8b9babbc0 --- /dev/null +++ b/Content.Server/Connection/Whitelist/WhitelistPrototype.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Server.Connection.Whitelist; + +/// <summary> +/// Used by the <see cref="ConnectionManager"/> to determine if a player should be allowed to join the server. +/// Used in the whitelist.prototype_list CVar. +/// +/// Whitelists are used to determine if a player is allowed to connect. +/// You define a PlayerConnectionWhitelist with a list of conditions. +/// Every condition has a type and a <see cref="ConditionAction"/> along with other parameters depending on the type. +/// Action must either be Allow, Deny or Next. +/// Allow means the player is instantly allowed to connect if the condition is met. +/// Deny means the player is instantly denied to connect if the condition is met. +/// Next means the next condition in the list is checked. +/// If the condition doesn't match, the next condition is checked. +/// </summary> +[Prototype("playerConnectionWhitelist")] +public sealed class PlayerConnectionWhitelistPrototype : IPrototype +{ + [IdDataField] + public string ID { get; } = default!; + + /// <summary> + /// Minimum number of players required for this whitelist to be active. + /// If there are less players than this, the whitelist will be ignored and the next one in the list will be used. + /// </summary> + [DataField] + public int MinimumPlayers { get; } = 0; + + /// <summary> + /// Maximum number of players allowed for this whitelist to be active. + /// If there are more players than this, the whitelist will be ignored and the next one in the list will be used. + /// </summary> + [DataField] + public int MaximumPlayers { get; } = int.MaxValue; + + [DataField] + public WhitelistCondition[] Conditions { get; } = default!; +} diff --git a/Content.Server/Construction/ConstructionSystem.Computer.cs b/Content.Server/Construction/ConstructionSystem.Computer.cs index 0685b08f4ff..6951d44b4d5 100644 --- a/Content.Server/Construction/ConstructionSystem.Computer.cs +++ b/Content.Server/Construction/ConstructionSystem.Computer.cs @@ -1,6 +1,7 @@ using Content.Server.Construction.Components; using Content.Server.Power.Components; using Content.Shared.Computer; +using Content.Shared.Power; using Robust.Shared.Containers; namespace Content.Server.Construction; diff --git a/Content.Server/Construction/ConstructionSystem.Initial.cs b/Content.Server/Construction/ConstructionSystem.Initial.cs index 39705fc1974..75a32ca3d8c 100644 --- a/Content.Server/Construction/ConstructionSystem.Initial.cs +++ b/Content.Server/Construction/ConstructionSystem.Initial.cs @@ -14,6 +14,7 @@ using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Storage; +using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -29,6 +30,7 @@ public sealed partial class ConstructionSystem [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; // --- WARNING! LEGACY CODE AHEAD! --- // This entire file contains the legacy code for initial construction. @@ -79,7 +81,7 @@ private IEnumerable<EntityUid> EnumerateNearby(EntityUid user) } } - var pos = Transform(user).MapPosition; + var pos = _transformSystem.GetMapCoordinates(user); foreach (var near in _lookupSystem.GetEntitiesInRange(pos, 2f, LookupFlags.Contained | LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Approximate)) { @@ -247,8 +249,7 @@ void ShutdownContainers() var doAfterArgs = new DoAfterArgs(EntityManager, user, doAfterTime, new AwaitedDoAfterEvent(), null) { BreakOnDamage = true, - BreakOnTargetMove = false, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = false, // allow simultaneously starting several construction jobs using the same stack of materials. CancelDuplicate = false, @@ -330,7 +331,7 @@ public async Task<bool> TryStartItemConstruction(string prototype, EntityUid use return false; } - if (constructionPrototype.EntityWhitelist != null && !constructionPrototype.EntityWhitelist.IsValid(user)) + if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user)) { _popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user); return false; @@ -409,7 +410,7 @@ private async void HandleStartStructureConstruction(TryStartStructureConstructio return; } - if (constructionPrototype.EntityWhitelist != null && !constructionPrototype.EntityWhitelist.IsValid(user)) + if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user)) { _popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user); return; diff --git a/Content.Server/Construction/ConstructionSystem.Interactions.cs b/Content.Server/Construction/ConstructionSystem.Interactions.cs index 5361b65b1ff..ad7b2a11b0e 100644 --- a/Content.Server/Construction/ConstructionSystem.Interactions.cs +++ b/Content.Server/Construction/ConstructionSystem.Interactions.cs @@ -286,8 +286,7 @@ private HandleResult HandleInteraction(EntityUid uid, object ev, ConstructionGra var doAfterEventArgs = new DoAfterArgs(EntityManager, interactUsing.User, step.DoAfter, doAfterEv, uid, uid, interactUsing.Used) { BreakOnDamage = false, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }; diff --git a/Content.Server/Construction/FlatpackSystem.cs b/Content.Server/Construction/FlatpackSystem.cs index be082eba307..8068783abfe 100644 --- a/Content.Server/Construction/FlatpackSystem.cs +++ b/Content.Server/Construction/FlatpackSystem.cs @@ -4,6 +4,8 @@ using Content.Shared.Construction; using Content.Shared.Construction.Components; using Content.Shared.Containers.ItemSlots; +using Content.Shared.Power; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Construction; diff --git a/Content.Server/Construction/PartExchangerSystem.cs b/Content.Server/Construction/PartExchangerSystem.cs index c84d65b75e0..97de3ee4860 100644 --- a/Content.Server/Construction/PartExchangerSystem.cs +++ b/Content.Server/Construction/PartExchangerSystem.cs @@ -180,7 +180,7 @@ private void OnAfterInteract(EntityUid uid, PartExchangerComponent component, Af _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ExchangeDuration, new ExchangerDoAfterEvent(), uid, target: args.Target, used: uid) { BreakOnDamage = true, - BreakOnUserMove = true + BreakOnMove = true }); } } diff --git a/Content.Server/Construction/RefiningSystem.cs b/Content.Server/Construction/RefiningSystem.cs deleted file mode 100644 index 2ca32baf906..00000000000 --- a/Content.Server/Construction/RefiningSystem.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Content.Server.Construction.Components; -using Content.Server.Stack; -using Content.Shared.Construction; -using Content.Shared.Interaction; -using Content.Shared.Stacks; -using Content.Shared.Storage; -using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem; - -namespace Content.Server.Construction; - -public sealed class RefiningSystem : EntitySystem -{ - [Dependency] private readonly SharedToolSystem _toolSystem = default!; - [Dependency] private readonly StackSystem _stackSystem = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent<WelderRefinableComponent, InteractUsingEvent>(OnInteractUsing); - SubscribeLocalEvent<WelderRefinableComponent, WelderRefineDoAfterEvent>(OnDoAfter); - } - - private void OnInteractUsing(EntityUid uid, WelderRefinableComponent component, InteractUsingEvent args) - { - if (args.Handled) - return; - - args.Handled = _toolSystem.UseTool(args.Used, args.User, uid, component.RefineTime, component.QualityNeeded, new WelderRefineDoAfterEvent(), fuel: component.RefineFuel); - } - - private void OnDoAfter(EntityUid uid, WelderRefinableComponent component, WelderRefineDoAfterEvent args) - { - if (args.Cancelled) - return; - - // Get last owner coordinates and delete it - var resultPosition = Transform(uid).Coordinates; - EntityManager.DeleteEntity(uid); - - // Spawn each result after refine - foreach (var ent in EntitySpawnCollection.GetSpawns(component.RefineResult ?? new())) - { - var droppedEnt = Spawn(ent, resultPosition); - - // TODO: If something has a stack... Just use a prototype with a single thing in the stack. - // This is not a good way to do it. - if (TryComp<StackComponent>(droppedEnt, out var stack)) - _stackSystem.SetCount(droppedEnt, 1, stack); - } - } -} diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 4389c68c049..e8d6f2210fd 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -10,7 +10,6 @@ using Content.Shared.Security; using Content.Shared.StationRecords; using Robust.Server.GameObjects; -using Robust.Shared.Player; using System.Diagnostics.CodeAnalysis; using Content.Shared.IdentityManagement; using Content.Shared.Security.Components; @@ -108,8 +107,13 @@ private void OnChangeStatus(Entity<CriminalRecordsConsoleComponent> ent, ref Cri var name = RecordName(key.Value); var officer = Loc.GetString("criminal-records-console-unknown-officer"); - if (_idCard.TryFindIdCard(mob.Value, out var id) && id.Comp.FullName is { } fullName) - officer = fullName; + + var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value); + RaiseLocalEvent(tryGetIdentityShortInfoEvent); + if (tryGetIdentityShortInfoEvent.Title != null) + { + officer = tryGetIdentityShortInfoEvent.Title; + } (string, object)[] args; if (reason != null) diff --git a/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs b/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs index e54090cdbbf..547c29a202d 100644 --- a/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs +++ b/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs @@ -1,22 +1,19 @@ using Content.Shared.Damage; using Content.Shared.Tools; -using Robust.Shared.Utility; +using Robust.Shared.Prototypes; -namespace Content.Server.Damage.Components +namespace Content.Server.Damage.Components; + +[RegisterComponent] +public sealed partial class DamageOnToolInteractComponent : Component { - [RegisterComponent] - public sealed partial class DamageOnToolInteractComponent : Component - { - [DataField("tools")] - public PrototypeFlags<ToolQualityPrototype> Tools { get; private set; } = new (); + [DataField] + public ProtoId<ToolQualityPrototype> Tools { get; private set; } - // TODO: Remove this snowflake stuff, make damage per-tool quality perhaps? - [DataField("weldingDamage")] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier? WeldingDamage { get; private set; } + // TODO: Remove this snowflake stuff, make damage per-tool quality perhaps? + [DataField] + public DamageSpecifier? WeldingDamage { get; private set; } - [DataField("defaultDamage")] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier? DefaultDamage { get; private set; } - } + [DataField] + public DamageSpecifier? DefaultDamage { get; private set; } } diff --git a/Content.Server/Damage/Systems/DamageOnLandSystem.cs b/Content.Server/Damage/Systems/DamageOnLandSystem.cs index 8f01e791ead..2e72e76e6d8 100644 --- a/Content.Server/Damage/Systems/DamageOnLandSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnLandSystem.cs @@ -22,7 +22,7 @@ public override void Initialize() private void OnAttemptPacifiedThrow(Entity<DamageOnLandComponent> ent, ref AttemptPacifiedThrowEvent args) { // Allow healing projectiles, forbid any that do damage: - if (ent.Comp.Damage.Any()) + if (ent.Comp.Damage.AnyPositive()) { args.Cancel("pacified-cannot-throw"); } diff --git a/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs b/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs index 42676c1891c..5980455e49c 100644 --- a/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Database; using Content.Shared.Interaction; using Content.Shared.Tools.Components; +using Content.Shared.Tools.Systems; using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent; namespace Content.Server.Damage.Systems @@ -12,6 +13,7 @@ public sealed class DamageOnToolInteractSystem : EntitySystem { [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; public override void Initialize() { @@ -42,8 +44,7 @@ private void OnInteracted(EntityUid uid, DamageOnToolInteractComponent component args.Handled = true; } else if (component.DefaultDamage is {} damage - && EntityManager.TryGetComponent(args.Used, out ToolComponent? tool) - && tool.Qualities.ContainsAny(component.Tools)) + && _toolSystem.HasQuality(args.Used, component.Tools)) { var dmg = _damageableSystem.TryChangeDamage(args.Target, damage, origin: args.User); diff --git a/Content.Server/Damage/Systems/GodmodeSystem.cs b/Content.Server/Damage/Systems/GodmodeSystem.cs index 404cc639057..d896fba71c4 100644 --- a/Content.Server/Damage/Systems/GodmodeSystem.cs +++ b/Content.Server/Damage/Systems/GodmodeSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Atmos.Components; +using Content.Shared.Atmos.Components; using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; diff --git a/Content.Server/Database/BanMatcher.cs b/Content.Server/Database/BanMatcher.cs new file mode 100644 index 00000000000..f477ccd822a --- /dev/null +++ b/Content.Server/Database/BanMatcher.cs @@ -0,0 +1,115 @@ +using System.Collections.Immutable; +using System.Net; +using Content.Server.IP; +using Content.Shared.Database; +using Robust.Shared.Network; + +namespace Content.Server.Database; + +/// <summary> +/// Implements logic to match a <see cref="ServerBanDef"/> against a player query. +/// </summary> +/// <remarks> +/// <para> +/// This implementation is used by in-game ban matching code, and partially by the SQLite database layer. +/// Some logic is duplicated into both the SQLite and PostgreSQL database layers to provide more optimal SQL queries. +/// Both should be kept in sync, please! +/// </para> +/// </remarks> +public static class BanMatcher +{ + /// <summary> + /// Check whether a ban matches the specified player info. + /// </summary> + /// <remarks> + /// <para> + /// This function does not check whether the ban itself is expired or manually unbanned. + /// </para> + /// </remarks> + /// <param name="ban">The ban information.</param> + /// <param name="player">Information about the player to match against.</param> + /// <returns>True if the ban matches the provided player info.</returns> + public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) + { + var exemptFlags = player.ExemptFlags; + // Any flag to bypass BlacklistedRange bans. + if (exemptFlags != ServerBanExemptFlags.None) + exemptFlags |= ServerBanExemptFlags.BlacklistedRange; + + if ((ban.ExemptFlags & exemptFlags) != 0) + return false; + + if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP) + && player.Address != null + && ban.Address is not null + && player.Address.IsInSubnet(ban.Address.Value) + && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer)) + { + return true; + } + + if (player.UserId is { } id && ban.UserId == id.UserId) + { + return true; + } + + switch (ban.HWId?.Type) + { + case HwidType.Legacy: + if (player.HWId is { Length: > 0 } hwIdVar + && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) + { + return true; + } + break; + case HwidType.Modern: + if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) + { + foreach (var hwid in modernHwIdVar) + { + if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) + return true; + } + } + break; + } + + return false; + } + + /// <summary> + /// A simple struct containing player info used to match bans against. + /// </summary> + public struct PlayerInfo + { + /// <summary> + /// The user ID of the player. + /// </summary> + public NetUserId? UserId; + + /// <summary> + /// The IP address of the player. + /// </summary> + public IPAddress? Address; + + /// <summary> + /// The LEGACY hardware ID of the player. Corresponds with <see cref="NetUserData.HWId"/>. + /// </summary> + public ImmutableArray<byte>? HWId; + + /// <summary> + /// The modern hardware IDs of the player. Corresponds with <see cref="NetUserData.ModernHWIds"/>. + /// </summary> + public ImmutableArray<ImmutableArray<byte>>? ModernHWIds; + + /// <summary> + /// Exemption flags the player has been granted. + /// </summary> + public ServerBanExemptFlags ExemptFlags; + + /// <summary> + /// True if this player is new and is thus eligible for more bans. + /// </summary> + public bool IsNewPlayer; + } +} diff --git a/Content.Server/Database/DatabaseRecords.cs b/Content.Server/Database/DatabaseRecords.cs index c0d81147bb4..30fba3434b8 100644 --- a/Content.Server/Database/DatabaseRecords.cs +++ b/Content.Server/Database/DatabaseRecords.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.Net; using Content.Shared.Database; using Robust.Shared.Network; @@ -121,7 +120,7 @@ public sealed record PlayerRecord( string LastSeenUserName, DateTimeOffset LastSeenTime, IPAddress LastSeenAddress, - ImmutableArray<byte>? HWId); + ImmutableTypedHwid? HWId); public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server); diff --git a/Content.Server/Database/ServerBanDef.cs b/Content.Server/Database/ServerBanDef.cs index 9d67537bd22..a09f9e959c6 100644 --- a/Content.Server/Database/ServerBanDef.cs +++ b/Content.Server/Database/ServerBanDef.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.Net; using Content.Shared.CCVar; using Content.Shared.Database; @@ -13,7 +12,7 @@ public sealed class ServerBanDef public int? Id { get; } public NetUserId? UserId { get; } public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableArray<byte>? HWId { get; } + public ImmutableTypedHwid? HWId { get; } public DateTimeOffset BanTime { get; } public DateTimeOffset? ExpirationTime { get; } @@ -23,12 +22,12 @@ public sealed class ServerBanDef public NoteSeverity Severity { get; set; } public NetUserId? BanningAdmin { get; } public ServerUnbanDef? Unban { get; } + public ServerBanExemptFlags ExemptFlags { get; } - public ServerBanDef( - int? id, + public ServerBanDef(int? id, NetUserId? userId, (IPAddress, int)? address, - ImmutableArray<byte>? hwId, + TypedHwid? hwId, DateTimeOffset banTime, DateTimeOffset? expirationTime, int? roundId, @@ -36,7 +35,8 @@ public ServerBanDef( string reason, NoteSeverity severity, NetUserId? banningAdmin, - ServerUnbanDef? unban) + ServerUnbanDef? unban, + ServerBanExemptFlags exemptFlags = default) { if (userId == null && address == null && hwId == null) { @@ -62,6 +62,7 @@ public ServerBanDef( Severity = severity; BanningAdmin = banningAdmin; Unban = unban; + ExemptFlags = exemptFlags; } public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc) diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index ee6a8631c97..5f9ef727d61 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -27,6 +27,8 @@ public abstract class ServerDbBase { private readonly ISawmill _opsLog; + public event Action<DatabaseNotification>? OnNotificationReceived; + /// <param name="opsLog">Sawmill to trace log database operations to.</param> public ServerDbBase(ISawmill opsLog) { @@ -353,12 +355,14 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// </summary> /// <param name="address">The ip address of the user.</param> /// <param name="userId">The id of the user.</param> - /// <param name="hwId">The HWId of the user.</param> + /// <param name="hwId">The legacy HWId of the user.</param> + /// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns> public abstract Task<ServerBanDef?> GetServerBanAsync( IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId); + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds); /// <summary> /// Looks up an user's ban history. @@ -367,13 +371,15 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// </summary> /// <param name="address">The ip address of the user.</param> /// <param name="userId">The id of the user.</param> - /// <param name="hwId">The HWId of the user.</param> + /// <param name="hwId">The legacy HWId of the user.</param> + /// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="includeUnbanned">Include pardoned and expired bans.</param> /// <returns>The user's ban history.</returns> public abstract Task<List<ServerBanDef>> GetServerBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, bool includeUnbanned); public abstract Task AddServerBanAsync(ServerBanDef serverBan); @@ -394,13 +400,16 @@ public async Task EditServerBan(int id, string reason, NoteSeverity severity, Da await db.DbContext.SaveChangesAsync(); } - protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(DbGuard db, NetUserId? userId) + protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore( + DbGuard db, + NetUserId? userId, + CancellationToken cancel = default) { if (userId == null) return null; var exemption = await db.DbContext.BanExemption - .SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId); + .SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId, cancellationToken: cancel); return exemption?.Flags; } @@ -431,11 +440,11 @@ public async Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flag await db.DbContext.SaveChangesAsync(); } - public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId) + public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel) { - await using var db = await GetDb(); + await using var db = await GetDb(cancel); - var flags = await GetBanExemptionCore(db, userId); + var flags = await GetBanExemptionCore(db, userId, cancel); return flags ?? ServerBanExemptFlags.None; } @@ -461,11 +470,13 @@ public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId) /// <param name="address">The IP address of the user.</param> /// <param name="userId">The NetUserId of the user.</param> /// <param name="hwId">The Hardware Id of the user.</param> + /// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param> /// <returns>The user's role ban history.</returns> public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, bool includeUnbanned); public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan); @@ -548,7 +559,7 @@ public async Task UpdatePlayerRecord( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId) + ImmutableTypedHwid? hwId) { await using var db = await GetDb(); @@ -565,7 +576,7 @@ public async Task UpdatePlayerRecord( record.LastSeenTime = DateTime.UtcNow; record.LastSeenAddress = address; record.LastSeenUserName = userName; - record.LastSeenHWId = hwId.ToArray(); + record.LastSeenHWId = hwId; await db.DbContext.SaveChangesAsync(); } @@ -594,6 +605,11 @@ public async Task UpdatePlayerRecord( return record == null ? null : MakePlayerRecord(record); } + protected async Task<bool> PlayerRecordExists(DbGuard db, NetUserId userId) + { + return await db.DbContext.Player.AnyAsync(p => p.UserId == userId); + } + [return: NotNullIfNotNull(nameof(player))] protected PlayerRecord? MakePlayerRecord(Player? player) { @@ -606,7 +622,7 @@ public async Task UpdatePlayerRecord( player.LastSeenUserName, new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)), player.LastSeenAddress, - player.LastSeenHWId?.ToImmutableArray()); + player.LastSeenHWId); } #endregion @@ -615,11 +631,11 @@ public async Task UpdatePlayerRecord( /* * CONNECTION LOG */ - public abstract Task<int> AddConnectionLogAsync( - NetUserId userId, + public abstract Task<int> AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId, + ImmutableTypedHwid? hwId, + float trust, ConnectionDenyReason? denied, int serverId); @@ -1001,6 +1017,53 @@ public async Task RemoveFromWhitelistAsync(NetUserId player) await db.DbContext.SaveChangesAsync(); } + public async Task<DateTimeOffset?> GetLastReadRules(NetUserId player) + { + await using var db = await GetDb(); + + return NormalizeDatabaseTime(await db.DbContext.Player + .Where(dbPlayer => dbPlayer.UserId == player) + .Select(dbPlayer => dbPlayer.LastReadRules) + .SingleOrDefaultAsync()); + } + + public async Task SetLastReadRules(NetUserId player, DateTimeOffset date) + { + await using var db = await GetDb(); + + var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player).SingleOrDefaultAsync(); + if (dbPlayer == null) + { + return; + } + + dbPlayer.LastReadRules = date.UtcDateTime; + await db.DbContext.SaveChangesAsync(); + } + + public async Task<bool> GetBlacklistStatusAsync(NetUserId player) + { + await using var db = await GetDb(); + + return await db.DbContext.Blacklist.AnyAsync(w => w.UserId == player); + } + + public async Task AddToBlacklistAsync(NetUserId player) + { + await using var db = await GetDb(); + + db.DbContext.Blacklist.Add(new Blacklist() { UserId = player }); + await db.DbContext.SaveChangesAsync(); + } + + public async Task RemoveFromBlacklistAsync(NetUserId player) + { + await using var db = await GetDb(); + var entry = await db.DbContext.Blacklist.SingleAsync(w => w.UserId == player); + db.DbContext.Blacklist.Remove(entry); + await db.DbContext.SaveChangesAsync(); + } + #endregion #region Uploaded Resources Logs @@ -1217,7 +1280,7 @@ ban.Unban is null ban.LastEditedAt, ban.ExpirationTime, ban.Hidden, - new[] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, + new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, MakePlayerRecord(unbanningAdmin), ban.Unban?.UnbanTime); } @@ -1381,10 +1444,10 @@ public async Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player) protected async Task<List<AdminWatchlistRecord>> GetActiveWatchlistsImpl(DbGuard db, Guid player) { var entities = await (from watchlist in db.DbContext.AdminWatchlists - where watchlist.PlayerUserId == player && - !watchlist.Deleted && - (watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime) - select watchlist) + where watchlist.PlayerUserId == player && + !watchlist.Deleted && + (watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime) + select watchlist) .Include(note => note.Round) .ThenInclude(r => r!.Server) .Include(note => note.CreatedBy) @@ -1409,9 +1472,9 @@ public async Task<List<AdminMessageRecord>> GetMessages(Guid player) protected async Task<List<AdminMessageRecord>> GetMessagesImpl(DbGuard db, Guid player) { 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) + 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) @@ -1617,5 +1680,10 @@ public async Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> jo } #endregion + + protected void NotificationReceived(DatabaseNotification notification) => + OnNotificationReceived?.Invoke(notification); + + public virtual void Shutdown() { } } } diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index dee6248cc39..c614e231cca 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -69,12 +69,14 @@ Task<PlayerPreferences> InitPrefsAsync( /// </summary> /// <param name="address">The ip address of the user.</param> /// <param name="userId">The id of the user.</param> - /// <param name="hwId">The hardware ID of the user.</param> + /// <param name="hwId">The legacy HWID of the user.</param> + /// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns> Task<ServerBanDef?> GetServerBanAsync( IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId); + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds); /// <summary> /// Looks up an user's ban history. @@ -82,14 +84,16 @@ Task<PlayerPreferences> InitPrefsAsync( /// </summary> /// <param name="address">The ip address of the user.</param> /// <param name="userId">The id of the user.</param> - /// <param name="hwId">The HWId of the user.</param> + /// <param name="hwId">The legacy HWId of the user.</param> + /// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param> /// <returns>The user's ban history.</returns> Task<List<ServerBanDef>> GetServerBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, - bool includeUnbanned = true); + ImmutableArray<ImmutableArray<byte>>? modernHWIds, + bool includeUnbanned=true); Task AddServerBanAsync(ServerBanDef serverBan); Task AddServerUnbanAsync(ServerUnbanDef serverBan); @@ -116,7 +120,7 @@ public Task EditServerBan( /// Get current ban exemption flags for a user /// </summary> /// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns> - Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId); + Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default); #endregion @@ -137,12 +141,14 @@ public Task EditServerBan( /// <param name="address">The IP address of the user.</param> /// <param name="userId">The NetUserId of the user.</param> /// <param name="hwId">The Hardware Id of the user.</param> + /// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param> /// <returns>The user's role ban history.</returns> Task<List<ServerRoleBanDef>> GetServerRoleBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, bool includeUnbanned = true); Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan); @@ -180,7 +186,7 @@ Task UpdatePlayerRecordAsync( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId); + ImmutableTypedHwid? hwId); Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default); Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default); #endregion @@ -191,7 +197,8 @@ Task<int> AddConnectionLogAsync( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId, + ImmutableTypedHwid? hwId, + float trust, ConnectionDenyReason? denied, int serverId); @@ -244,6 +251,16 @@ Task<int> AddConnectionLogAsync( #endregion + #region Blacklist + + Task<bool> GetBlacklistStatusAsync(NetUserId player); + + Task AddToBlacklistAsync(NetUserId player); + + Task RemoveFromBlacklistAsync(NetUserId player); + + #endregion + #region Uploaded Resources Logs Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data); @@ -252,6 +269,13 @@ Task<int> AddConnectionLogAsync( #endregion + #region Rules + + Task<DateTimeOffset?> GetLastReadRules(NetUserId player); + Task SetLastReadRules(NetUserId player, DateTimeOffset time); + + #endregion + #region Admin Notes Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); @@ -297,6 +321,43 @@ Task<int> AddConnectionLogAsync( Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job); #endregion + + #region DB Notifications + + void SubscribeToNotifications(Action<DatabaseNotification> handler); + + /// <summary> + /// Inject a notification as if it was created by the database. This is intended for testing. + /// </summary> + /// <param name="notification">The notification to trigger</param> + void InjectTestNotification(DatabaseNotification notification); + + #endregion + } + + /// <summary> + /// Represents a notification sent between servers via the database layer. + /// </summary> + /// <remarks> + /// <para> + /// Database notifications are a simple system to broadcast messages to an entire server group + /// backed by the same database. For example, this is used to notify all servers of new ban records. + /// </para> + /// <para> + /// They are currently implemented by the PostgreSQL <c>NOTIFY</c> and <c>LISTEN</c> commands. + /// </para> + /// </remarks> + public struct DatabaseNotification + { + /// <summary> + /// The channel for the notification. This can be used to differentiate notifications for different purposes. + /// </summary> + public required string Channel { get; set; } + + /// <summary> + /// The actual contents of the notification. Optional. + /// </summary> + public string? Payload { get; set; } } public sealed class ServerDbManager : IServerDbManager @@ -326,6 +387,8 @@ public sealed class ServerDbManager : IServerDbManager // This is that connection, close it when we shut down. private SqliteConnection? _sqliteInMemoryConnection; + private readonly List<Action<DatabaseNotification>> _notificationHandlers = []; + public void Init() { _msLogProvider = new LoggingProvider(_logMgr); @@ -338,6 +401,7 @@ public void Init() var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower(); var opsLog = _logMgr.GetSawmill("db.op"); + var notifyLog = _logMgr.GetSawmill("db.notify"); switch (engine) { case "sqlite": @@ -345,17 +409,22 @@ public void Init() _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog); break; case "postgres": - var pgOptions = CreatePostgresOptions(); - _db = new ServerDbPostgres(pgOptions, _cfg, opsLog); + var (pgOptions, conString) = CreatePostgresOptions(); + _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog); break; default: throw new InvalidDataException($"Unknown database engine {engine}."); } + + _db.OnNotificationReceived += HandleDatabaseNotification; } public void Shutdown() { + _db.OnNotificationReceived -= HandleDatabaseNotification; + _sqliteInMemoryConnection?.Dispose(); + _db.Shutdown(); } public Task<PlayerPreferences> InitPrefsAsync( @@ -418,20 +487,22 @@ public Task AssignUserIdAsync(string name, NetUserId userId) public Task<ServerBanDef?> GetServerBanAsync( IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId) + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId)); + return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds)); } public Task<List<ServerBanDef>> GetServerBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, - bool includeUnbanned = true) + ImmutableArray<ImmutableArray<byte>>? modernHWIds, + bool includeUnbanned=true) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, includeUnbanned)); + return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); } public Task AddServerBanAsync(ServerBanDef serverBan) @@ -458,10 +529,10 @@ public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags) return RunDbCommand(() => _db.UpdateBanExemption(userId, flags)); } - public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId) + public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetBanExemption(userId)); + return RunDbCommand(() => _db.GetBanExemption(userId, cancel)); } #region Role Ban @@ -475,10 +546,11 @@ public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, bool includeUnbanned = true) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned)); + return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); } public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) @@ -520,7 +592,7 @@ public Task UpdatePlayerRecordAsync( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId) + ImmutableTypedHwid? hwId) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId)); @@ -542,12 +614,13 @@ public Task<int> AddConnectionLogAsync( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId, + ImmutableTypedHwid? hwId, + float trust, ConnectionDenyReason? denied, int serverId) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, denied, serverId)); + return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId)); } public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans) @@ -688,6 +761,24 @@ public Task RemoveFromWhitelistAsync(NetUserId player) return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player)); } + public Task<bool> GetBlacklistStatusAsync(NetUserId player) + { + DbReadOpsMetric.Inc(); + return RunDbCommand(() => _db.GetBlacklistStatusAsync(player)); + } + + public Task AddToBlacklistAsync(NetUserId player) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.AddToBlacklistAsync(player)); + } + + public Task RemoveFromBlacklistAsync(NetUserId player) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.RemoveFromBlacklistAsync(player)); + } + public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data) { DbWriteOpsMetric.Inc(); @@ -700,6 +791,18 @@ public Task PurgeUploadedResourceLogAsync(int days) return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days)); } + public Task<DateTimeOffset?> GetLastReadRules(NetUserId player) + { + DbReadOpsMetric.Inc(); + return RunDbCommand(() => _db.GetLastReadRules(player)); + } + + public Task SetLastReadRules(NetUserId player, DateTimeOffset time) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.SetLastReadRules(player, time)); + } + public Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); @@ -888,6 +991,30 @@ public Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job) return RunDbCommand(() => _db.RemoveJobWhitelist(player, job)); } + public void SubscribeToNotifications(Action<DatabaseNotification> handler) + { + lock (_notificationHandlers) + { + _notificationHandlers.Add(handler); + } + } + + public void InjectTestNotification(DatabaseNotification notification) + { + HandleDatabaseNotification(notification); + } + + private async void HandleDatabaseNotification(DatabaseNotification notification) + { + lock (_notificationHandlers) + { + foreach (var handler in _notificationHandlers) + { + handler(notification); + } + } + } + // Wrapper functions to run DB commands from the thread pool. // This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread. // For SQLite, this will also enable read parallelization (within limits). @@ -943,7 +1070,7 @@ private IAsyncEnumerable<T> RunDbCommand<T>(Func<IAsyncEnumerable<T>> command) return enumerable; } - private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions() + private (DbContextOptions<PostgresServerDbContext> options, string connectionString) CreatePostgresOptions() { var host = _cfg.GetCVar(CCVars.DatabasePgHost); var port = _cfg.GetCVar(CCVars.DatabasePgPort); @@ -965,7 +1092,7 @@ private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions() builder.UseNpgsql(connectionString); SetupLogging(builder); - return builder.Options; + return (builder.Options, connectionString); } private void SetupSqlite(out Func<DbContextOptions<SqliteServerDbContext>> contextFunc, out bool inMemory) diff --git a/Content.Server/Database/ServerDbPostgres.Notifications.cs b/Content.Server/Database/ServerDbPostgres.Notifications.cs new file mode 100644 index 00000000000..fe358923bf0 --- /dev/null +++ b/Content.Server/Database/ServerDbPostgres.Notifications.cs @@ -0,0 +1,121 @@ +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.Administration.Managers; +using Npgsql; + +namespace Content.Server.Database; + +/// Listens for ban_notification containing the player id and the banning server id using postgres listen/notify. +/// Players a ban_notification got received for get banned, except when the current server id and the one in the notification payload match. + +public sealed partial class ServerDbPostgres +{ + /// <summary> + /// The list of notify channels to subscribe to. + /// </summary> + private static readonly string[] NotificationChannels = + [ + BanManager.BanNotificationChannel, + ]; + + private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10); + + private readonly CancellationTokenSource _notificationTokenSource = new(); + + private NpgsqlConnection? _notificationConnection; + private TimeSpan _reconnectWaitTime = TimeSpan.Zero; + + /// <summary> + /// Sets up the database connection and the notification handler + /// </summary> + private void InitNotificationListener(string connectionString) + { + _notificationConnection = new NpgsqlConnection(connectionString); + _notificationConnection.Notification += OnNotification; + + var cancellationToken = _notificationTokenSource.Token; + Task.Run(() => NotificationListener(cancellationToken), cancellationToken); + } + + /// <summary> + /// Listens to the notification channel with basic error handling and reopens the connection if it got closed + /// </summary> + private async Task NotificationListener(CancellationToken cancellationToken) + { + if (_notificationConnection == null) + return; + + _notifyLog.Verbose("Starting notification listener"); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + if (_notificationConnection.State == ConnectionState.Broken) + { + _notifyLog.Debug("Notification listener entered broken state, closing..."); + await _notificationConnection.CloseAsync(); + } + + if (_notificationConnection.State == ConnectionState.Closed) + { + _notifyLog.Debug("Opening notification listener connection..."); + if (_reconnectWaitTime != TimeSpan.Zero) + { + _notifyLog.Verbose($"_reconnectWaitTime is {_reconnectWaitTime}"); + await Task.Delay(_reconnectWaitTime, cancellationToken); + } + + await _notificationConnection.OpenAsync(cancellationToken); + _reconnectWaitTime = TimeSpan.Zero; + _notifyLog.Verbose($"Notification connection opened..."); + } + + foreach (var channel in NotificationChannels) + { + _notifyLog.Verbose($"Listening on channel {channel}"); + await using var cmd = new NpgsqlCommand($"LISTEN {channel}", _notificationConnection); + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + while (!cancellationToken.IsCancellationRequested) + { + _notifyLog.Verbose("Waiting on notifications..."); + await _notificationConnection.WaitAsync(cancellationToken); + } + } + catch (OperationCanceledException) + { + // Abort loop on cancel. + _notifyLog.Verbose($"Shutting down notification listener due to cancellation"); + return; + } + catch (Exception e) + { + _reconnectWaitTime += ReconnectWaitIncrease; + _notifyLog.Error($"Error in notification listener: {e}"); + } + } + } + + private void OnNotification(object _, NpgsqlNotificationEventArgs notification) + { + _notifyLog.Verbose($"Received notification on channel {notification.Channel}"); + NotificationReceived(new DatabaseNotification + { + Channel = notification.Channel, + Payload = notification.Payload, + }); + } + + public override void Shutdown() + { + _notificationTokenSource.Cancel(); + if (_notificationConnection == null) + return; + + _notificationConnection.Notification -= OnNotification; + _notificationConnection.Dispose(); + } +} diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index fd4699fff4e..c0346708377 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -9,6 +9,7 @@ using Content.Server.Administration.Logs; using Content.Server.IP; using Content.Shared.CCVar; +using Content.Shared.Database; using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; using Robust.Shared.Network; @@ -16,23 +17,26 @@ namespace Content.Server.Database { - public sealed class ServerDbPostgres : ServerDbBase + public sealed partial class ServerDbPostgres : ServerDbBase { private readonly DbContextOptions<PostgresServerDbContext> _options; + private readonly ISawmill _notifyLog; private readonly SemaphoreSlim _prefsSemaphore; private readonly Task _dbReadyTask; private int _msLag; - public ServerDbPostgres( - DbContextOptions<PostgresServerDbContext> options, + public ServerDbPostgres(DbContextOptions<PostgresServerDbContext> options, + string connectionString, IConfigurationManager cfg, - ISawmill opsLog) + ISawmill opsLog, + ISawmill notifyLog) : base(opsLog) { var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency); _options = options; + _notifyLog = notifyLog; _prefsSemaphore = new SemaphoreSlim(concurrency, concurrency); _dbReadyTask = Task.Run(async () => @@ -49,6 +53,8 @@ public ServerDbPostgres( }); cfg.OnValueChanged(CCVars.DatabasePgFakeLag, v => _msLag = v, true); + + InitNotificationListener(connectionString); } #region Ban @@ -68,7 +74,8 @@ public ServerDbPostgres( public override async Task<ServerBanDef?> GetServerBanAsync( IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId) + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds) { if (address == null && userId == null && hwId == null) { @@ -78,7 +85,8 @@ public ServerDbPostgres( await using var db = await GetDbImpl(); var exempt = await GetBanExemptionCore(db, userId); - var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt) + var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value); + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer) .OrderByDescending(b => b.BanTime); var ban = await query.FirstOrDefaultAsync(); @@ -88,7 +96,9 @@ public ServerDbPostgres( public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId, bool includeUnbanned) + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, + bool includeUnbanned) { if (address == null && userId == null && hwId == null) { @@ -98,7 +108,8 @@ public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? add await using var db = await GetDbImpl(); var exempt = await GetBanExemptionCore(db, userId); - var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt); + var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId); + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer); var queryBans = await query.ToArrayAsync(); var bans = new List<ServerBanDef>(queryBans.Length); @@ -120,37 +131,27 @@ private static IQueryable<ServerBan> MakeBanLookupQuery( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, DbGuardImpl db, bool includeUnbanned, - ServerBanExemptFlags? exemptFlags) + ServerBanExemptFlags? exemptFlags, + bool newPlayer) { DebugTools.Assert(!(address == null && userId == null && hwId == null)); - IQueryable<ServerBan>? query = null; - - if (userId is { } uid) - { - var newQ = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(b => b.PlayerUserId == uid.UserId); - - query = query == null ? newQ : query.Union(newQ); - } + var query = MakeBanLookupQualityShared<ServerBan, ServerUnban>( + userId, + hwId, + modernHWIds, + db.PgDbContext.Ban); if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)) { var newQ = db.PgDbContext.Ban .Include(p => p.Unban) - .Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address)); - - query = query == null ? newQ : query.Union(newQ); - } - - if (hwId != null && hwId.Value.Length > 0) - { - var newQ = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray())); + .Where(b => b.Address != null + && EF.Functions.ContainsOrEqual(b.Address.Value, address) + && !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer)); query = query == null ? newQ : query.Union(newQ); } @@ -167,12 +168,58 @@ private static IQueryable<ServerBan> MakeBanLookupQuery( if (exemptFlags is { } exempt) { + if (exempt != ServerBanExemptFlags.None) + exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange + query = query.Where(b => (b.ExemptFlags & exempt) == 0); } return query.Distinct(); } + private static IQueryable<TBan>? MakeBanLookupQualityShared<TBan, TUnban>( + NetUserId? userId, + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, + DbSet<TBan> set) + where TBan : class, IBanCommon<TUnban> + where TUnban : class, IUnbanCommon + { + IQueryable<TBan>? query = null; + + if (userId is { } uid) + { + var newQ = set + .Include(p => p.Unban) + .Where(b => b.PlayerUserId == uid.UserId); + + query = query == null ? newQ : query.Union(newQ); + } + + if (hwId != null && hwId.Value.Length > 0) + { + var newQ = set + .Include(p => p.Unban) + .Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray())); + + query = query == null ? newQ : query.Union(newQ); + } + + if (modernHWIds != null) + { + foreach (var modernHwid in modernHWIds) + { + var newQ = set + .Include(p => p.Unban) + .Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray())); + + query = query == null ? newQ : query.Union(newQ); + } + } + + return query; + } + private static ServerBanDef? ConvertBan(ServerBan? ban) { if (ban == null) @@ -198,7 +245,7 @@ private static IQueryable<ServerBan> MakeBanLookupQuery( ban.Id, uid, ban.Address.ToTuple(), - ban.HWId == null ? null : ImmutableArray.Create(ban.HWId), + ban.HWId, ban.BanTime, ban.ExpirationTime, ban.RoundId, @@ -206,7 +253,8 @@ private static IQueryable<ServerBan> MakeBanLookupQuery( ban.Reason, ban.Severity, aUid, - unbanDef); + unbanDef, + ban.ExemptFlags); } private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) @@ -235,7 +283,7 @@ public override async Task AddServerBanAsync(ServerBanDef serverBan) db.PgDbContext.Ban.Add(new ServerBan { Address = serverBan.Address.ToNpgsqlInet(), - HWId = serverBan.HWId?.ToArray(), + HWId = serverBan.HWId, Reason = serverBan.Reason, Severity = serverBan.Severity, BanningAdmin = serverBan.BanningAdmin?.UserId, @@ -243,7 +291,8 @@ public override async Task AddServerBanAsync(ServerBanDef serverBan) ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, RoundId = serverBan.RoundId, PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId + PlayerUserId = serverBan.UserId?.UserId, + ExemptFlags = serverBan.ExemptFlags }); await db.PgDbContext.SaveChangesAsync(); @@ -282,6 +331,7 @@ public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, bool includeUnbanned) { if (address == null && userId == null && hwId == null) @@ -291,7 +341,7 @@ public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddr await using var db = await GetDbImpl(); - var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned) + var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned) .OrderByDescending(b => b.BanTime); return await QueryRoleBans(query); @@ -319,19 +369,15 @@ private static IQueryable<ServerRoleBan> MakeRoleBanLookupQuery( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, DbGuardImpl db, bool includeUnbanned) { - IQueryable<ServerRoleBan>? query = null; - - if (userId is { } uid) - { - var newQ = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(b => b.PlayerUserId == uid.UserId); - - query = query == null ? newQ : query.Union(newQ); - } + var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>( + userId, + hwId, + modernHWIds, + db.PgDbContext.RoleBan); if (address != null) { @@ -342,15 +388,6 @@ private static IQueryable<ServerRoleBan> MakeRoleBanLookupQuery( query = query == null ? newQ : query.Union(newQ); } - if (hwId != null && hwId.Value.Length > 0) - { - var newQ = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray())); - - query = query == null ? newQ : query.Union(newQ); - } - if (!includeUnbanned) { query = query?.Where(p => @@ -387,7 +424,7 @@ private static IQueryable<ServerRoleBan> MakeRoleBanLookupQuery( ban.Id, uid, ban.Address.ToTuple(), - ban.HWId == null ? null : ImmutableArray.Create(ban.HWId), + ban.HWId, ban.BanTime, ban.ExpirationTime, ban.RoundId, @@ -425,7 +462,7 @@ public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBan var ban = new ServerRoleBan { Address = serverRoleBan.Address.ToNpgsqlInet(), - HWId = serverRoleBan.HWId?.ToArray(), + HWId = serverRoleBan.HWId, Reason = serverRoleBan.Reason, Severity = serverRoleBan.Severity, BanningAdmin = serverRoleBan.BanningAdmin?.UserId, @@ -461,7 +498,8 @@ public override async Task<int> AddConnectionLogAsync( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId, + ImmutableTypedHwid? hwId, + float trust, ConnectionDenyReason? denied, int serverId) { @@ -473,9 +511,10 @@ public override async Task<int> AddConnectionLogAsync( Time = DateTime.UtcNow, UserId = userId.UserId, UserName = userName, - HWId = hwId.ToArray(), + HWId = hwId, Denied = denied, - ServerId = serverId + ServerId = serverId, + Trust = trust, }; db.PgDbContext.ConnectionLog.Add(connectionLog); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index ffec90bb43d..623b979f4b9 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -9,6 +9,7 @@ using Content.Server.IP; using Content.Server.Preferences.Managers; using Content.Shared.CCVar; +using Content.Shared.Database; using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; using Robust.Shared.Network; @@ -80,37 +81,55 @@ public ServerDbSqlite( public override async Task<ServerBanDef?> GetServerBanAsync( IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId) + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds) { await using var db = await GetDbImpl(); - var exempt = await GetBanExemptionCore(db, userId); - - // SQLite can't do the net masking stuff we need to match IP address ranges. - // So just pull down the whole list into memory. - var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt); - - return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt)) is { } foundBan - ? ConvertBan(foundBan) - : null; + return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault(); } - public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address, + public override async Task<List<ServerBanDef>> GetServerBansAsync( + IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId, bool includeUnbanned) + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, + bool includeUnbanned) { await using var db = await GetDbImpl(); + return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList(); + } + + private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync( + DbGuardImpl db, + IPAddress? address, + NetUserId? userId, + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, + bool includeUnbanned) + { var exempt = await GetBanExemptionCore(db, userId); + var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId); + // SQLite can't do the net masking stuff we need to match IP address ranges. // So just pull down the whole list into memory. var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt); + var playerInfo = new BanMatcher.PlayerInfo + { + Address = address, + UserId = userId, + ExemptFlags = exempt ?? default, + HWId = hwId, + ModernHWIds = modernHWIds, + IsNewPlayer = newPlayer, + }; + return queryBans - .Where(b => BanMatches(b, address, userId, hwId, exempt)) .Select(ConvertBan) - .ToList()!; + .Where(b => BanMatcher.BanMatches(b!, playerInfo))!; } private static async Task<List<ServerBan>> GetAllBans( @@ -133,26 +152,6 @@ private static async Task<List<ServerBan>> GetAllBans( return await query.ToListAsync(); } - private static bool BanMatches(ServerBan ban, - IPAddress? address, - NetUserId? userId, - ImmutableArray<byte>? hwId, - ServerBanExemptFlags? exemptFlags) - { - if (!exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP) - && address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value)) - { - return true; - } - - if (userId is { } id && ban.PlayerUserId == id.UserId) - { - return true; - } - - return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId); - } - public override async Task AddServerBanAsync(ServerBanDef serverBan) { await using var db = await GetDbImpl(); @@ -163,12 +162,13 @@ public override async Task AddServerBanAsync(ServerBanDef serverBan) Reason = serverBan.Reason, Severity = serverBan.Severity, BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId?.ToArray(), + HWId = serverBan.HWId, BanTime = serverBan.BanTime.UtcDateTime, ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, RoundId = serverBan.RoundId, PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId + PlayerUserId = serverBan.UserId?.UserId, + ExemptFlags = serverBan.ExemptFlags }); await db.SqliteDbContext.SaveChangesAsync(); @@ -206,6 +206,7 @@ public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds, bool includeUnbanned) { await using var db = await GetDbImpl(); @@ -215,7 +216,7 @@ public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync( var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned); return queryBans - .Where(b => RoleBanMatches(b, address, userId, hwId)) + .Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds)) .Select(ConvertRoleBan) .ToList()!; } @@ -238,7 +239,8 @@ private static bool RoleBanMatches( ServerRoleBan ban, IPAddress? address, NetUserId? userId, - ImmutableArray<byte>? hwId) + ImmutableArray<byte>? hwId, + ImmutableArray<ImmutableArray<byte>>? modernHWIds) { if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value)) { @@ -250,7 +252,27 @@ private static bool RoleBanMatches( return true; } - return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId); + switch (ban.HWId?.Type) + { + case HwidType.Legacy: + if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid)) + return true; + break; + + case HwidType.Modern: + if (modernHWIds != null) + { + foreach (var modernHWId in modernHWIds) + { + if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid)) + return true; + } + } + + break; + } + + return false; } public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan) @@ -263,7 +285,7 @@ public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBan Reason = serverBan.Reason, Severity = serverBan.Severity, BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId?.ToArray(), + HWId = serverBan.HWId, BanTime = serverBan.BanTime.UtcDateTime, ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, RoundId = serverBan.RoundId, @@ -317,7 +339,7 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba ban.Id, uid, ban.Address.ToTuple(), - ban.HWId == null ? null : ImmutableArray.Create(ban.HWId), + ban.HWId, // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), @@ -351,6 +373,7 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba } #endregion + [return: NotNullIfNotNull(nameof(ban))] private static ServerBanDef? ConvertBan(ServerBan? ban) { if (ban == null) @@ -376,7 +399,7 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba ban.Id, uid, ban.Address.ToTuple(), - ban.HWId == null ? null : ImmutableArray.Create(ban.HWId), + ban.HWId, // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), @@ -412,7 +435,8 @@ public override async Task<int> AddConnectionLogAsync( NetUserId userId, string userName, IPAddress address, - ImmutableArray<byte> hwId, + ImmutableTypedHwid? hwId, + float trust, ConnectionDenyReason? denied, int serverId) { @@ -424,9 +448,10 @@ public override async Task<int> AddConnectionLogAsync( Time = DateTime.UtcNow, UserId = userId.UserId, UserName = userName, - HWId = hwId.ToArray(), + HWId = hwId, Denied = denied, - ServerId = serverId + ServerId = serverId, + Trust = trust, }; db.SqliteDbContext.ConnectionLog.Add(connectionLog); diff --git a/Content.Server/Database/ServerRoleBanDef.cs b/Content.Server/Database/ServerRoleBanDef.cs index f615d5da4d1..dda3a822378 100644 --- a/Content.Server/Database/ServerRoleBanDef.cs +++ b/Content.Server/Database/ServerRoleBanDef.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.Net; using Content.Shared.Database; using Robust.Shared.Network; @@ -10,7 +9,7 @@ public sealed class ServerRoleBanDef public int? Id { get; } public NetUserId? UserId { get; } public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableArray<byte>? HWId { get; } + public ImmutableTypedHwid? HWId { get; } public DateTimeOffset BanTime { get; } public DateTimeOffset? ExpirationTime { get; } @@ -26,7 +25,7 @@ public ServerRoleBanDef( int? id, NetUserId? userId, (IPAddress, int)? address, - ImmutableArray<byte>? hwId, + ImmutableTypedHwid? hwId, DateTimeOffset banTime, DateTimeOffset? expirationTime, int? roundId, diff --git a/Content.Server/Database/UserDbDataManager.cs b/Content.Server/Database/UserDbDataManager.cs index fde610a6fdd..af246831cbf 100644 --- a/Content.Server/Database/UserDbDataManager.cs +++ b/Content.Server/Database/UserDbDataManager.cs @@ -1,6 +1,8 @@ using System.Threading; using System.Threading.Tasks; +using Content.Server.Players.PlayTimeTracking; using Content.Server.Preferences.Managers; +using Robust.Server.Player; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Utility; @@ -17,7 +19,9 @@ namespace Content.Server.Database; /// </remarks> public sealed class UserDbDataManager : IPostInjectInit { + [Dependency] private readonly IServerPreferencesManager _prefs = default!; [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!; private readonly Dictionary<NetUserId, UserData> _users = new(); private readonly List<OnLoadPlayer> _onLoadPlayer = []; @@ -65,18 +69,14 @@ private async Task Load(ICommonSession session, CancellationToken cancel) { var tasks = new List<Task>(); foreach (var action in _onLoadPlayer) - { tasks.Add(action(session, cancel)); - } await Task.WhenAll(tasks); - cancel.ThrowIfCancellationRequested(); + foreach (var action in _onFinishLoad) - { action(session); - } - + _prefs.SanitizeData(session); _sawmill.Verbose($"Load complete for user {session}"); } catch (OperationCanceledException) diff --git a/Content.Server/DeltaV/Cabinet/SpareIDSafeComponent.cs b/Content.Server/DeltaV/Cabinet/SpareIDSafeComponent.cs new file mode 100644 index 00000000000..40f97486ce3 --- /dev/null +++ b/Content.Server/DeltaV/Cabinet/SpareIDSafeComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Server.DeltaV.Cabinet; + +[RegisterComponent] +public sealed partial class SpareIDSafeComponent : Component; diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs new file mode 100644 index 00000000000..89a2bd21eb3 --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs @@ -0,0 +1,82 @@ +using Content.Shared.Audio; +using Content.Shared.CartridgeLoader; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.NanoChat; + +namespace Content.Server.CartridgeLoader.Cartridges; + +public sealed partial class LogProbeCartridgeSystem +{ + private void InitializeNanoChat() + { + SubscribeLocalEvent<NanoChatRecipientUpdatedEvent>(OnRecipientUpdated); + SubscribeLocalEvent<NanoChatMessageReceivedEvent>(OnMessageReceived); + } + + private void OnRecipientUpdated(ref NanoChatRecipientUpdatedEvent args) + { + var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>(); + while (query.MoveNext(out var uid, out var probe, out var cartridge)) + { + if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid) + continue; + + if (!TryComp<NanoChatCardComponent>(args.CardUid, out var card)) + continue; + + probe.ScannedNanoChatData = new NanoChatData( + new Dictionary<uint, NanoChatRecipient>(card.Recipients), + probe.ScannedNanoChatData.Value.Messages, + card.Number, + GetNetEntity(args.CardUid)); + + if (cartridge.LoaderUid != null) + UpdateUiState((uid, probe), cartridge.LoaderUid.Value); + } + } + + private void OnMessageReceived(ref NanoChatMessageReceivedEvent args) + { + var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>(); + while (query.MoveNext(out var uid, out var probe, out var cartridge)) + { + if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid) + continue; + + if (!TryComp<NanoChatCardComponent>(args.CardUid, out var card)) + continue; + + probe.ScannedNanoChatData = new NanoChatData( + probe.ScannedNanoChatData.Value.Recipients, + new Dictionary<uint, List<NanoChatMessage>>(card.Messages), + card.Number, + GetNetEntity(args.CardUid)); + + if (cartridge.LoaderUid != null) + UpdateUiState((uid, probe), cartridge.LoaderUid.Value); + } + } + + private void ScanNanoChatCard(Entity<LogProbeCartridgeComponent> ent, + CartridgeAfterInteractEvent args, + EntityUid target, + NanoChatCardComponent card) + { + _audioSystem.PlayEntity(ent.Comp.SoundScan, + args.InteractEvent.User, + target, + AudioHelpers.WithVariation(0.25f, _random)); + _popupSystem.PopupCursor(Loc.GetString("log-probe-scan-nanochat", ("card", target)), args.InteractEvent.User); + + ent.Comp.PulledAccessLogs.Clear(); + + ent.Comp.ScannedNanoChatData = new NanoChatData( + new Dictionary<uint, NanoChatRecipient>(card.Recipients), + new Dictionary<uint, List<NanoChatMessage>>(card.Messages), + card.Number, + GetNetEntity(target) + ); + + UpdateUiState(ent, args.Loader); + } +} diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs new file mode 100644 index 00000000000..2b95462a663 --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Radio; +using Robust.Shared.Prototypes; + +namespace Content.Server.DeltaV.CartridgeLoader.Cartridges; + +[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))] +public sealed partial class NanoChatCartridgeComponent : Component +{ + /// <summary> + /// Station entity to keep track of. + /// </summary> + [DataField] + public EntityUid? Station; + + /// <summary> + /// The NanoChat card to keep track of. + /// </summary> + [DataField] + public EntityUid? Card; + + /// <summary> + /// The <see cref="RadioChannelPrototype" /> required to send or receive messages. + /// </summary> + [DataField] + public ProtoId<RadioChannelPrototype> RadioChannel = "Common"; +} diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs new file mode 100644 index 00000000000..e9e01041f15 --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs @@ -0,0 +1,541 @@ +using System.Linq; +using Content.Server.Administration.Logs; +using Content.Server.CartridgeLoader; +using Content.Server.Power.Components; +using Content.Server.Radio; +using Content.Server.Radio.Components; +using Content.Server.Station.Systems; +using Content.Shared.Access.Components; +using Content.Shared.CartridgeLoader; +using Content.Shared.Database; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.NanoChat; +using Content.Shared.PDA; +using Content.Shared.Radio.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.DeltaV.CartridgeLoader.Cartridges; + +public sealed class NanoChatCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridge = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + + // Messages in notifications get cut off after this point + // no point in storing it on the comp + private const int NotificationMaxLength = 64; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady); + SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage); + + Subs.BuiEvents<PdaComponent>(PdaUiKey.Key, subs => + { + subs.Event<BoundUIClosedEvent>(OnPdaClosed); + }); + } + + // Reset current chat when PDA closes. + private void OnPdaClosed(EntityUid uid, PdaComponent component, BoundUIClosedEvent args) + { + var exists = GetCardEntity(uid, out var cardEntity); + + if (!exists) + return; + + _nanoChat.SetCurrentChat( + (cardEntity.Owner, (NanoChatCardComponent?) cardEntity.Comp), + null + ); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Update card references for any cartridges that need it + var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>(); + while (query.MoveNext(out var uid, out var nanoChat, out var cartridge)) + { + if (cartridge.LoaderUid == null) + continue; + + // Check if we need to update our card reference + if (!TryComp<PdaComponent>(cartridge.LoaderUid, out var pda)) + continue; + + var newCard = pda.ContainedId; + var currentCard = nanoChat.Card; + + // If the cards match, nothing to do + if (newCard == currentCard) + continue; + + // Update card reference + nanoChat.Card = newCard; + + // Update UI state since card reference changed + UpdateUI((uid, nanoChat), cartridge.LoaderUid.Value); + } + } + + /// <summary> + /// Handles incoming UI messages from the NanoChat cartridge. + /// </summary> + private void OnMessage(Entity<NanoChatCartridgeComponent> ent, ref CartridgeMessageEvent args) + { + if (args is not NanoChatUiMessageEvent msg) + return; + + if (!GetCardEntity(GetEntity(args.LoaderUid), out var card)) + return; + + switch (msg.Type) + { + case NanoChatUiMessageType.NewChat: + HandleNewChat(card, msg); + break; + case NanoChatUiMessageType.SelectChat: + HandleSelectChat(card, msg); + break; + case NanoChatUiMessageType.CloseChat: + HandleCloseChat(card); + break; + case NanoChatUiMessageType.ToggleMute: + HandleToggleMute(card); + break; + case NanoChatUiMessageType.DeleteChat: + HandleDeleteChat(card, msg); + break; + case NanoChatUiMessageType.SendMessage: + HandleSendMessage(ent, card, msg); + break; + } + + UpdateUI(ent, GetEntity(args.LoaderUid)); + } + + /// <summary> + /// Gets the ID card entity associated with a PDA. + /// </summary> + /// <param name="loaderUid">The PDA entity ID</param> + /// <param name="card">Output parameter containing the found card entity and component</param> + /// <returns>True if a valid NanoChat card was found</returns> + private bool GetCardEntity( + EntityUid loaderUid, + out Entity<NanoChatCardComponent> card) + { + card = default; + + // Get the PDA and check if it has an ID card + if (!TryComp<PdaComponent>(loaderUid, out var pda) || + pda.ContainedId == null || + !TryComp<NanoChatCardComponent>(pda.ContainedId, out var idCard)) + return false; + + card = (pda.ContainedId.Value, idCard); + return true; + } + + /// <summary> + /// Handles creation of a new chat conversation. + /// </summary> + private void HandleNewChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg) + { + if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number) + return; + + // Add new recipient + var recipient = new NanoChatRecipient(msg.RecipientNumber.Value, + msg.Content, + msg.RecipientJob); + + // Initialize or update recipient + _nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient); + + _adminLogger.Add(LogType.Action, + LogImpact.Low, + $"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({msg.Content})"); + + var recipientEv = new NanoChatRecipientUpdatedEvent(card); + RaiseLocalEvent(ref recipientEv); + UpdateUIForCard(card); + } + + /// <summary> + /// Handles selecting a chat conversation. + /// </summary> + private void HandleSelectChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg) + { + if (msg.RecipientNumber == null) + return; + + _nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber); + + // Clear unread flag when selecting chat + if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient) + { + _nanoChat.SetRecipient((card, card.Comp), + msg.RecipientNumber.Value, + recipient with { HasUnread = false }); + } + } + + /// <summary> + /// Handles closing the current chat conversation. + /// </summary> + private void HandleCloseChat(Entity<NanoChatCardComponent> card) + { + _nanoChat.SetCurrentChat((card, card.Comp), null); + } + + /// <summary> + /// Handles deletion of a chat conversation. + /// </summary> + private void HandleDeleteChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg) + { + if (msg.RecipientNumber == null || card.Comp.Number == null) + return; + + // Delete chat but keep the messages + var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true); + + if (!deleted) + return; + + _adminLogger.Add(LogType.Action, + LogImpact.Low, + $"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}"); + + UpdateUIForCard(card); + } + + /// <summary> + /// Handles toggling notification mute state. + /// </summary> + private void HandleToggleMute(Entity<NanoChatCardComponent> card) + { + _nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp))); + UpdateUIForCard(card); + } + + /// <summary> + /// Handles sending a new message in a chat conversation. + /// </summary> + private void HandleSendMessage(Entity<NanoChatCartridgeComponent> cartridge, + Entity<NanoChatCardComponent> card, + NanoChatUiMessageEvent msg) + { + if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null) + return; + + if (!EnsureRecipientExists(card, msg.RecipientNumber.Value)) + return; + + // Create and store message for sender + var message = new NanoChatMessage( + _timing.CurTime, + msg.Content, + (uint)card.Comp.Number + ); + + // Attempt delivery + var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value); + + // Update delivery status + message = message with { DeliveryFailed = deliveryFailed }; + + // Store message in sender's outbox under recipient's number + _nanoChat.AddMessage((card, card.Comp), msg.RecipientNumber.Value, message); + + // Log message attempt + var recipientsText = recipients.Count > 0 + ? string.Join(", ", recipients.Select(r => ToPrettyString(r))) + : $"#{msg.RecipientNumber:D4}"; + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {msg.Content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}"); + + var msgEv = new NanoChatMessageReceivedEvent(card); + RaiseLocalEvent(ref msgEv); + + if (deliveryFailed) + return; + + foreach (var recipient in recipients) + { + DeliverMessageToRecipient(card, recipient, message); + } + } + + /// <summary> + /// Ensures a recipient exists in the sender's contacts. + /// </summary> + /// <param name="card">The card to check contacts for</param> + /// <param name="recipientNumber">The recipient's number to check</param> + /// <returns>True if the recipient exists or was created successfully</returns> + private bool EnsureRecipientExists(Entity<NanoChatCardComponent> card, uint recipientNumber) + { + return _nanoChat.EnsureRecipientExists((card, card.Comp), recipientNumber, GetCardInfo(recipientNumber)); + } + + /// <summary> + /// Attempts to deliver a message to recipients. + /// </summary> + /// <param name="sender">The sending cartridge entity</param> + /// <param name="recipientNumber">The recipient's number</param> + /// <returns>Tuple containing delivery status and recipients if found.</returns> + private (bool failed, List<Entity<NanoChatCardComponent>> recipient) AttemptMessageDelivery( + Entity<NanoChatCartridgeComponent> sender, + uint recipientNumber) + { + // First verify we can send from this device + var channel = _prototype.Index(sender.Comp.RadioChannel); + var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender); + RaiseLocalEvent(ref sendAttemptEvent); + if (sendAttemptEvent.Cancelled) + return (true, new List<Entity<NanoChatCardComponent>>()); + + var foundRecipients = new List<Entity<NanoChatCardComponent>>(); + + // Find all cards with matching number + var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>(); + while (cardQuery.MoveNext(out var cardUid, out var card)) + { + if (card.Number != recipientNumber) + continue; + + foundRecipients.Add((cardUid, card)); + } + + if (foundRecipients.Count == 0) + return (true, foundRecipients); + + // Now check if any of these cards can receive + var deliverableRecipients = new List<Entity<NanoChatCardComponent>>(); + foreach (var recipient in foundRecipients) + { + // Find any cartridges that have this card + var cartridgeQuery = EntityQueryEnumerator<NanoChatCartridgeComponent, ActiveRadioComponent>(); + while (cartridgeQuery.MoveNext(out var receiverUid, out var receiverCart, out _)) + { + if (receiverCart.Card != recipient.Owner) + continue; + + // Check if devices are on same station/map + var recipientStation = _station.GetOwningStation(receiverUid); + var senderStation = _station.GetOwningStation(sender); + + // Both entities must be on a station + if (recipientStation == null || senderStation == null) + continue; + + // Must be on same map/station unless long range allowed + if (!channel.LongRange && recipientStation != senderStation) + continue; + + // Needs telecomms + if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value)) + continue; + + // Check if recipient can receive + var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid); + RaiseLocalEvent(ref receiveAttemptEv); + if (receiveAttemptEv.Cancelled) + continue; + + // Found valid cartridge that can receive + deliverableRecipients.Add(recipient); + break; // Only need one valid cartridge per card + } + } + + return (deliverableRecipients.Count == 0, deliverableRecipients); + } + + /// <summary> + /// Checks if there are any active telecomms servers on the given station + /// </summary> + private bool HasActiveServer(EntityUid station) + { + // I have no idea why this isn't public in the RadioSystem + var query = + EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>(); + + while (query.MoveNext(out var uid, out _, out _, out var power)) + { + if (_station.GetOwningStation(uid) == station && power.Powered) + return true; + } + + return false; + } + + /// <summary> + /// Delivers a message to the recipient and handles associated notifications. + /// </summary> + /// <param name="sender">The sender's card entity</param> + /// <param name="recipient">The recipient's card entity</param> + /// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param> + private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender, + Entity<NanoChatCardComponent> recipient, + NanoChatMessage message) + { + var senderNumber = sender.Comp.Number; + if (senderNumber == null) + return; + + // Always try to get and add sender info to recipient's contacts + if (!EnsureRecipientExists(recipient, senderNumber.Value)) + return; + + _nanoChat.AddMessage((recipient, recipient.Comp), senderNumber.Value, message with { DeliveryFailed = false }); + HandleUnreadNotification(recipient, message); + + var msgEv = new NanoChatMessageReceivedEvent(recipient); + RaiseLocalEvent(ref msgEv); + UpdateUIForCard(recipient); + } + + /// <summary> + /// Handles unread message notifications and updates unread status. + /// </summary> + private void HandleUnreadNotification(Entity<NanoChatCardComponent> recipient, NanoChatMessage message) + { + // Get sender name from contacts or fall back to number + var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp)); + var senderName = recipients.TryGetValue(message.SenderId, out var existingRecipient) + ? existingRecipient.Name + : $"#{message.SenderId:D4}"; + + var shouldUnread = true; + + if (!recipient.Comp.Recipients[message.SenderId].HasUnread && !recipient.Comp.NotificationsMuted) + { + var pdaQuery = EntityQueryEnumerator<PdaComponent>(); + while (pdaQuery.MoveNext(out var pdaUid, out var pdaComp)) + { + if (pdaComp.ContainedId != recipient) + continue; + + if (_ui.IsUiOpen((pdaUid, null), PdaUiKey.Key) && recipient.Comp.CurrentChat == message.SenderId) + { + shouldUnread = false; + break; + } + + _cartridge.SendNotification(pdaUid, + Loc.GetString("nano-chat-new-message-title", ("sender", senderName)), + Loc.GetString("nano-chat-new-message-body", ("message", TruncateMessage(message.Content)))); + break; + } + } + + // Update unread status + _nanoChat.SetRecipient( + (recipient, recipient.Comp), + message.SenderId, + existingRecipient with { HasUnread = shouldUnread }); + } + + /// <summary> + /// Updates the UI for any PDAs containing the specified card. + /// </summary> + private void UpdateUIForCard(EntityUid cardUid) + { + // Find any PDA containing this card and update its UI + var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>(); + while (query.MoveNext(out var uid, out var comp, out var cartridge)) + { + if (comp.Card != cardUid || cartridge.LoaderUid == null) + continue; + + UpdateUI((uid, comp), cartridge.LoaderUid.Value); + } + } + + /// <summary> + /// Gets the <see cref="NanoChatRecipient" /> for a given NanoChat number. + /// </summary> + private NanoChatRecipient? GetCardInfo(uint number) + { + // Find card with this number to get its info + var query = EntityQueryEnumerator<NanoChatCardComponent>(); + while (query.MoveNext(out var uid, out var card)) + { + if (card.Number != number) + continue; + + // Try to get job title from ID card if possible + string? jobTitle = null; + var name = "Unknown"; + if (TryComp<IdCardComponent>(uid, out var idCard)) + { + jobTitle = idCard.LocalizedJobTitle; + name = idCard.FullName ?? name; + } + + return new NanoChatRecipient(number, name, jobTitle); + } + + return null; + } + + /// <summary> + /// Truncates a message to the notification maximum length. + /// </summary> + private static string TruncateMessage(string message) + { + return message.Length <= NotificationMaxLength + ? message + : message[..(NotificationMaxLength - 4)] + " [...]"; + } + + private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args) + { + _cartridge.RegisterBackgroundProgram(args.Loader, ent); + UpdateUI(ent, args.Loader); + } + + private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader) + { + if (_station.GetOwningStation(loader) is { } station) + ent.Comp.Station = station; + + var recipients = new Dictionary<uint, NanoChatRecipient>(); + var messages = new Dictionary<uint, List<NanoChatMessage>>(); + uint? currentChat = null; + uint ownNumber = 0; + var maxRecipients = 50; + var notificationsMuted = false; + + if (ent.Comp.Card != null && TryComp<NanoChatCardComponent>(ent.Comp.Card, out var card)) + { + recipients = card.Recipients; + messages = card.Messages; + currentChat = card.CurrentChat; + ownNumber = card.Number ?? 0; + maxRecipients = card.MaxRecipients; + notificationsMuted = card.NotificationsMuted; + } + + var state = new NanoChatUiState(recipients, + messages, + currentChat, + ownNumber, + maxRecipients, + notificationsMuted); + _cartridge.UpdateCartridgeUiState(loader, state); + } +} diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs new file mode 100644 index 00000000000..7ab11f64d4a --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/StockTradingCartridgeComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.DeltaV.CartridgeLoader.Cartridges; + +[RegisterComponent, Access(typeof(StockTradingCartridgeSystem))] +public sealed partial class StockTradingCartridgeComponent : Component +{ + /// <summary> + /// Station entity to keep track of + /// </summary> + [DataField] + public EntityUid? Station; +} diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs new file mode 100644 index 00000000000..cd68c5adb43 --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/StockTradingCartridgeSystem.cs @@ -0,0 +1,101 @@ +using System.Linq; +using Content.Server.Cargo.Components; +using Content.Server.DeltaV.Cargo.Components; +using Content.Server.DeltaV.Cargo.Systems; +using Content.Server.Station.Systems; +using Content.Server.CartridgeLoader; +using Content.Shared.Cargo.Components; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; + +namespace Content.Server.DeltaV.CartridgeLoader.Cartridges; + +public sealed class StockTradingCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!; + [Dependency] private readonly StationSystem _station = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<StockTradingCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady); + SubscribeLocalEvent<StockMarketUpdatedEvent>(OnStockMarketUpdated); + SubscribeLocalEvent<StationStockMarketComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<StockTradingCartridgeComponent, BankBalanceUpdatedEvent>(OnBalanceUpdated); + } + + private void OnBalanceUpdated(Entity<StockTradingCartridgeComponent> ent, ref BankBalanceUpdatedEvent args) + { + UpdateAllCartridges(args.Station); + } + + private void OnUiReady(Entity<StockTradingCartridgeComponent> ent, ref CartridgeUiReadyEvent args) + { + UpdateUI(ent, args.Loader); + } + + private void OnStockMarketUpdated(StockMarketUpdatedEvent args) + { + UpdateAllCartridges(args.Station); + } + + private void OnMapInit(Entity<StationStockMarketComponent> ent, ref MapInitEvent args) + { + // Initialize price history for each company + for (var i = 0; i < ent.Comp.Companies.Count; i++) + { + var company = ent.Comp.Companies[i]; + + // Create initial price history using base price + company.PriceHistory = new List<float>(); + for (var j = 0; j < 5; j++) + { + company.PriceHistory.Add(company.BasePrice); + } + + ent.Comp.Companies[i] = company; + } + + if (_station.GetOwningStation(ent.Owner) is { } station) + UpdateAllCartridges(station); + } + + private void UpdateAllCartridges(EntityUid station) + { + var query = EntityQueryEnumerator<StockTradingCartridgeComponent, CartridgeComponent>(); + while (query.MoveNext(out var uid, out var comp, out var cartridge)) + { + if (cartridge.LoaderUid is not { } loader || comp.Station != station) + continue; + UpdateUI((uid, comp), loader); + } + } + + private void UpdateUI(Entity<StockTradingCartridgeComponent> ent, EntityUid loader) + { + if (_station.GetOwningStation(loader) is { } station) + ent.Comp.Station = station; + + if (!TryComp<StationStockMarketComponent>(ent.Comp.Station, out var stockMarket) || + !TryComp<StationBankAccountComponent>(ent.Comp.Station, out var bankAccount)) + return; + + // Convert company data to UI state format + var entries = stockMarket.Companies.Select(company => new StockCompanyStruct( + displayName: company.LocalizedDisplayName, + currentPrice: company.CurrentPrice, + basePrice: company.BasePrice, + priceHistory: company.PriceHistory)) + .ToList(); + + // Send the UI state with balance and owned stocks + var state = new StockTradingUiState( + entries: entries, + ownedStocks: stockMarket.StockOwnership, + balance: bankAccount.Balance + ); + + _cartridgeLoader.UpdateCartridgeUiState(loader, state); + } +} diff --git a/Content.Server/DeltaV/NPC/Roboisseur/RoboisseurComponent.cs b/Content.Server/DeltaV/NPC/Roboisseur/RoboisseurComponent.cs index 0e0d2e4e683..55eda0d68bc 100644 --- a/Content.Server/DeltaV/NPC/Roboisseur/RoboisseurComponent.cs +++ b/Content.Server/DeltaV/NPC/Roboisseur/RoboisseurComponent.cs @@ -159,7 +159,7 @@ public sealed partial class RoboisseurComponent : Component "FoodBurgerHuman", "FoodNoodlesBoiled", "FoodMothOatStew", - "FoodMeatLizardtailKebab", + "FoodKebabSkewer", "FoodSoupTomato", "FoodDonkpocketBerryWarm", "FoodBreadButteredToast", @@ -167,11 +167,9 @@ public sealed partial class RoboisseurComponent : Component "LeavesTobaccoDried", "FoodSoupEyeball", "FoodMothKachumbariSalad", - "FoodMeatHumanKebab", "FoodMeatRatdoubleKebab", "FoodBurgerCorgi", "FoodBreadPlain", - "FoodMeatKebab", "FoodBreadBun", "FoodBurgerCat", "FoodSoupTomatoBlood", @@ -191,7 +189,6 @@ public sealed partial class RoboisseurComponent : Component // "FoodBreadMoldySlice", "FoodRiceBoiled", "FoodMothEyeballSoup", - "FoodMeatRatKebab", "FoodBreadCreamcheese", "FoodSoupOnion", "FoodBurgerAppendix", diff --git a/Content.Server/DeltaV/NanoChat/NanoChatSystem.cs b/Content.Server/DeltaV/NanoChat/NanoChatSystem.cs new file mode 100644 index 00000000000..fb0ca32aa66 --- /dev/null +++ b/Content.Server/DeltaV/NanoChat/NanoChatSystem.cs @@ -0,0 +1,130 @@ +using System.Linq; +using Content.Server.Access.Systems; +using Content.Server.Administration.Logs; +using Content.Server.Kitchen.Components; +using Content.Server.NameIdentifier; +using Content.Shared.Database; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.DeltaV.NanoChat; +using Content.Shared.NameIdentifier; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.DeltaV.NanoChat; + +/// <summary> +/// Handles NanoChat features that are specific to the server but not related to the cartridge itself. +/// </summary> +public sealed class NanoChatSystem : SharedNanoChatSystem +{ + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly NameIdentifierSystem _name = default!; + + private readonly ProtoId<NameIdentifierGroupPrototype> _nameIdentifierGroup = "NanoChat"; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit); + SubscribeLocalEvent<NanoChatCardComponent, BeingMicrowavedEvent>(OnMicrowaved, after: [typeof(IdCardSystem)]); + } + + private void OnMicrowaved(Entity<NanoChatCardComponent> ent, ref BeingMicrowavedEvent args) + { + // Skip if the entity was deleted (e.g., by ID card system burning it) + if (Deleted(ent)) + return; + + if (!TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken) + return; + + var randomPick = _random.NextFloat(); + + // Super lucky - erase all messages (10% chance) + if (randomPick <= 0.10f) + { + ent.Comp.Messages.Clear(); + // TODO: these shouldn't be shown at the same time as the popups from IdCardSystem + // _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-erased", ("card", ent)), + // ent, + // PopupType.Medium); + + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(args.Microwave)} erased all messages on {ToPrettyString(ent)}"); + } + else + { + // Scramble random messages for random recipients + ScrambleMessages(ent); + // _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-scrambled", ("card", ent)), + // ent, + // PopupType.Medium); + + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(args.Microwave)} scrambled messages on {ToPrettyString(ent)}"); + } + + Dirty(ent); + } + + private void ScrambleMessages(NanoChatCardComponent component) + { + foreach (var (recipientNumber, messages) in component.Messages) + { + for (var i = 0; i < messages.Count; i++) + { + // 50% chance to scramble each message + if (!_random.Prob(0.5f)) + continue; + + var message = messages[i]; + message.Content = ScrambleText(message.Content); + messages[i] = message; + } + + // 25% chance to reassign the conversation to a random recipient + if (_random.Prob(0.25f) && component.Recipients.Count > 0) + { + var newRecipient = _random.Pick(component.Recipients.Keys.ToList()); + if (newRecipient == recipientNumber) + continue; + + if (!component.Messages.ContainsKey(newRecipient)) + component.Messages[newRecipient] = new List<NanoChatMessage>(); + + component.Messages[newRecipient].AddRange(messages); + component.Messages[recipientNumber].Clear(); + } + } + } + + private string ScrambleText(string text) + { + var chars = text.ToCharArray(); + var n = chars.Length; + + // Fisher-Yates shuffle of characters + while (n > 1) + { + n--; + var k = _random.Next(n + 1); + (chars[k], chars[n]) = (chars[n], chars[k]); + } + + return new string(chars); + } + + private void OnCardInit(Entity<NanoChatCardComponent> ent, ref MapInitEvent args) + { + if (ent.Comp.Number != null) + return; + + // Assign a random number + _name.GenerateUniqueName(ent, _nameIdentifierGroup, out var number); + ent.Comp.Number = (uint)number; + Dirty(ent); + } +} diff --git a/Content.Server/DeltaV/Nutrition/Events.cs b/Content.Server/DeltaV/Nutrition/Events.cs index fb1b2b870bd..403484f4cb6 100644 --- a/Content.Server/DeltaV/Nutrition/Events.cs +++ b/Content.Server/DeltaV/Nutrition/Events.cs @@ -4,7 +4,11 @@ namespace Content.Server.Nutrition; /// Raised on a food being sliced. /// Used by deep frier to apply friedness to slices (e.g. deep fried pizza) /// </summary> -public sealed class SliceFoodEvent : EntityEventArgs +/// <remarks> +/// Not to be confused with upstream SliceFoodEvent which doesn't pass the slice entities, and is only raised once. +/// </remarks> +[ByRefEvent] +public sealed class FoodSlicedEvent : EntityEventArgs { /// <summary> /// Who did the slicing? @@ -25,10 +29,10 @@ public sealed class SliceFoodEvent : EntityEventArgs /// <summary> public EntityUid Slice; - public SliceFoodEvent(EntityUid user, EntityUid food, EntityUid slice) + public FoodSlicedEvent(EntityUid user, EntityUid food, EntityUid slice) { User = user; Food = food; Slice = slice; } -} +} \ No newline at end of file diff --git a/Content.Server/DeltaV/RoundEnd/RoundEndSystem.Pacified.cs b/Content.Server/DeltaV/RoundEnd/RoundEndSystem.Pacified.cs index aa268ccf74a..b068c56a2bd 100644 --- a/Content.Server/DeltaV/RoundEnd/RoundEndSystem.Pacified.cs +++ b/Content.Server/DeltaV/RoundEnd/RoundEndSystem.Pacified.cs @@ -10,6 +10,7 @@ using Content.Shared.Explosion.Components; using Content.Shared.FixedPoint; using Content.Shared.Flash.Components; +using Content.Shared.Store.Components; using Robust.Server.Player; using Robust.Shared.Configuration; @@ -64,7 +65,8 @@ private void OnRoundEnded(RoundEndTextAppendEvent ev) var uplinkQuery = EntityQueryEnumerator<StoreComponent>(); while (uplinkQuery.MoveNext(out var uid, out var store)) { - store.Listings.Clear(); + store.FullListingsCatalog.Clear(); + store.LastAvailableListings.Clear(); } } } diff --git a/Content.Server/DeltaV/Station/Components/CaptainStateComponent.cs b/Content.Server/DeltaV/Station/Components/CaptainStateComponent.cs new file mode 100644 index 00000000000..96d7c441071 --- /dev/null +++ b/Content.Server/DeltaV/Station/Components/CaptainStateComponent.cs @@ -0,0 +1,64 @@ +using Content.Server.DeltaV.Station.Systems; +using Content.Server.Station.Systems; +using Content.Shared.Access; +using Robust.Shared.Prototypes; + +namespace Content.Server.DeltaV.Station.Components; + +/// <summary> +/// Denotes a station has no captain and holds data for automatic ACO systems +/// </summary> +[RegisterComponent, Access(typeof(CaptainStateSystem), typeof(StationSystem))] +public sealed partial class CaptainStateComponent : Component +{ + /// <summary> + /// Denotes wether the entity has a captain or not + /// </summary> + /// <remarks> + /// Assume no captain unless specified + /// </remarks> + [DataField] + public bool HasCaptain; + + /// <summary> + /// The localization ID used for announcing the cancellation of ACO requests + /// </summary> + [DataField] + public LocId RevokeACOMessage = "captain-arrived-revoke-aco-announcement"; + + /// <summary> + /// The localization ID for requesting an ACO vote when AA will be unlocked + /// </summary> + [DataField] + public LocId ACORequestWithAAMessage = "no-captain-request-aco-vote-with-aa-announcement"; + + /// <summary> + /// The localization ID for requesting an ACO vote when AA will not be unlocked + /// </summary> + [DataField] + public LocId ACORequestNoAAMessage = "no-captain-request-aco-vote-announcement"; + + /// <summary> + /// Set after ACO has been requested to avoid duplicate calls + /// </summary> + [DataField] + public bool IsACORequestActive; + + /// <summary> + /// Used to denote that AA has been brought into the round either from captain or safe. + /// </summary> + [DataField] + public bool IsAAInPlay; + + /// <summary> + /// The localization ID for announcing that AA has been unlocked for ACO + /// </summary> + [DataField] + public LocId AAUnlockedMessage = "no-captain-aa-unlocked-announcement"; + + /// <summary> + /// The access level to grant to spare ID cabinets + /// </summary> + [DataField] + public ProtoId<AccessLevelPrototype> ACOAccess = "Command"; +} diff --git a/Content.Server/DeltaV/Station/Events/PlayerJobEvents.cs b/Content.Server/DeltaV/Station/Events/PlayerJobEvents.cs new file mode 100644 index 00000000000..d34e5994202 --- /dev/null +++ b/Content.Server/DeltaV/Station/Events/PlayerJobEvents.cs @@ -0,0 +1,21 @@ +using Content.Shared.Roles; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Server.DeltaV.Station.Events; + +/// <summary> +/// Raised on a station when a after a players jobs are removed from the PlayerJobs +/// </summary> +/// <param name="NetUserId">Player whos jobs were removed</param> +/// <param name="PlayerJobs">Entry in PlayerJobs removed a list of JobPrototypes</param> +[ByRefEvent] +public record struct PlayerJobsRemovedEvent(NetUserId NetUserId, List<ProtoId<JobPrototype>> PlayerJobs); + +/// <summary> +/// Raised on a staion when a job is added to a player +/// </summary> +/// <param name="NetUserId">Player who recived a job</param> +/// <param name="JobPrototypeId">Id of the jobPrototype added</param> +[ByRefEvent] +public record struct PlayerJobAddedEvent(NetUserId NetUserId, string JobPrototypeId); diff --git a/Content.Server/DeltaV/Station/Systems/CaptainStateSystem.cs b/Content.Server/DeltaV/Station/Systems/CaptainStateSystem.cs new file mode 100644 index 00000000000..8a286869cf0 --- /dev/null +++ b/Content.Server/DeltaV/Station/Systems/CaptainStateSystem.cs @@ -0,0 +1,168 @@ +using Content.Server.Chat.Systems; +using Content.Server.DeltaV.Cabinet; +using Content.Server.DeltaV.Station.Components; +using Content.Server.DeltaV.Station.Events; +using Content.Server.GameTicking; +using Content.Server.Station.Components; +using Content.Shared.Access.Components; +using Content.Shared.Access; +using Content.Shared.DeltaV.CCVars; +using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Content.Server.DeltaV.Station.Systems; + +public sealed class CaptainStateSystem : EntitySystem +{ + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly GameTicker _ticker = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private bool _aaEnabled; + private bool _acoOnDeparture; + private TimeSpan _aaDelay; + private TimeSpan _acoDelay; + + public override void Initialize() + { + SubscribeLocalEvent<CaptainStateComponent, PlayerJobAddedEvent>(OnPlayerJobAdded); + SubscribeLocalEvent<CaptainStateComponent, PlayerJobsRemovedEvent>(OnPlayerJobsRemoved); + Subs.CVar(_cfg, DCCVars.AutoUnlockAllAccessEnabled, a => _aaEnabled = a, true); + Subs.CVar(_cfg, DCCVars.RequestAcoOnCaptainDeparture, a => _acoOnDeparture = a, true); + Subs.CVar(_cfg, DCCVars.AutoUnlockAllAccessDelay, a => _aaDelay = a, true); + Subs.CVar(_cfg, DCCVars.RequestAcoDelay, a => _acoDelay = a, true); + base.Initialize(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var currentTime = _ticker.RoundDuration(); // Caching to reduce redundant calls + if (currentTime < _acoDelay) // Avoid timing issues. No need to run before _acoDelay is reached anyways. + return; + var query = EntityQueryEnumerator<CaptainStateComponent>(); + while (query.MoveNext(out var station, out var captainState)) + { + if (captainState.HasCaptain) + HandleHasCaptain(station, captainState); + else + HandleNoCaptain(station, captainState, currentTime); + } + } + + private void OnPlayerJobAdded(Entity<CaptainStateComponent> ent, ref PlayerJobAddedEvent args) + { + if (args.JobPrototypeId == "Captain") + { + ent.Comp.IsAAInPlay = true; + ent.Comp.HasCaptain = true; + } + } + + private void OnPlayerJobsRemoved(Entity<CaptainStateComponent> ent, ref PlayerJobsRemovedEvent args) + { + if (!TryComp<StationJobsComponent>(ent, out var stationJobs)) + return; + if (!args.PlayerJobs.Contains("Captain")) // If the player that left was a captain we need to check if there are any captains left + return; + if (stationJobs.PlayerJobs.Any(playerJobs => playerJobs.Value.Contains("Captain"))) // We check the PlayerJobs if there are any cpatins left + return; + ent.Comp.HasCaptain = false; + if (_acoOnDeparture) + { + _chat.DispatchStationAnnouncement( + ent, + Loc.GetString(ent.Comp.ACORequestNoAAMessage, ("minutes", _aaDelay.TotalMinutes)), + colorOverride: Color.Gold); + + ent.Comp.IsACORequestActive = true; + } + } + + /// <summary> + /// Handles cases for when there is a captain + /// </summary> + /// <param name="station"></param> + /// <param name="captainState"></param> + private void HandleHasCaptain(Entity<CaptainStateComponent?> station, CaptainStateComponent captainState) + { + // If ACO vote has been called we need to cancel and alert to return to normal chain of command + if (!captainState.IsACORequestActive) + return; + + _chat.DispatchStationAnnouncement(station, + Loc.GetString(captainState.RevokeACOMessage), + colorOverride: Color.Gold); + + captainState.IsACORequestActive = false; + } + + /// <summary> + /// Handles cases for when there is no captain + /// </summary> + /// <param name="station"></param> + /// <param name="captainState"></param> + private void HandleNoCaptain(Entity<CaptainStateComponent?> station, CaptainStateComponent captainState, TimeSpan currentTime) + { + if (CheckACORequest(captainState, currentTime)) + { + var message = + CheckUnlockAA(captainState, null) + ? captainState.ACORequestWithAAMessage + : captainState.ACORequestNoAAMessage; + + _chat.DispatchStationAnnouncement( + station, + Loc.GetString(message, ("minutes", _aaDelay.TotalMinutes)), + colorOverride: Color.Gold); + + captainState.IsACORequestActive = true; + } + if (CheckUnlockAA(captainState, currentTime)) + { + captainState.IsAAInPlay = true; + _chat.DispatchStationAnnouncement(station, Loc.GetString(captainState.AAUnlockedMessage), colorOverride: Color.Red); + + // Extend access of spare id lockers to command so they can access emergency AA + var query = EntityQueryEnumerator<SpareIDSafeComponent>(); + while (query.MoveNext(out var spareIDSafe, out _)) + { + if (!TryComp<AccessReaderComponent>(spareIDSafe, out var accessReader)) + continue; + var accesses = accessReader.AccessLists; + if (accesses.Count <= 0) // Avoid restricting access for readers with no accesses + continue; + // Awful and disgusting but the accessReader has no proper api for adding acceses to readers without awful type casting. See AccessOverriderSystem + accesses.Add(new HashSet<ProtoId<AccessLevelPrototype>> { captainState.ACOAccess }); + Dirty(spareIDSafe, accessReader); + RaiseLocalEvent(spareIDSafe, new AccessReaderConfigurationChangedEvent()); + } + } + } + + /// <summary> + /// Checks the conditions for if an ACO should be requested + /// </summary> + /// <param name="captainState"></param> + /// <returns>True if conditions are met for an ACO to be requested, False otherwise</returns> + private bool CheckACORequest(CaptainStateComponent captainState, TimeSpan currentTime) + { + return !captainState.IsACORequestActive && currentTime > _acoDelay; + } + + /// <summary> + /// Checks the conditions for if AA should be unlocked + /// If time is null its condition is ignored + /// </summary> + /// <param name="captainState"></param> + /// <returns>True if conditions are met for AA to be unlocked, False otherwise</returns> + private bool CheckUnlockAA(CaptainStateComponent captainState, TimeSpan? currentTime) + { + if (captainState.IsAAInPlay || !_aaEnabled) + return false; + return currentTime == null || currentTime > _acoDelay + _aaDelay; + } +} diff --git a/Content.Server/DeltaV/VendingMachines/ShopVendorSystem.cs b/Content.Server/DeltaV/VendingMachines/ShopVendorSystem.cs new file mode 100644 index 00000000000..d3e99bfcf83 --- /dev/null +++ b/Content.Server/DeltaV/VendingMachines/ShopVendorSystem.cs @@ -0,0 +1,47 @@ +using Content.Server.Advertise; +using Content.Server.Advertise.Components; +using Content.Shared.DeltaV.VendingMachines; + +namespace Content.Server.DeltaV.VendingMachines; + +public sealed class ShopVendorSystem : SharedShopVendorSystem +{ + [Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator<ShopVendorComponent, TransformComponent>(); + var now = Timing.CurTime; + while (query.MoveNext(out var uid, out var comp, out var xform)) + { + var ent = (uid, comp); + var dirty = false; + if (comp.Ejecting is {} ejecting && now > comp.NextEject) + { + Spawn(ejecting, xform.Coordinates); + comp.Ejecting = null; + dirty = true; + } + + if (comp.Denying && now > comp.NextDeny) + { + comp.Denying = false; + dirty = true; + } + + if (dirty) + { + Dirty(uid, comp); + UpdateVisuals(ent); + } + } + } + + protected override void AfterPurchase(Entity<ShopVendorComponent> ent) + { + if (TryComp<SpeakOnUIClosedComponent>(ent, out var speak)) + _speakOnUIClosed.TrySetFlag((ent.Owner, speak)); + } +} diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs index 093f05fceee..4f3efde6612 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs @@ -4,6 +4,7 @@ using Content.Shared.Destructible.Thresholds; using Content.Shared.Prototypes; using Content.Shared.Stacks; +using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -30,7 +31,8 @@ public sealed partial class SpawnEntitiesBehavior : IThresholdBehavior public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) { - var position = system.EntityManager.GetComponent<TransformComponent>(owner).MapPosition; + var tSys = system.EntityManager.System<TransformSystem>(); + var position = tSys.GetMapCoordinates(owner); var getRandomVector = () => new Vector2(system.Random.NextFloat(-Offset, Offset), system.Random.NextFloat(-Offset, Offset)); @@ -48,7 +50,8 @@ public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause ? minMax.Min : system.Random.Next(minMax.Min, minMax.Max + 1); - if (count == 0) continue; + if (count == 0) + continue; if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.ComponentFactory)) { diff --git a/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs b/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs index 6e7bd255c5d..f47a5df8ac4 100644 --- a/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs @@ -1,6 +1,7 @@ using Content.Server.DeviceNetwork.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; +using Content.Shared.Power.EntitySystems; namespace Content.Server.DeviceNetwork.Systems; diff --git a/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs b/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs index cdc083feacd..d189afc0a02 100644 --- a/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Medical.CrewMonitoring; using Content.Server.Power.Components; using Content.Server.Station.Systems; +using Content.Shared.Power; namespace Content.Server.DeviceNetwork.Systems; diff --git a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs index 84a835f523c..1b9ab97be6b 100644 --- a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs +++ b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs @@ -24,7 +24,7 @@ using Content.Shared.Item; using Content.Shared.Movement.Events; using Content.Shared.Popups; -using Content.Shared.Throwing; +using Content.Shared.Power; using Content.Shared.Verbs; using Robust.Server.Audio; using Robust.Server.GameObjects; @@ -236,7 +236,7 @@ private void OnUiButtonPressed(EntityUid uid, SharedDisposalUnitComponent compon _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit flush button on {ToPrettyString(uid)}, it's now {(component.Engaged ? "on" : "off")}"); break; case SharedDisposalUnitComponent.UiButton.Power: - _power.TryTogglePower(uid, user: args.Actor); + _power.TogglePower(uid, user: args.Actor); break; default: throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}"); @@ -486,8 +486,7 @@ public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, var doAfterArgs = new DoAfterArgs(EntityManager, userId.Value, delay, new DisposalDoAfterEvent(), unitId, target: toInsertId, used: unitId) { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = false }; diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs index fd5d3a9ceba..e9f1db13ffb 100644 --- a/Content.Server/Doors/Systems/AirlockSystem.cs +++ b/Content.Server/Doors/Systems/AirlockSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; using Content.Shared.Interaction; +using Content.Shared.Power; using Content.Shared.Wires; using Robust.Shared.Player; diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index 5968e445c19..292f8ec8e97 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -1,9 +1,9 @@ using Content.Server.Access; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Power.Components; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; +using Content.Shared.Power; using Robust.Shared.Physics.Components; namespace Content.Server.Doors.Systems; diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index 3d4c8a4ec59..6c1e711b6f1 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -11,8 +11,10 @@ using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; +using Content.Shared.Power; using Content.Shared.Popups; using Content.Shared.Prying.Components; +using Robust.Server.GameObjects; using Robust.Shared.Map.Components; namespace Content.Server.Doors.Systems diff --git a/Content.Server/Dragon/Components/DragonComponent.cs b/Content.Server/Dragon/Components/DragonComponent.cs index f403979a007..80461e156af 100644 --- a/Content.Server/Dragon/Components/DragonComponent.cs +++ b/Content.Server/Dragon/Components/DragonComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.NPC.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -57,5 +58,12 @@ public sealed partial class DragonComponent : Component { Params = AudioParams.Default.WithVolume(3f), }; + + /// <summary> + /// NPC faction to re-add after being zombified. + /// Prevents zombie dragon from being attacked by its own carp. + /// </summary> + [DataField] + public ProtoId<NpcFactionPrototype> Faction = "Dragon"; } } diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index 79e5c0a2a9c..6a7d2d840cb 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -10,7 +10,9 @@ using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Movement.Systems; +using Content.Shared.NPC.Systems; using Content.Shared.Zombies; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -22,6 +24,7 @@ public sealed partial class DragonSystem : EntitySystem [Dependency] private readonly CarpRiftsConditionSystem _carpRifts = default!; [Dependency] private readonly ITileDefinitionManager _tileDef = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly NpcFactionSystem _faction = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; @@ -54,6 +57,7 @@ public override void Initialize() SubscribeLocalEvent<DragonComponent, RefreshMovementSpeedModifiersEvent>(OnDragonMove); SubscribeLocalEvent<DragonComponent, MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<DragonComponent, GenericAntagCreatedEvent>(OnCreated); + SubscribeLocalEvent<DragonComponent, EntityZombifiedEvent>(OnZombified); } public override void Update(float frameTime) @@ -162,7 +166,7 @@ private void OnSpawnRift(EntityUid uid, DragonComponent component, DragonSpawnRi return; } - var carpUid = Spawn(component.RiftPrototype, xform.MapPosition); + var carpUid = Spawn(component.RiftPrototype, _transform.GetMapCoordinates(uid, xform: xform)); component.Rifts.Add(carpUid); Comp<DragonRiftComponent>(carpUid).Dragon = uid; } @@ -201,6 +205,12 @@ private void OnCreated(EntityUid uid, DragonComponent comp, ref GenericAntagCrea }, mind); } + private void OnZombified(Entity<DragonComponent> ent, ref EntityZombifiedEvent args) + { + // prevent carp attacking zombie dragon + _faction.AddFaction(ent.Owner, ent.Comp.Faction); + } + private void Roar(EntityUid uid, DragonComponent comp) { if (comp.SoundRoar != null) diff --git a/Content.Server/Drowsiness/DrowsinessSystem.cs b/Content.Server/Drowsiness/DrowsinessSystem.cs new file mode 100644 index 00000000000..2511bc790e9 --- /dev/null +++ b/Content.Server/Drowsiness/DrowsinessSystem.cs @@ -0,0 +1,50 @@ +using Content.Shared.Bed.Sleep; +using Content.Shared.Drowsiness; +using Content.Shared.StatusEffect; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Drowsiness; + +public sealed class DrowsinessSystem : SharedDrowsinessSystem +{ + [ValidatePrototypeId<StatusEffectPrototype>] + private const string SleepKey = "ForcedSleep"; // Same one used by N2O and other sleep chems. + + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + + /// <inheritdoc/> + public override void Initialize() + { + SubscribeLocalEvent<DrowsinessComponent, ComponentStartup>(OnInit); + } + + private void OnInit(EntityUid uid, DrowsinessComponent component, ComponentStartup args) + { + component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y)); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator<DrowsinessComponent>(); + while (query.MoveNext(out var uid, out var component)) + { + if (_timing.CurTime < component.NextIncidentTime) + continue; + + // Set the new time. + component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y)); + + // sleep duration + var duration = TimeSpan.FromSeconds(_random.NextFloat(component.DurationOfIncident.X, component.DurationOfIncident.Y)); + + // Make sure the sleep time doesn't cut into the time to next incident. + component.NextIncidentTime += duration; + + _statusEffects.TryAddStatusEffect<ForcedSleepingComponent>(uid, SleepKey, duration, false); + } + } +} diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index 4db815a3944..ee1965a91b7 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -168,7 +168,7 @@ private void OnElectrifiedAttacked(EntityUid uid, ElectrifiedComponent electrifi if (!electrified.OnAttacked) return; - if (!_meleeWeapon.GetDamage(args.Used, args.User).Any()) + if (_meleeWeapon.GetDamage(args.Used, args.User).Empty) return; TryDoElectrifiedAct(uid, args.User, 1, electrified); @@ -185,7 +185,7 @@ private void OnLightAttacked(EntityUid uid, PoweredLightComponent component, Att if (!component.CurrentLit || args.Used != args.User) return; - if (!_meleeWeapon.GetDamage(args.Used, args.User).Any()) + if (_meleeWeapon.GetDamage(args.Used, args.User).Empty) return; DoCommonElectrocution(args.User, uid, component.UnarmedHitShock, component.UnarmedHitStun, false); diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs index 3a1d2d28196..d125c9b6fee 100644 --- a/Content.Server/Emp/EmpSystem.cs +++ b/Content.Server/Emp/EmpSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Radio; using Content.Shared.Emp; using Content.Shared.Examine; +using Robust.Server.GameObjects; using Robust.Shared.Map; namespace Content.Server.Emp; @@ -11,6 +12,7 @@ namespace Content.Server.Emp; public sealed class EmpSystem : SharedEmpSystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly TransformSystem _transform = default!; public const string EmpPulseEffectPrototype = "EffectEmpPulse"; @@ -117,7 +119,7 @@ private void OnExamine(EntityUid uid, EmpDisabledComponent component, ExaminedEv private void HandleEmpTrigger(EntityUid uid, EmpOnTriggerComponent comp, TriggerEvent args) { - EmpPulse(Transform(uid).MapPosition, comp.Range, comp.EnergyConsumption, comp.DisableDuration); + EmpPulse(_transform.GetMapCoordinates(uid), comp.Range, comp.EnergyConsumption, comp.DisableDuration); args.Handled = true; } diff --git a/Content.Server/Engineering/EntitySystems/DisassembleOnAltVerbSystem.cs b/Content.Server/Engineering/EntitySystems/DisassembleOnAltVerbSystem.cs index 08bf68c4d29..61b6f3d93d2 100644 --- a/Content.Server/Engineering/EntitySystems/DisassembleOnAltVerbSystem.cs +++ b/Content.Server/Engineering/EntitySystems/DisassembleOnAltVerbSystem.cs @@ -45,7 +45,7 @@ public async void AttemptDisassemble(EntityUid uid, EntityUid user, EntityUid ta { var doAfterArgs = new DoAfterArgs(EntityManager, user, component.DoAfterTime, new AwaitedDoAfterEvent(), null) { - BreakOnUserMove = true, + BreakOnMove = true, }; var result = await doAfterSystem.WaitDoAfter(doAfterArgs); diff --git a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs index a0bbbdf350a..281bbc47211 100644 --- a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs +++ b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs @@ -46,7 +46,7 @@ bool IsTileClear() { var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.DoAfterTime, new AwaitedDoAfterEvent(), null) { - BreakOnUserMove = true, + BreakOnMove = true, }; var result = await _doAfterSystem.WaitDoAfter(doAfterArgs); diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs index 8dd3d56a1ee..a5ebb0fd9e1 100644 --- a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs +++ b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs @@ -121,8 +121,7 @@ public void TryFree(EntityUid target, EntityUid user, EntityUid ensnare, Ensnari var doAfterEventArgs = new DoAfterArgs(EntityManager, user, freeTime, new EnsnareableDoAfterEvent(), target, target: target, used: ensnare) { - BreakOnUserMove = breakOnMove, - BreakOnTargetMove = breakOnMove, + BreakOnMove = breakOnMove, BreakOnDamage = false, NeedHand = true, BlockDuplicate = true, diff --git a/Content.Server/EntityEffects/EffectConditions/BodyTemperature.cs b/Content.Server/EntityEffects/EffectConditions/BodyTemperature.cs new file mode 100644 index 00000000000..9f68bec9182 --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/BodyTemperature.cs @@ -0,0 +1,35 @@ +using Content.Server.Temperature.Components; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.EffectConditions; + +/// <summary> +/// Requires the target entity to be above or below a certain temperature. +/// Used for things like cryoxadone and pyroxadone. +/// </summary> +public sealed partial class Temperature : EntityEffectCondition +{ + [DataField] + public float Min = 0; + + [DataField] + public float Max = float.PositiveInfinity; + public override bool Condition(EntityEffectBaseArgs args) + { + if (args.EntityManager.TryGetComponent(args.TargetEntity, out TemperatureComponent? temp)) + { + if (temp.CurrentTemperature > Min && temp.CurrentTemperature < Max) + return true; + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-body-temperature", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); + } +} diff --git a/Content.Server/Chemistry/ReagentEffectConditions/HasTagCondition.cs b/Content.Server/EntityEffects/EffectConditions/HasTagCondition.cs similarity index 68% rename from Content.Server/Chemistry/ReagentEffectConditions/HasTagCondition.cs rename to Content.Server/EntityEffects/EffectConditions/HasTagCondition.cs index 52d4d00eb3f..b0428b41bfc 100644 --- a/Content.Server/Chemistry/ReagentEffectConditions/HasTagCondition.cs +++ b/Content.Server/EntityEffects/EffectConditions/HasTagCondition.cs @@ -1,13 +1,13 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Tag; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Chemistry.ReagentEffectConditions; +namespace Content.Server.EntityEffects.EffectConditions; [UsedImplicitly] -public sealed partial class HasTag : ReagentEffectCondition +public sealed partial class HasTag : EntityEffectCondition { [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>))] public string Tag = default!; @@ -15,10 +15,10 @@ public sealed partial class HasTag : ReagentEffectCondition [DataField] public bool Invert = false; - public override bool Condition(ReagentEffectArgs args) + public override bool Condition(EntityEffectBaseArgs args) { - if (args.EntityManager.TryGetComponent<TagComponent>(args.SolutionEntity, out var tag)) - return EntitySystem.Get<TagSystem>().HasTag(tag, Tag) ^ Invert; + if (args.EntityManager.TryGetComponent<TagComponent>(args.TargetEntity, out var tag)) + return args.EntityManager.System<TagSystem>().HasTag(tag, Tag) ^ Invert; return false; } diff --git a/Content.Server/EntityEffects/EffectConditions/JobCondition.cs b/Content.Server/EntityEffects/EffectConditions/JobCondition.cs new file mode 100644 index 00000000000..c62cb60a22d --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/JobCondition.cs @@ -0,0 +1,39 @@ +using System.Linq; +using Content.Shared.EntityEffects; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; +using Content.Shared.Mind.Components; +using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; + +namespace Content.Server.EntityEffects.EffectConditions; + +public sealed partial class JobCondition : EntityEffectCondition +{ + [DataField(required: true)] public List<ProtoId<JobPrototype>> Job; + + public override bool Condition(EntityEffectBaseArgs args) + { + args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer); + if (mindContainer != null && mindContainer.Mind != null) + { + var prototypeManager = IoCManager.Resolve<IPrototypeManager>(); + if (args.EntityManager.TryGetComponent<JobComponent>(mindContainer?.Mind, out var comp) && prototypeManager.TryIndex(comp.Prototype, out var prototype)) + { + foreach (var jobId in Job) + if (prototype.ID == jobId) + return true; + } + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList(); + return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames))); + } +} + + diff --git a/Content.Server/EntityEffects/EffectConditions/MobStateCondition.cs b/Content.Server/EntityEffects/EffectConditions/MobStateCondition.cs new file mode 100644 index 00000000000..d676b29bf9e --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/MobStateCondition.cs @@ -0,0 +1,29 @@ +using Content.Shared.EntityEffects; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.EffectConditions; + +public sealed partial class MobStateCondition : EntityEffectCondition +{ + [DataField] + public MobState Mobstate = MobState.Alive; + + public override bool Condition(EntityEffectBaseArgs args) + { + if (args.EntityManager.TryGetComponent(args.TargetEntity, out MobStateComponent? mobState)) + { + if (mobState.CurrentState == Mobstate) + return true; + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate)); + } +} + diff --git a/Content.Server/EntityEffects/EffectConditions/OrganType.cs b/Content.Server/EntityEffects/EffectConditions/OrganType.cs new file mode 100644 index 00000000000..fc52608fab2 --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/OrganType.cs @@ -0,0 +1,53 @@ +using Content.Server.Body.Components; +using Content.Shared.Body.Prototypes; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.EntityEffects.EffectConditions; + +/// <summary> +/// Requires that the metabolizing organ is or is not tagged with a certain MetabolizerType +/// </summary> +public sealed partial class OrganType : EntityEffectCondition +{ + [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<MetabolizerTypePrototype>))] + public string Type = default!; + + /// <summary> + /// Does this condition pass when the organ has the type, or when it doesn't have the type? + /// </summary> + [DataField] + public bool ShouldHave = true; + + public override bool Condition(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.OrganEntity == null) + return false; + + return Condition(reagentArgs.OrganEntity.Value, reagentArgs.EntityManager); + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } + + public bool Condition(Entity<MetabolizerComponent?> metabolizer, IEntityManager entMan) + { + metabolizer.Comp ??= entMan.GetComponentOrNull<MetabolizerComponent>(metabolizer.Owner); + if (metabolizer.Comp != null + && metabolizer.Comp.MetabolizerTypes != null + && metabolizer.Comp.MetabolizerTypes.Contains(Type)) + return ShouldHave; + return !ShouldHave; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-organ-type", + ("name", prototype.Index<MetabolizerTypePrototype>(Type).LocalizedName), + ("shouldhave", ShouldHave)); + } +} diff --git a/Content.Server/EntityEffects/EffectConditions/ReagentThreshold.cs b/Content.Server/EntityEffects/EffectConditions/ReagentThreshold.cs new file mode 100644 index 00000000000..6cbd7b4ea57 --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/ReagentThreshold.cs @@ -0,0 +1,57 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.EffectConditions; + +/// <summary> +/// Used for implementing reagent effects that require a certain amount of reagent before it should be applied. +/// For instance, overdoses. +/// +/// This can also trigger on -other- reagents, not just the one metabolizing. By default, it uses the +/// one being metabolized. +/// </summary> +public sealed partial class ReagentThreshold : EntityEffectCondition +{ + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + // TODO use ReagentId + [DataField] + public string? Reagent; + + public override bool Condition(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + var reagent = Reagent ?? reagentArgs.Reagent?.ID; + if (reagent == null) + return true; // No condition to apply. + + var quant = FixedPoint2.Zero; + if (reagentArgs.Source != null) + quant = reagentArgs.Source.GetTotalPrototypeQuantity(reagent); + + return quant >= Min && quant <= Max; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + ReagentPrototype? reagentProto = null; + if (Reagent is not null) + prototype.TryIndex(Reagent, out reagentProto); + + return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold", + ("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")), + ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), + ("min", Min.Float())); + } +} diff --git a/Content.Server/EntityEffects/EffectConditions/SolutionTemperature.cs b/Content.Server/EntityEffects/EffectConditions/SolutionTemperature.cs new file mode 100644 index 00000000000..b964435db52 --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/SolutionTemperature.cs @@ -0,0 +1,40 @@ +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.EffectConditions; + +/// <summary> +/// Requires the solution to be above or below a certain temperature. +/// Used for things like explosives. +/// </summary> +public sealed partial class SolutionTemperature : EntityEffectCondition +{ + [DataField] + public float Min = 0.0f; + + [DataField] + public float Max = float.PositiveInfinity; + public override bool Condition(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.Source == null) + return false; + if (reagentArgs.Source.Temperature < Min) + return false; + if (reagentArgs.Source.Temperature > Max) + return false; + return true; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-solution-temperature", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); + } +} diff --git a/Content.Server/EntityEffects/EffectConditions/TotalDamage.cs b/Content.Server/EntityEffects/EffectConditions/TotalDamage.cs new file mode 100644 index 00000000000..2d039a1ac8f --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/TotalDamage.cs @@ -0,0 +1,34 @@ +using Content.Shared.EntityEffects; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.EffectConditions; + +public sealed partial class TotalDamage : EntityEffectCondition +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + public override bool Condition(EntityEffectBaseArgs args) + { + if (args.EntityManager.TryGetComponent(args.TargetEntity, out DamageableComponent? damage)) + { + var total = damage.TotalDamage; + if (total > Min && total < Max) + return true; + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-total-damage", + ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), + ("min", Min.Float())); + } +} diff --git a/Content.Server/EntityEffects/EffectConditions/TotalHunger.cs b/Content.Server/EntityEffects/EffectConditions/TotalHunger.cs new file mode 100644 index 00000000000..84ad4c22403 --- /dev/null +++ b/Content.Server/EntityEffects/EffectConditions/TotalHunger.cs @@ -0,0 +1,34 @@ +using Content.Shared.EntityEffects; +using Content.Shared.Nutrition.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.EffectConditions; + +public sealed partial class Hunger : EntityEffectCondition +{ + [DataField] + public float Max = float.PositiveInfinity; + + [DataField] + public float Min = 0; + + public override bool Condition(EntityEffectBaseArgs args) + { + if (args.EntityManager.TryGetComponent(args.TargetEntity, out HungerComponent? hunger)) + { + var total = hunger.CurrentHunger; + if (total > Min && total < Max) + return true; + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-total-hunger", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/ActivateArtifact.cs b/Content.Server/EntityEffects/Effects/ActivateArtifact.cs similarity index 56% rename from Content.Server/Chemistry/ReagentEffects/ActivateArtifact.cs rename to Content.Server/EntityEffects/Effects/ActivateArtifact.cs index 41af90f60a1..3e973884995 100644 --- a/Content.Server/Chemistry/ReagentEffects/ActivateArtifact.cs +++ b/Content.Server/EntityEffects/Effects/ActivateArtifact.cs @@ -1,15 +1,16 @@ -using Content.Server.Xenoarchaeology.XenoArtifacts; +using Content.Server.Xenoarchaeology.XenoArtifacts; using Content.Shared.Chemistry.Reagent; using Robust.Shared.Prototypes; +using Content.Shared.EntityEffects; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class ActivateArtifact : ReagentEffect +public sealed partial class ActivateArtifact : EntityEffect { - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var artifact = args.EntityManager.EntitySysManager.GetEntitySystem<ArtifactSystem>(); - artifact.TryActivateArtifact(args.SolutionEntity); + artifact.TryActivateArtifact(args.TargetEntity); } protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => diff --git a/Content.Server/EntityEffects/Effects/AddToSolutionReaction.cs b/Content.Server/EntityEffects/Effects/AddToSolutionReaction.cs new file mode 100644 index 00000000000..10fb667ca48 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/AddToSolutionReaction.cs @@ -0,0 +1,38 @@ +using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects +{ + [UsedImplicitly] + public sealed partial class AddToSolutionReaction : EntityEffect + { + [DataField("solution")] + private string _solution = "reagents"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) { + if (reagentArgs.Reagent == null) + return; + + // TODO see if this is correct + var solutionContainerSystem = reagentArgs.EntityManager.System<SolutionContainerSystem>(); + if (!solutionContainerSystem.TryGetSolution(reagentArgs.TargetEntity, _solution, out var solutionContainer)) + return; + + if (solutionContainerSystem.TryAddReagent(solutionContainer.Value, reagentArgs.Reagent.ID, reagentArgs.Quantity, out var accepted)) + reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, accepted); + + return; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("reagent-effect-guidebook-add-to-solution-reaction", ("chance", Probability)); + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs b/Content.Server/EntityEffects/Effects/AdjustAlert.cs similarity index 76% rename from Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs rename to Content.Server/EntityEffects/Effects/AdjustAlert.cs index 40858176bd1..3bf57b309d1 100644 --- a/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs +++ b/Content.Server/EntityEffects/Effects/AdjustAlert.cs @@ -1,11 +1,11 @@ -using Content.Shared.Alert; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Alert; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class AdjustAlert : ReagentEffect +public sealed partial class AdjustAlert : EntityEffect { /// <summary> /// The specific Alert that will be adjusted @@ -34,15 +34,15 @@ public sealed partial class AdjustAlert : ReagentEffect //JUSTIFICATION: This just changes some visuals, doesn't need to be documented. protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var alertSys = args.EntityManager.EntitySysManager.GetEntitySystem<AlertsSystem>(); - if (!args.EntityManager.HasComponent<AlertsComponent>(args.SolutionEntity)) + if (!args.EntityManager.HasComponent<AlertsComponent>(args.TargetEntity)) return; if (Clear && Time <= 0) { - alertSys.ClearAlert(args.SolutionEntity, AlertType); + alertSys.ClearAlert(args.TargetEntity, AlertType); } else { @@ -52,7 +52,7 @@ public override void Effect(ReagentEffectArgs args) if ((ShowCooldown || Clear) && Time > 0) cooldown = (timing.CurTime, timing.CurTime + TimeSpan.FromSeconds(Time)); - alertSys.ShowAlert(args.SolutionEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown); + alertSys.ShowAlert(args.TargetEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown); } } diff --git a/Content.Server/Chemistry/ReagentEffects/AdjustReagent.cs b/Content.Server/EntityEffects/Effects/AdjustReagent.cs similarity index 58% rename from Content.Server/Chemistry/ReagentEffects/AdjustReagent.cs rename to Content.Server/EntityEffects/Effects/AdjustReagent.cs index 16d69edd9aa..71117d6ec53 100644 --- a/Content.Server/Chemistry/ReagentEffects/AdjustReagent.cs +++ b/Content.Server/EntityEffects/Effects/AdjustReagent.cs @@ -1,14 +1,15 @@ using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { [UsedImplicitly] - public sealed partial class AdjustReagent : ReagentEffect + public sealed partial class AdjustReagent : EntityEffect { /// <summary> /// The reagent ID to remove. Only one of this and <see cref="Group"/> should be active. @@ -27,36 +28,43 @@ public sealed partial class AdjustReagent : ReagentEffect [DataField(required: true)] public FixedPoint2 Amount = default!; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.Source == null) - return; + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.Source == null) + return; - var amount = Amount; - amount *= args.Scale; + var amount = Amount; + amount *= reagentArgs.Scale; - if (Reagent != null) - { - if (amount < 0 && args.Source.ContainsPrototype(Reagent)) - args.Source.RemoveReagent(Reagent, -amount); - if (amount > 0) - args.Source.AddReagent(Reagent, amount); - } - else if (Group != null) - { - var prototypeMan = IoCManager.Resolve<IPrototypeManager>(); - foreach (var quant in args.Source.Contents.ToArray()) + if (Reagent != null) { - var proto = prototypeMan.Index<ReagentPrototype>(quant.Reagent.Prototype); - if (proto.Metabolisms != null && proto.Metabolisms.ContainsKey(Group)) + if (amount < 0 && reagentArgs.Source.ContainsPrototype(Reagent)) + reagentArgs.Source.RemoveReagent(Reagent, -amount); + if (amount > 0) + reagentArgs.Source.AddReagent(Reagent, amount); + } + else if (Group != null) + { + var prototypeMan = IoCManager.Resolve<IPrototypeManager>(); + foreach (var quant in reagentArgs.Source.Contents.ToArray()) { - if (amount < 0) - args.Source.RemoveReagent(quant.Reagent, amount); - if (amount > 0) - args.Source.AddReagent(quant.Reagent, amount); + var proto = prototypeMan.Index<ReagentPrototype>(quant.Reagent.Prototype); + if (proto.Metabolisms != null && proto.Metabolisms.ContainsKey(Group)) + { + if (amount < 0) + reagentArgs.Source.RemoveReagent(quant.Reagent, amount); + if (amount > 0) + reagentArgs.Source.AddReagent(quant.Reagent, amount); + } } } + return; } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); } protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) diff --git a/Content.Server/Chemistry/ReagentEffects/AdjustTemperature.cs b/Content.Server/EntityEffects/Effects/AdjustTemperature.cs similarity index 56% rename from Content.Server/Chemistry/ReagentEffects/AdjustTemperature.cs rename to Content.Server/EntityEffects/Effects/AdjustTemperature.cs index 9b975720678..3c5c77c309d 100644 --- a/Content.Server/Chemistry/ReagentEffects/AdjustTemperature.cs +++ b/Content.Server/EntityEffects/Effects/AdjustTemperature.cs @@ -1,11 +1,11 @@ using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { - public sealed partial class AdjustTemperature : ReagentEffect + public sealed partial class AdjustTemperature : EntityEffect { [DataField] public float Amount; @@ -16,16 +16,19 @@ public sealed partial class AdjustTemperature : ReagentEffect ("deltasign", MathF.Sign(Amount)), ("amount", MathF.Abs(Amount))); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out TemperatureComponent? temp)) + if (args.EntityManager.TryGetComponent(args.TargetEntity, out TemperatureComponent? temp)) { var sys = args.EntityManager.EntitySysManager.GetEntitySystem<TemperatureSystem>(); var amount = Amount; - amount *= args.Scale; + if (args is EntityEffectReagentArgs reagentArgs) + { + amount *= reagentArgs.Scale.Float(); + } - sys.ChangeHeat(args.SolutionEntity, amount, true, temp); + sys.ChangeHeat(args.TargetEntity, amount, true, temp); } } } diff --git a/Content.Server/EntityEffects/Effects/AreaReactionEffect.cs b/Content.Server/EntityEffects/Effects/AreaReactionEffect.cs new file mode 100644 index 00000000000..481f1fc27c5 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/AreaReactionEffect.cs @@ -0,0 +1,91 @@ +using Content.Server.Fluids.EntitySystems; +using Content.Shared.Audio; +using Content.Shared.Coordinates.Helpers; +using Content.Shared.Database; +using Content.Shared.EntityEffects; +using Content.Shared.FixedPoint; +using Content.Shared.Maps; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Basically smoke and foam reactions. +/// </summary> +[UsedImplicitly] +[DataDefinition] +public sealed partial class AreaReactionEffect : EntityEffect +{ + /// <summary> + /// How many seconds will the effect stay, counting after fully spreading. + /// </summary> + [DataField("duration")] private float _duration = 10; + + /// <summary> + /// How many units of reaction for 1 smoke entity. + /// </summary> + [DataField] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5); + + /// <summary> + /// The entity prototype that will be spawned as the effect. + /// </summary> + [DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] + private string _prototypeId = default!; + + /// <summary> + /// Sound that will get played when this reaction effect occurs. + /// </summary> + [DataField("sound", required: true)] private SoundSpecifier _sound = default!; + + public override bool ShouldLog => true; + + protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-area-reaction", + ("duration", _duration) + ); + + public override LogImpact LogImpact => LogImpact.High; + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.Source == null) + return; + + var spreadAmount = (int) Math.Max(0, Math.Ceiling((reagentArgs.Quantity / OverflowThreshold).Float())); + var splitSolution = reagentArgs.Source.SplitSolution(reagentArgs.Source.Volume); + var transform = reagentArgs.EntityManager.GetComponent<TransformComponent>(reagentArgs.TargetEntity); + var mapManager = IoCManager.Resolve<IMapManager>(); + var mapSys = reagentArgs.EntityManager.System<MapSystem>(); + var sys = reagentArgs.EntityManager.System<TransformSystem>(); + var mapCoords = sys.GetMapCoordinates(reagentArgs.TargetEntity, xform: transform); + + if (!mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) || + !mapSys.TryGetTileRef(gridUid, grid, transform.Coordinates, out var tileRef) || + tileRef.Tile.IsSpace()) + { + return; + } + + var coords = mapSys.MapToGrid(gridUid, mapCoords); + var ent = reagentArgs.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid()); + + var smoke = reagentArgs.EntityManager.System<SmokeSystem>(); + smoke.StartSmoke(ent, splitSolution, _duration, spreadAmount); + + var audio = reagentArgs.EntityManager.System<SharedAudioSystem>(); + audio.PlayPvs(_sound, reagentArgs.TargetEntity, AudioHelpers.WithVariation(0.125f)); + return; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/CauseZombieInfection.cs b/Content.Server/EntityEffects/Effects/CauseZombieInfection.cs similarity index 64% rename from Content.Server/Chemistry/ReagentEffects/CauseZombieInfection.cs rename to Content.Server/EntityEffects/Effects/CauseZombieInfection.cs index 029b1495002..dcbc676fe77 100644 --- a/Content.Server/Chemistry/ReagentEffects/CauseZombieInfection.cs +++ b/Content.Server/EntityEffects/Effects/CauseZombieInfection.cs @@ -1,21 +1,20 @@ using Content.Server.Zombies; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Configuration; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class CauseZombieInfection : ReagentEffect +public sealed partial class CauseZombieInfection : EntityEffect { protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-cause-zombie-infection", ("chance", Probability)); // Adds the Zombie Infection Components - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var entityManager = args.EntityManager; - entityManager.EnsureComponent<ZombifyOnDeathComponent>(args.SolutionEntity); - entityManager.EnsureComponent<PendingZombieComponent>(args.SolutionEntity); + entityManager.EnsureComponent<ZombifyOnDeathComponent>(args.TargetEntity); + entityManager.EnsureComponent<PendingZombieComponent>(args.TargetEntity); } } diff --git a/Content.Server/Chemistry/ReagentEffects/ChemAddMoodlet.cs b/Content.Server/EntityEffects/Effects/ChemAddMoodlet.cs similarity index 73% rename from Content.Server/Chemistry/ReagentEffects/ChemAddMoodlet.cs rename to Content.Server/EntityEffects/Effects/ChemAddMoodlet.cs index f5bb6292996..bfb61b368ab 100644 --- a/Content.Server/Chemistry/ReagentEffects/ChemAddMoodlet.cs +++ b/Content.Server/EntityEffects/Effects/ChemAddMoodlet.cs @@ -1,15 +1,15 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Mood; using JetBrains.Annotations; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; /// <summary> /// Adds a moodlet to an entity. /// </summary> [UsedImplicitly] -public sealed partial class ChemAddMoodlet : ReagentEffect +public sealed partial class ChemAddMoodlet : EntityEffect { protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) { @@ -25,10 +25,13 @@ public sealed partial class ChemAddMoodlet : ReagentEffect [DataField(required: true)] public ProtoId<MoodEffectPrototype> MoodPrototype = default!; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { + if (args is not EntityEffectReagentArgs _) + return; + var entityManager = IoCManager.Resolve<EntityManager>(); var ev = new MoodEffectEvent(MoodPrototype); - entityManager.EventBus.RaiseLocalEvent(args.SolutionEntity, ev); + entityManager.EventBus.RaiseLocalEvent(args.TargetEntity, ev); } } diff --git a/Content.Server/EntityEffects/Effects/ChemCleanBloodstream.cs b/Content.Server/EntityEffects/Effects/ChemCleanBloodstream.cs new file mode 100644 index 00000000000..b7f6f0dc885 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ChemCleanBloodstream.cs @@ -0,0 +1,40 @@ +using Content.Server.Body.Systems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Basically smoke and foam reactions. +/// </summary> +[UsedImplicitly] +public sealed partial class ChemCleanBloodstream : EntityEffect +{ + [DataField] + public float CleanseRate = 3.0f; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-chem-clean-bloodstream", ("chance", Probability)); + + public override void Effect(EntityEffectBaseArgs args) + { + + var cleanseRate = CleanseRate; + + var bloodstreamSys = args.EntityManager.System<BloodstreamSystem>(); + + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.Source == null || reagentArgs.Reagent == null) + return; + + cleanseRate *= reagentArgs.Scale.Float(); + bloodstreamSys.FlushChemicals(args.TargetEntity, reagentArgs.Reagent.ID, cleanseRate); + } + else + { + bloodstreamSys.FlushChemicals(args.TargetEntity, "", cleanseRate); + } + } +} diff --git a/Content.Server/EntityEffects/Effects/ChemHealEyeDamage.cs b/Content.Server/EntityEffects/Effects/ChemHealEyeDamage.cs new file mode 100644 index 00000000000..42fc9ffa9fb --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ChemHealEyeDamage.cs @@ -0,0 +1,31 @@ +using Content.Shared.EntityEffects; +using Content.Shared.Eye.Blinding.Systems; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Heal or apply eye damage +/// </summary> +[UsedImplicitly] +public sealed partial class ChemHealEyeDamage : EntityEffect +{ + /// <summary> + /// How much eye damage to add. + /// </summary> + [DataField] + public int Amount = -1; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-cure-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + if (reagentArgs.Scale != 1f) // huh? + return; + + args.EntityManager.EntitySysManager.GetEntitySystem<BlindableSystem>().AdjustEyeDamage(args.TargetEntity, Amount); + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/ChemVomit.cs b/Content.Server/EntityEffects/Effects/ChemVomit.cs similarity index 65% rename from Content.Server/Chemistry/ReagentEffects/ChemVomit.cs rename to Content.Server/EntityEffects/Effects/ChemVomit.cs index 851c0adf5f2..0d1bac87d1c 100644 --- a/Content.Server/Chemistry/ReagentEffects/ChemVomit.cs +++ b/Content.Server/EntityEffects/Effects/ChemVomit.cs @@ -1,15 +1,15 @@ using Content.Server.Medical; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { /// <summary> /// Forces you to vomit. /// </summary> [UsedImplicitly] - public sealed partial class ChemVomit : ReagentEffect + public sealed partial class ChemVomit : EntityEffect { /// How many units of thirst to add each time we vomit [DataField] @@ -21,14 +21,15 @@ public sealed partial class ChemVomit : ReagentEffect protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-chem-vomit", ("chance", Probability)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.Scale != 1f) - return; + if (args is EntityEffectReagentArgs reagentArgs) + if (reagentArgs.Scale != 1f) + return; var vomitSys = args.EntityManager.EntitySysManager.GetEntitySystem<VomitSystem>(); - vomitSys.Vomit(args.SolutionEntity, ThirstAmount, HungerAmount); + vomitSys.Vomit(args.TargetEntity, ThirstAmount, HungerAmount); } } } diff --git a/Content.Server/Chemistry/ReactionEffects/CreateEntityReactionEffect.cs b/Content.Server/EntityEffects/Effects/CreateEntityReactionEffect.cs similarity index 80% rename from Content.Server/Chemistry/ReactionEffects/CreateEntityReactionEffect.cs rename to Content.Server/EntityEffects/Effects/CreateEntityReactionEffect.cs index f8c0378452b..5288cb587da 100644 --- a/Content.Server/Chemistry/ReactionEffects/CreateEntityReactionEffect.cs +++ b/Content.Server/EntityEffects/Effects/CreateEntityReactionEffect.cs @@ -1,12 +1,11 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.GameObjects; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Chemistry.ReactionEffects; +namespace Content.Server.EntityEffects.Effects; [DataDefinition] -public sealed partial class CreateEntityReactionEffect : ReagentEffect +public sealed partial class CreateEntityReactionEffect : EntityEffect { /// <summary> /// What entity to create. @@ -26,15 +25,17 @@ public sealed partial class CreateEntityReactionEffect : ReagentEffect ("entname", IoCManager.Resolve<IPrototypeManager>().Index<EntityPrototype>(Entity).Name), ("amount", Number)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity); + var transform = args.EntityManager.GetComponent<TransformComponent>(args.TargetEntity); var transformSystem = args.EntityManager.System<SharedTransformSystem>(); - var quantity = Number * args.Quantity.Int(); + var quantity = (int)Number; + if (args is EntityEffectReagentArgs reagentArgs) + quantity *= reagentArgs.Quantity.Int(); for (var i = 0; i < quantity; i++) { - var uid = args.EntityManager.SpawnEntity(Entity, transform.MapPosition); + var uid = args.EntityManager.SpawnEntity(Entity, transformSystem.GetMapCoordinates(args.TargetEntity, xform: transform)); transformSystem.AttachToGridOrMap(uid); // TODO figure out how to properly spawn inside of containers diff --git a/Content.Server/Chemistry/ReagentEffects/CreateGas.cs b/Content.Server/EntityEffects/Effects/CreateGas.cs similarity index 62% rename from Content.Server/Chemistry/ReagentEffects/CreateGas.cs rename to Content.Server/EntityEffects/Effects/CreateGas.cs index c1da3c48db3..fb57a43c48d 100644 --- a/Content.Server/Chemistry/ReagentEffects/CreateGas.cs +++ b/Content.Server/EntityEffects/Effects/CreateGas.cs @@ -1,12 +1,12 @@ -using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos; -using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class CreateGas : ReagentEffect +public sealed partial class CreateGas : EntityEffect { [DataField(required: true)] public Gas Gas = default!; @@ -31,15 +31,22 @@ public sealed partial class CreateGas : ReagentEffect public override LogImpact LogImpact => LogImpact.High; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var atmosSys = args.EntityManager.EntitySysManager.GetEntitySystem<AtmosphereSystem>(); - var tileMix = atmosSys.GetContainingMixture(args.SolutionEntity, false, true); + var tileMix = atmosSys.GetContainingMixture(args.TargetEntity, false, true); if (tileMix != null) { - tileMix.AdjustMoles(Gas, args.Quantity.Float() * Multiplier); + if (args is EntityEffectReagentArgs reagentArgs) + { + tileMix.AdjustMoles(Gas, reagentArgs.Quantity.Float() * Multiplier); + } + else + { + tileMix.AdjustMoles(Gas, Multiplier); + } } } } diff --git a/Content.Server/Chemistry/ReagentEffects/CureZombieInfection.cs b/Content.Server/EntityEffects/Effects/CureZombieInfection.cs similarity index 73% rename from Content.Server/Chemistry/ReagentEffects/CureZombieInfection.cs rename to Content.Server/EntityEffects/Effects/CureZombieInfection.cs index d56fc115310..066fa872489 100644 --- a/Content.Server/Chemistry/ReagentEffects/CureZombieInfection.cs +++ b/Content.Server/EntityEffects/Effects/CureZombieInfection.cs @@ -1,11 +1,10 @@ using Content.Server.Zombies; -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Configuration; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class CureZombieInfection : ReagentEffect +public sealed partial class CureZombieInfection : EntityEffect { [DataField] public bool Innoculate; @@ -19,18 +18,18 @@ public sealed partial class CureZombieInfection : ReagentEffect } // Removes the Zombie Infection Components - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var entityManager = args.EntityManager; - if (entityManager.HasComponent<IncurableZombieComponent>(args.SolutionEntity)) + if (entityManager.HasComponent<IncurableZombieComponent>(args.TargetEntity)) return; - entityManager.RemoveComponent<ZombifyOnDeathComponent>(args.SolutionEntity); - entityManager.RemoveComponent<PendingZombieComponent>(args.SolutionEntity); + entityManager.RemoveComponent<ZombifyOnDeathComponent>(args.TargetEntity); + entityManager.RemoveComponent<PendingZombieComponent>(args.TargetEntity); if (Innoculate) { - entityManager.EnsureComponent<ZombieImmuneComponent>(args.SolutionEntity); + entityManager.EnsureComponent<ZombieImmuneComponent>(args.TargetEntity); } } } diff --git a/Content.Server/Chemistry/ReagentEffects/Drunk.cs b/Content.Server/EntityEffects/Effects/Drunk.cs similarity index 65% rename from Content.Server/Chemistry/ReagentEffects/Drunk.cs rename to Content.Server/EntityEffects/Effects/Drunk.cs index 6088db5787c..c41c349aaa1 100644 --- a/Content.Server/Chemistry/ReagentEffects/Drunk.cs +++ b/Content.Server/EntityEffects/Effects/Drunk.cs @@ -1,10 +1,10 @@ -using Content.Shared.Chemistry.Reagent; using Content.Shared.Drunk; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class Drunk : ReagentEffect +public sealed partial class Drunk : EntityEffect { /// <summary> /// BoozePower is how long each metabolism cycle will make the drunk effect last for. @@ -21,13 +21,14 @@ public sealed partial class Drunk : ReagentEffect protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-drunk", ("chance", Probability)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var boozePower = BoozePower; - boozePower *= Math.Max(args.Scale, 1); - + if (args is EntityEffectReagentArgs reagentArgs) + boozePower *= reagentArgs.Scale.Float(); + var drunkSys = args.EntityManager.EntitySysManager.GetEntitySystem<SharedDrunkSystem>(); - drunkSys.TryApplyDrunkenness(args.SolutionEntity, boozePower, SlurSpeech); + drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower, SlurSpeech); } } diff --git a/Content.Server/EntityEffects/Effects/Electrocute.cs b/Content.Server/EntityEffects/Effects/Electrocute.cs new file mode 100644 index 00000000000..f6a5f1a2da7 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Electrocute.cs @@ -0,0 +1,38 @@ +using Content.Server.Electrocution; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class Electrocute : EntityEffect +{ + [DataField] public int ElectrocuteTime = 2; + + [DataField] public int ElectrocuteDamageScale = 5; + + /// <remarks> + /// true - refresh electrocute time, false - accumulate electrocute time + /// </remarks> + [DataField] public bool Refresh = true; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime)); + + public override bool ShouldLog => true; + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + reagentArgs.EntityManager.System<ElectrocutionSystem>().TryDoElectrocution(reagentArgs.TargetEntity, null, + Math.Max((reagentArgs.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); + + if (reagentArgs.Reagent != null) + reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity); + } else + { + args.EntityManager.System<ElectrocutionSystem>().TryDoElectrocution(args.TargetEntity, null, + Math.Max(ElectrocuteDamageScale, 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); + } + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/Emote.cs b/Content.Server/EntityEffects/Effects/Emote.cs similarity index 59% rename from Content.Server/Chemistry/ReagentEffects/Emote.cs rename to Content.Server/EntityEffects/Effects/Emote.cs index 729f0671217..1d8e27ab0f8 100644 --- a/Content.Server/Chemistry/ReagentEffects/Emote.cs +++ b/Content.Server/EntityEffects/Effects/Emote.cs @@ -1,18 +1,18 @@ using Content.Server.Chat.Systems; using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; /// <summary> -/// Tries to force someone to emote (scream, laugh, etc). +/// Tries to force someone to emote (scream, laugh, etc). Still respects whitelists/blacklists and other limits of the specified emote unless forced. /// </summary> [UsedImplicitly] -public sealed partial class Emote : ReagentEffect +public sealed partial class Emote : EntityEffect { [DataField("emote", customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))] public string? EmoteId; @@ -20,20 +20,23 @@ public sealed partial class Emote : ReagentEffect [DataField] public bool ShowInChat; + [DataField] + public bool Force = false; + // JUSTIFICATION: Emoting is flavor, so same reason popup messages are not in here. protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { if (EmoteId == null) return; var chatSys = args.EntityManager.System<ChatSystem>(); if (ShowInChat) - chatSys.TryEmoteWithChat(args.SolutionEntity, EmoteId, ChatTransmitRange.GhostRangeLimit); + chatSys.TryEmoteWithChat(args.TargetEntity, EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: Force); else - chatSys.TryEmoteWithoutChat(args.SolutionEntity, EmoteId); + chatSys.TryEmoteWithoutChat(args.TargetEntity, EmoteId); } } diff --git a/Content.Server/Chemistry/ReactionEffects/EmpReactionEffect.cs b/Content.Server/EntityEffects/Effects/EmpReactionEffect.cs similarity index 59% rename from Content.Server/Chemistry/ReactionEffects/EmpReactionEffect.cs rename to Content.Server/EntityEffects/Effects/EmpReactionEffect.cs index b6714ca28d0..ec7801f0832 100644 --- a/Content.Server/Chemistry/ReactionEffects/EmpReactionEffect.cs +++ b/Content.Server/EntityEffects/Effects/EmpReactionEffect.cs @@ -1,15 +1,17 @@ using Content.Server.Emp; +using Content.Shared.EntityEffects; using Content.Shared.Chemistry.Reagent; +using Robust.Server.GameObjects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReactionEffects; +namespace Content.Server.EntityEffects.Effects; [DataDefinition] -public sealed partial class EmpReactionEffect : ReagentEffect +public sealed partial class EmpReactionEffect : EntityEffect { /// <summary> - /// Impulse range per unit of reagent + /// Impulse range per unit of quantity /// </summary> [DataField("rangePerUnit")] public float EmpRangePerUnit = 0.5f; @@ -35,13 +37,20 @@ public sealed partial class EmpReactionEffect : ReagentEffect protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-emp-reaction-effect", ("chance", Probability)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity); - var range = MathF.Min((float) (args.Quantity*EmpRangePerUnit), EmpMaxRange); + var tSys = args.EntityManager.System<TransformSystem>(); + var transform = args.EntityManager.GetComponent<TransformComponent>(args.TargetEntity); - args.EntityManager.System<EmpSystem>().EmpPulse( - transform.MapPosition, + var range = EmpRangePerUnit; + + if (args is EntityEffectReagentArgs reagentArgs) + { + range = MathF.Min((float) (reagentArgs.Quantity * EmpRangePerUnit), EmpMaxRange); + } + + args.EntityManager.System<EmpSystem>() + .EmpPulse(tSys.GetMapCoordinates(args.TargetEntity, xform: transform), range, EnergyConsumption, DisableDuration); diff --git a/Content.Server/EntityEffects/Effects/ExplosionReactionEffect.cs b/Content.Server/EntityEffects/Effects/ExplosionReactionEffect.cs new file mode 100644 index 00000000000..689757d0440 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ExplosionReactionEffect.cs @@ -0,0 +1,77 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.Database; +using Content.Shared.EntityEffects; +using Content.Shared.Explosion; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using System.Text.Json.Serialization; + +namespace Content.Server.EntityEffects.Effects; + +[DataDefinition] +public sealed partial class ExplosionReactionEffect : EntityEffect +{ + /// <summary> + /// The type of explosion. Determines damage types and tile break chance scaling. + /// </summary> + [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))] + [JsonIgnore] + public string ExplosionType = default!; + + /// <summary> + /// The max intensity the explosion can have at a given tile. Places an upper limit of damage and tile break + /// chance. + /// </summary> + [DataField] + [JsonIgnore] + public float MaxIntensity = 5; + + /// <summary> + /// How quickly intensity drops off as you move away from the epicenter + /// </summary> + [DataField] + [JsonIgnore] + public float IntensitySlope = 1; + + /// <summary> + /// The maximum total intensity that this chemical reaction can achieve. Basically here to prevent people + /// from creating a nuke by collecting enough potassium and water. + /// </summary> + /// <remarks> + /// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles. + /// </remarks> + [DataField] + [JsonIgnore] + public float MaxTotalIntensity = 100; + + /// <summary> + /// The intensity of the explosion per unit reaction. + /// </summary> + [DataField] + [JsonIgnore] + public float IntensityPerUnit = 1; + + public override bool ShouldLog => true; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-explosion-reaction-effect", ("chance", Probability)); + public override LogImpact LogImpact => LogImpact.High; + + public override void Effect(EntityEffectBaseArgs args) + { + var intensity = IntensityPerUnit; + + if (args is EntityEffectReagentArgs reagentArgs) + { + intensity = MathF.Min((float) reagentArgs.Quantity * IntensityPerUnit, MaxTotalIntensity); + } + + args.EntityManager.System<ExplosionSystem>() + .QueueExplosion( + args.TargetEntity, + ExplosionType, + intensity, + IntensitySlope, + MaxIntensity); + } +} diff --git a/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs b/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs new file mode 100644 index 00000000000..6d7e7c2fcd8 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs @@ -0,0 +1,36 @@ +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects +{ + [UsedImplicitly] + public sealed partial class ExtinguishReaction : EntityEffect + { + /// <summary> + /// Amount of firestacks reduced. + /// </summary> + [DataField] + public float FireStacksAdjustment = -1.5f; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-extinguish-reaction", ("chance", Probability)); + + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out FlammableComponent? flammable)) return; + + var flammableSystem = args.EntityManager.System<FlammableSystem>(); + flammableSystem.Extinguish(args.TargetEntity, flammable); + if (args is EntityEffectReagentArgs reagentArgs) + { + flammableSystem.AdjustFireStacks(reagentArgs.TargetEntity, FireStacksAdjustment * (float) reagentArgs.Quantity, flammable); + } else + { + flammableSystem.AdjustFireStacks(args.TargetEntity, FireStacksAdjustment, flammable); + } + } + } +} diff --git a/Content.Server/EntityEffects/Effects/FlammableReaction.cs b/Content.Server/EntityEffects/Effects/FlammableReaction.cs new file mode 100644 index 00000000000..edd499ab4a5 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/FlammableReaction.cs @@ -0,0 +1,45 @@ +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Database; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects +{ + [UsedImplicitly] + public sealed partial class FlammableReaction : EntityEffect + { + [DataField] + public float Multiplier = 0.05f; + + [DataField] + public float MultiplierOnExisting = 1f; + + public override bool ShouldLog => true; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-flammable-reaction", ("chance", Probability)); + + public override LogImpact LogImpact => LogImpact.Medium; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out FlammableComponent? flammable)) + return; + + var multiplier = flammable.FireStacks == 0f ? Multiplier : MultiplierOnExisting; + var quantity = 1f; + if (args is EntityEffectReagentArgs reagentArgs) + { + quantity = reagentArgs.Quantity.Float(); + reagentArgs.EntityManager.System<FlammableSystem>().AdjustFireStacks(args.TargetEntity, quantity * multiplier, flammable); + if (reagentArgs.Reagent != null) + reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity); + } else + { + args.EntityManager.System<FlammableSystem>().AdjustFireStacks(args.TargetEntity, multiplier, flammable); + } + } + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs b/Content.Server/EntityEffects/Effects/HealthChange.cs similarity index 85% rename from Content.Server/Chemistry/ReagentEffects/HealthChange.cs rename to Content.Server/EntityEffects/Effects/HealthChange.cs index 1e45f095a4c..0d2a236bb45 100644 --- a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs +++ b/Content.Server/EntityEffects/Effects/HealthChange.cs @@ -1,6 +1,6 @@ -using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Content.Shared.Localizations; using Content.Shared._Shitmed.Targeting; // Shitmed Change @@ -9,16 +9,16 @@ using System.Linq; using System.Text.Json.Serialization; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { /// <summary> - /// Default metabolism for medicine reagents. + /// Default metabolism used for medicine reagents. /// </summary> [UsedImplicitly] - public sealed partial class HealthChange : ReagentEffect + public sealed partial class HealthChange : EntityEffect { /// <summary> - /// Damage to apply every metabolism cycle. Damage Ignores resistances. + /// Damage to apply every cycle. Damage Ignores resistances. /// </summary> [DataField(required: true)] [JsonPropertyName("damage")] @@ -27,6 +27,7 @@ public sealed partial class HealthChange : ReagentEffect /// <summary> /// Should this effect scale the damage by the amount of chemical in the solution? /// Useful for touch reactions, like styptic powder or acid. + /// Only usable if the EntityEffectBaseArgs is an EntityEffectReagentArgs. /// </summary> [DataField] [JsonPropertyName("scaleByQuantity")] @@ -111,13 +112,17 @@ protected override string ReagentEffectGuidebookText(IPrototypeManager prototype ("healsordeals", healsordeals)); } - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - var scale = ScaleByQuantity ? args.Quantity : FixedPoint2.New(1); - scale *= args.Scale; + var scale = FixedPoint2.New(1); + + if (args is EntityEffectReagentArgs reagentArgs) + { + scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale; + } args.EntityManager.System<DamageableSystem>().TryChangeDamage( - args.SolutionEntity, + args.TargetEntity, Damage * scale, IgnoreResistances, interruptsDoAfters: false, diff --git a/Content.Server/EntityEffects/Effects/Ignite.cs b/Content.Server/EntityEffects/Effects/Ignite.cs new file mode 100644 index 00000000000..cca2a301fdf --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Ignite.cs @@ -0,0 +1,31 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Database; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Ignites a mob. +/// </summary> +public sealed partial class Ignite : EntityEffect +{ + public override bool ShouldLog => true; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-ignite", ("chance", Probability)); + + public override LogImpact LogImpact => LogImpact.Medium; + + public override void Effect(EntityEffectBaseArgs args) + { + var flamSys = args.EntityManager.System<FlammableSystem>(); + if (args is EntityEffectReagentArgs reagentArgs) + { + flamSys.Ignite(reagentArgs.TargetEntity, reagentArgs.OrganEntity ?? reagentArgs.TargetEntity); + } else + { + flamSys.Ignite(args.TargetEntity, args.TargetEntity); + } + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/EntityEffects/Effects/MakeSentient.cs similarity index 92% rename from Content.Server/Chemistry/ReagentEffects/MakeSentient.cs rename to Content.Server/EntityEffects/Effects/MakeSentient.cs index 21bfe239232..65da9e72219 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/EntityEffects/Effects/MakeSentient.cs @@ -2,7 +2,7 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Language; using Content.Server.Speech.Components; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Language; using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; @@ -11,17 +11,17 @@ using Content.Shared.Language.Components; //Delta-V - Banning humanoids from becoming ghost roles. using Content.Shared.Language.Events; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class MakeSentient : ReagentEffect +public sealed partial class MakeSentient : EntityEffect { protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-make-sentient", ("chance", Probability)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var entityManager = args.EntityManager; - var uid = args.SolutionEntity; + var uid = args.TargetEntity; // Let affected entities speak normally to make this effect different from, say, the "random sentience" event // This also works on entities that already have a mind diff --git a/Content.Server/EntityEffects/Effects/ModifyBleedAmount.cs b/Content.Server/EntityEffects/Effects/ModifyBleedAmount.cs new file mode 100644 index 00000000000..58bc304f5ea --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ModifyBleedAmount.cs @@ -0,0 +1,35 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class ModifyBleedAmount : EntityEffect +{ + [DataField] + public bool Scaled = false; + + [DataField] + public float Amount = -1.0f; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-modify-bleed-amount", ("chance", Probability), + ("deltasign", MathF.Sign(Amount))); + + public override void Effect(EntityEffectBaseArgs args) + { + if (args.EntityManager.TryGetComponent<BloodstreamComponent>(args.TargetEntity, out var blood)) + { + var sys = args.EntityManager.System<BloodstreamSystem>(); + var amt = Amount; + if (args is EntityEffectReagentArgs reagentArgs) { + if (Scaled) + amt *= reagentArgs.Quantity.Float(); + amt *= reagentArgs.Scale.Float(); + } + + sys.TryModifyBleedAmount(args.TargetEntity, amt, blood); + } + } +} diff --git a/Content.Server/EntityEffects/Effects/ModifyBloodLevel.cs b/Content.Server/EntityEffects/Effects/ModifyBloodLevel.cs new file mode 100644 index 00000000000..d8aca7d2842 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ModifyBloodLevel.cs @@ -0,0 +1,37 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class ModifyBloodLevel : EntityEffect +{ + [DataField] + public bool Scaled = false; + + [DataField] + public FixedPoint2 Amount = 1.0f; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-modify-blood-level", ("chance", Probability), + ("deltasign", MathF.Sign(Amount.Float()))); + + public override void Effect(EntityEffectBaseArgs args) + { + if (args.EntityManager.TryGetComponent<BloodstreamComponent>(args.TargetEntity, out var blood)) + { + var sys = args.EntityManager.System<BloodstreamSystem>(); + var amt = Amount; + if (args is EntityEffectReagentArgs reagentArgs) + { + if (Scaled) + amt *= reagentArgs.Quantity; + amt *= reagentArgs.Scale; + } + + sys.TryModifyBloodLevel(args.TargetEntity, amt, blood); + } + } +} diff --git a/Content.Server/EntityEffects/Effects/ModifyLungGas.cs b/Content.Server/EntityEffects/Effects/ModifyLungGas.cs new file mode 100644 index 00000000000..b4f35e3d56b --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ModifyLungGas.cs @@ -0,0 +1,49 @@ +using Content.Server.Body.Components; +using Content.Shared.Atmos; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; +using Content.Shared.EntityEffects; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class ModifyLungGas : EntityEffect +{ + [DataField("ratios", required: true)] + private Dictionary<Gas, float> _ratios = default!; + + // JUSTIFICATION: This is internal magic that players never directly interact with. + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => null; + + public override void Effect(EntityEffectBaseArgs args) + { + + LungComponent? lung; + float amount = 1f; + + if (args is EntityEffectReagentArgs reagentArgs) + { + if (!args.EntityManager.TryGetComponent<LungComponent>(reagentArgs.OrganEntity, out var organLung)) + return; + lung = organLung; + amount = reagentArgs.Quantity.Float(); + } + else + { + if (!args.EntityManager.TryGetComponent<LungComponent>(args.TargetEntity, out var organLung)) //Likely needs to be modified to ensure it works correctly + return; + lung = organLung; + } + + if (lung != null) + { + foreach (var (gas, ratio) in _ratios) + { + var quantity = ratio * amount / Atmospherics.BreathMolesToReagentMultiplier; + if (quantity < 0) + quantity = Math.Max(quantity, -lung.Air[(int) gas]); + lung.Air.AdjustMoles(gas, quantity); + } + } + } +} diff --git a/Content.Server/EntityEffects/Effects/MovespeedModifier.cs b/Content.Server/EntityEffects/Effects/MovespeedModifier.cs new file mode 100644 index 00000000000..ac1f143e9fb --- /dev/null +++ b/Content.Server/EntityEffects/Effects/MovespeedModifier.cs @@ -0,0 +1,78 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.EntityEffects; +using Content.Shared.Movement.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Default metabolism for stimulants and tranqs. Attempts to find a MovementSpeedModifier on the target, +/// adding one if not there and to change the movespeed +/// </summary> +public sealed partial class MovespeedModifier : EntityEffect +{ + /// <summary> + /// How much the entities' walk speed is multiplied by. + /// </summary> + [DataField] + public float WalkSpeedModifier { get; set; } = 1; + + /// <summary> + /// How much the entities' run speed is multiplied by. + /// </summary> + [DataField] + public float SprintSpeedModifier { get; set; } = 1; + + /// <summary> + /// How long the modifier applies (in seconds). + /// Is scaled by reagent amount if used with an EntityEffectReagentArgs. + /// </summary> + [DataField] + public float StatusLifetime = 2f; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return Loc.GetString("reagent-effect-guidebook-movespeed-modifier", + ("chance", Probability), + ("walkspeed", WalkSpeedModifier), + ("time", StatusLifetime)); + } + + /// <summary> + /// Remove reagent at set rate, changes the movespeed modifiers and adds a MovespeedModifierMetabolismComponent if not already there. + /// </summary> + public override void Effect(EntityEffectBaseArgs args) + { + var status = args.EntityManager.EnsureComponent<MovespeedModifierMetabolismComponent>(args.TargetEntity); + + // Only refresh movement if we need to. + var modified = !status.WalkSpeedModifier.Equals(WalkSpeedModifier) || + !status.SprintSpeedModifier.Equals(SprintSpeedModifier); + + status.WalkSpeedModifier = WalkSpeedModifier; + status.SprintSpeedModifier = SprintSpeedModifier; + + // only going to scale application time + var statusLifetime = StatusLifetime; + + if (args is EntityEffectReagentArgs reagentArgs) + { + statusLifetime *= reagentArgs.Scale.Float(); + } + + IncreaseTimer(status, statusLifetime); + + if (modified) + args.EntityManager.System<MovementSpeedModifierSystem>().RefreshMovementSpeedModifiers(args.TargetEntity); + } + public void IncreaseTimer(MovespeedModifierMetabolismComponent status, float time) + { + var gameTiming = IoCManager.Resolve<IGameTiming>(); + + var offsetTime = Math.Max(status.ModifierTimer.TotalSeconds, gameTiming.CurTime.TotalSeconds); + + status.ModifierTimer = TimeSpan.FromSeconds(offsetTime + time); + status.Dirty(); + } +} diff --git a/Content.Server/EntityEffects/Effects/Oxygenate.cs b/Content.Server/EntityEffects/Effects/Oxygenate.cs new file mode 100644 index 00000000000..60383188c9f --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Oxygenate.cs @@ -0,0 +1,32 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class Oxygenate : EntityEffect +{ + [DataField] + public float Factor = 1f; + + // JUSTIFICATION: This is internal magic that players never directly interact with. + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => null; + + public override void Effect(EntityEffectBaseArgs args) + { + + var multiplier = 1f; + if (args is EntityEffectReagentArgs reagentArgs) + { + multiplier = reagentArgs.Quantity.Float(); + } + + if (args.EntityManager.TryGetComponent<RespiratorComponent>(args.TargetEntity, out var resp)) + { + var respSys = args.EntityManager.System<RespiratorSystem>(); + respSys.UpdateSaturation(args.TargetEntity, multiplier * Factor, resp); + } + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/Paralyze.cs b/Content.Server/EntityEffects/Effects/Paralyze.cs similarity index 57% rename from Content.Server/Chemistry/ReagentEffects/Paralyze.cs rename to Content.Server/EntityEffects/Effects/Paralyze.cs index 077d1abf2c2..7dd2fdc1057 100644 --- a/Content.Server/Chemistry/ReagentEffects/Paralyze.cs +++ b/Content.Server/EntityEffects/Effects/Paralyze.cs @@ -1,10 +1,10 @@ using Content.Server.Stunnable; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class Paralyze : ReagentEffect +public sealed partial class Paralyze : EntityEffect { [DataField] public double ParalyzeTime = 2; @@ -18,12 +18,14 @@ public sealed partial class Paralyze : ReagentEffect ("chance", Probability), ("time", ParalyzeTime)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var paralyzeTime = ParalyzeTime; - paralyzeTime *= args.Scale; - EntitySystem.Get<StunSystem>().TryParalyze(args.SolutionEntity, TimeSpan.FromSeconds(paralyzeTime), Refresh); + if (args is EntityEffectReagentArgs reagentArgs) + paralyzeTime *= (double)reagentArgs.Scale; + + args.EntityManager.System<StunSystem>().TryParalyze(args.TargetEntity, TimeSpan.FromSeconds(paralyzeTime), Refresh); } } diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs new file mode 100644 index 00000000000..5133fe502ff --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs @@ -0,0 +1,61 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class PlantAdjustAttribute : EntityEffect +{ + [DataField] + public float Amount { get; protected set; } = 1; + + /// <summary> + /// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions. + /// </summary> + [DataField] + public abstract string GuidebookAttributeName { get; set; } + + /// <summary> + /// Whether the attribute in question is a good thing. Used for guidebook descriptions to determine the color of the number. + /// </summary> + [DataField] + public virtual bool GuidebookIsAttributePositive { get; protected set; } = true; + + /// <summary> + /// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default. + /// </summary> + /// <param name="plantHolder">The entity holding the plant</param> + /// <param name="plantHolderComponent">The plant holder component</param> + /// <param name="entityManager">The entity manager</param> + /// <param name="mustHaveAlivePlant">Whether to check if it has an alive plant or not</param> + /// <returns></returns> + public bool CanMetabolize(EntityUid plantHolder, [NotNullWhen(true)] out PlantHolderComponent? plantHolderComponent, + IEntityManager entityManager, + bool mustHaveAlivePlant = true) + { + plantHolderComponent = null; + + if (!entityManager.TryGetComponent(plantHolder, out plantHolderComponent) + || mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead)) + return false; + + return true; + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + string color; + if (GuidebookIsAttributePositive ^ Amount < 0.0) + { + color = "green"; + } + else + { + color = "red"; + } + return Loc.GetString("reagent-effect-guidebook-plant-attribute", ("attribute", Loc.GetString(GuidebookAttributeName)), ("amount", Amount.ToString("0.00")), ("colorName", color), ("chance", Probability)); + } +} diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs new file mode 100644 index 00000000000..774f6ec48c6 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs @@ -0,0 +1,21 @@ +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +public sealed partial class PlantAdjustHealth : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-health"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + var plantHolder = args.EntityManager.System<PlantHolderSystem>(); + + plantHolderComp.Health += Amount; + plantHolder.CheckHealth(args.TargetEntity, plantHolderComp); + } +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs new file mode 100644 index 00000000000..bf41a21bcb0 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs @@ -0,0 +1,16 @@ +using Content.Shared.EntityEffects; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +public sealed partial class PlantAdjustMutationLevel : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-level"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + plantHolderComp.MutationLevel += Amount * plantHolderComp.MutationMod; + } +} diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs new file mode 100644 index 00000000000..6ed793e6143 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs @@ -0,0 +1,19 @@ +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAdjustMutationMod : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-mod"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + plantHolderComp.MutationMod += Amount; + } +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs new file mode 100644 index 00000000000..b9389afacdf --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs @@ -0,0 +1,21 @@ +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAdjustNutrition : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-nutrition"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false)) + return; + + var plantHolder = args.EntityManager.System<PlantHolderSystem>(); + + plantHolder.AdjustNutrient(args.TargetEntity, Amount, plantHolderComp); + } +} diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs new file mode 100644 index 00000000000..219529bfc4c --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs @@ -0,0 +1,20 @@ +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAdjustPests : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-pests"; + public override bool GuidebookIsAttributePositive { get; protected set; } = false; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + plantHolderComp.PestLevel += Amount; + } +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs new file mode 100644 index 00000000000..ab1baa42850 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs @@ -0,0 +1,20 @@ +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAdjustToxins : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-toxins"; + public override bool GuidebookIsAttributePositive { get; protected set; } = false; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + plantHolderComp.Toxins += Amount; + } +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs new file mode 100644 index 00000000000..41774977d54 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs @@ -0,0 +1,22 @@ +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAdjustWater : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-water"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false)) + return; + + var plantHolder = args.EntityManager.System<PlantHolderSystem>(); + + plantHolder.AdjustWater(args.TargetEntity, Amount, plantHolderComp); + } +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs new file mode 100644 index 00000000000..421e31998db --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs @@ -0,0 +1,19 @@ +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAdjustWeeds : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-weeds"; + public override bool GuidebookIsAttributePositive { get; protected set; } = false; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + plantHolderComp.WeedLevel += Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs new file mode 100644 index 00000000000..397eace399a --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs @@ -0,0 +1,22 @@ +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +public sealed partial class PlantAffectGrowth : PlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-growth"; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager)) + return; + + var plantHolder = args.EntityManager.System<PlantHolderSystem>(); + + plantHolder.AffectGrowth(args.TargetEntity, (int) Amount, plantHolderComp); + } +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs new file mode 100644 index 00000000000..7fe4b7a1707 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs @@ -0,0 +1,32 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +[DataDefinition] +public sealed partial class PlantCryoxadone : EntityEffect +{ + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out PlantHolderComponent? plantHolderComp) + || plantHolderComp.Seed == null || plantHolderComp.Dead) + return; + + var deviation = 0; + var seed = plantHolderComp.Seed; + var random = IoCManager.Resolve<IRobustRandom>(); + if (plantHolderComp.Age > seed.Maturation) + deviation = (int) Math.Max(seed.Maturation - 1, plantHolderComp.Age - random.Next(7, 10)); + else + deviation = (int) (seed.Maturation / seed.GrowthStages); + plantHolderComp.Age -= deviation; + plantHolderComp.SkipAging++; + plantHolderComp.ForceUpdate = true; + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-cryoxadone", ("chance", Probability)); +} diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs new file mode 100644 index 00000000000..36b6a833423 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs @@ -0,0 +1,41 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +[DataDefinition] +public sealed partial class PlantDiethylamine : EntityEffect +{ + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out PlantHolderComponent? plantHolderComp) + || plantHolderComp.Seed == null || plantHolderComp.Dead || + plantHolderComp.Seed.Immutable) + return; + + + var plantHolder = args.EntityManager.System<PlantHolderSystem>(); + + var random = IoCManager.Resolve<IRobustRandom>(); + + if (random.Prob(0.1f)) + { + plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp); + plantHolderComp.Seed.Lifespan++; + } + + if (random.Prob(0.1f)) + { + plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp); + plantHolderComp.Seed.Endurance++; + } + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-diethylamine", ("chance", Probability)); +} + diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs new file mode 100644 index 00000000000..96d98bfbf2e --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs @@ -0,0 +1,23 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +[DataDefinition] +public sealed partial class PlantPhalanximine : EntityEffect +{ + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out PlantHolderComponent? plantHolderComp) + || plantHolderComp.Seed == null || plantHolderComp.Dead || + plantHolderComp.Seed.Immutable) + return; + + plantHolderComp.Seed.Viable = true; + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-phalanximine", ("chance", Probability)); +} diff --git a/Content.Server/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs b/Content.Server/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs new file mode 100644 index 00000000000..695cb96675c --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs @@ -0,0 +1,53 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.PlantMetabolism; + +[UsedImplicitly] +[DataDefinition] +public sealed partial class RobustHarvest : EntityEffect +{ + [DataField] + public int PotencyLimit = 50; + + [DataField] + public int PotencyIncrease = 3; + + [DataField] + public int PotencySeedlessThreshold = 30; + + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out PlantHolderComponent? plantHolderComp) + || plantHolderComp.Seed == null || plantHolderComp.Dead || + plantHolderComp.Seed.Immutable) + return; + + + var plantHolder = args.EntityManager.System<PlantHolderSystem>(); + var random = IoCManager.Resolve<IRobustRandom>(); + + if (plantHolderComp.Seed.Potency < PotencyLimit) + { + plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp); + plantHolderComp.Seed.Potency = Math.Min(plantHolderComp.Seed.Potency + PotencyIncrease, PotencyLimit); + + if (plantHolderComp.Seed.Potency > PotencySeedlessThreshold) + { + plantHolderComp.Seed.Seedless = true; + } + } + else if (plantHolderComp.Seed.Yield > 1 && random.Prob(0.1f)) + { + // Too much of a good thing reduces yield + plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp); + plantHolderComp.Seed.Yield--; + } + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-robust-harvest", ("seedlesstreshold", PotencySeedlessThreshold), ("limit", PotencyLimit), ("increase", PotencyIncrease), ("chance", Probability)); +} diff --git a/Content.Server/Chemistry/ReagentEffects/Polymorph.cs b/Content.Server/EntityEffects/Effects/Polymorph.cs similarity index 80% rename from Content.Server/Chemistry/ReagentEffects/Polymorph.cs rename to Content.Server/EntityEffects/Effects/Polymorph.cs index fea372125ee..00a5c6613d0 100644 --- a/Content.Server/Chemistry/ReagentEffects/Polymorph.cs +++ b/Content.Server/EntityEffects/Effects/Polymorph.cs @@ -1,13 +1,14 @@ using Content.Server.Polymorph.Components; using Content.Server.Polymorph.Systems; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Polymorph; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Shared.EntityEffects; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; -public sealed partial class Polymorph : ReagentEffect +public sealed partial class Polymorph : EntityEffect { /// <summary> /// What polymorph prototype is used on effect @@ -20,10 +21,10 @@ public sealed partial class Polymorph : ReagentEffect ("chance", Probability), ("entityname", prototype.Index<EntityPrototype>(prototype.Index<PolymorphPrototype>(PolymorphPrototype).Configuration.Entity).Name)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var entityManager = args.EntityManager; - var uid = args.SolutionEntity; + var uid = args.TargetEntity; var polySystem = entityManager.System<PolymorphSystem>(); // Make it into a prototype diff --git a/Content.Server/Chemistry/ReagentEffects/PopupMessage.cs b/Content.Server/EntityEffects/Effects/PopupMessage.cs similarity index 60% rename from Content.Server/Chemistry/ReagentEffects/PopupMessage.cs rename to Content.Server/EntityEffects/Effects/PopupMessage.cs index 8278e95c1da..ac22b13051b 100644 --- a/Content.Server/Chemistry/ReagentEffects/PopupMessage.cs +++ b/Content.Server/EntityEffects/Effects/PopupMessage.cs @@ -1,12 +1,11 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Popups; -using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { - public sealed partial class PopupMessage : ReagentEffect + public sealed partial class PopupMessage : EntityEffect { [DataField(required: true)] public string[] Messages = default!; @@ -21,20 +20,30 @@ public sealed partial class PopupMessage : ReagentEffect protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var popupSys = args.EntityManager.EntitySysManager.GetEntitySystem<SharedPopupSystem>(); var random = IoCManager.Resolve<IRobustRandom>(); var msg = random.Pick(Messages); - var msgArgs = new (string, object)[] { - ("entity", args.SolutionEntity), - ("organ", args.OrganEntity.GetValueOrDefault()), + var msgArgs = new (string, object)[] + { + ("entity", args.TargetEntity), }; + + if (args is EntityEffectReagentArgs reagentArgs) + { + msgArgs = new (string, object)[] + { + ("entity", reagentArgs.TargetEntity), + ("organ", reagentArgs.OrganEntity.GetValueOrDefault()), + }; + } + if (Type == PopupRecipients.Local) - popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.SolutionEntity, args.SolutionEntity, VisualType); + popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.TargetEntity, args.TargetEntity, VisualType); else if (Type == PopupRecipients.Pvs) - popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.SolutionEntity, VisualType); + popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.TargetEntity, VisualType); } } diff --git a/Content.Server/Chemistry/ReagentEffects/PurifyEvil.cs b/Content.Server/EntityEffects/Effects/PurifyEvil.cs similarity index 84% rename from Content.Server/Chemistry/ReagentEffects/PurifyEvil.cs rename to Content.Server/EntityEffects/Effects/PurifyEvil.cs index 896ecf2eac5..d28104d1db0 100644 --- a/Content.Server/Chemistry/ReagentEffects/PurifyEvil.cs +++ b/Content.Server/EntityEffects/Effects/PurifyEvil.cs @@ -1,5 +1,5 @@ using System.Threading; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Jittering; using Content.Shared.WhiteDream.BloodCult.BloodCultist; using JetBrains.Annotations; @@ -8,7 +8,7 @@ namespace Content.Server.Chemistry.ReagentEffects; [UsedImplicitly] -public sealed partial class PurifyEvil : ReagentEffect +public sealed partial class PurifyEvil : EntityEffect { [DataField] public float Amplitude = 10.0f; @@ -24,15 +24,16 @@ protected override string ReagentEffectGuidebookText(IPrototypeManager prototype return Loc.GetString("reagent-effect-guidebook-purify-evil"); } - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { + if (args is not EntityEffectReagentArgs e) + return; + var entityManager = args.EntityManager; - var uid = args.SolutionEntity; + var uid = args.TargetEntity; if (!entityManager.TryGetComponent(uid, out BloodCultistComponent? bloodCultist) || bloodCultist.DeconvertToken is not null) - { return; - } entityManager.System<SharedJitteringSystem>().DoJitter(uid, Time, true, Amplitude, Frequency); diff --git a/Content.Server/Chemistry/ReagentEffects/ReduceRotting.cs b/Content.Server/EntityEffects/Effects/ReduceRotting.cs similarity index 61% rename from Content.Server/Chemistry/ReagentEffects/ReduceRotting.cs rename to Content.Server/EntityEffects/Effects/ReduceRotting.cs index cea4f853321..5c00492744d 100644 --- a/Content.Server/Chemistry/ReagentEffects/ReduceRotting.cs +++ b/Content.Server/EntityEffects/Effects/ReduceRotting.cs @@ -1,15 +1,16 @@ using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Content.Server.Atmos.Rotting; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { /// <summary> /// Reduces the rotting accumulator on the patient, making them revivable. /// </summary> [UsedImplicitly] - public sealed partial class ReduceRotting : ReagentEffect + public sealed partial class ReduceRotting : EntityEffect { [DataField("seconds")] public double RottingAmount = 10; @@ -18,14 +19,17 @@ public sealed partial class ReduceRotting : ReagentEffect => Loc.GetString("reagent-effect-guidebook-reduce-rotting", ("chance", Probability), ("time", RottingAmount)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.Scale != 1f) - return; + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.Scale != 1f) + return; + } var rottingSys = args.EntityManager.EntitySysManager.GetEntitySystem<RottingSystem>(); - rottingSys.ReduceAccumulator(args.SolutionEntity, TimeSpan.FromSeconds(RottingAmount)); + rottingSys.ReduceAccumulator(args.TargetEntity, TimeSpan.FromSeconds(RottingAmount)); } } } diff --git a/Content.Server/Chemistry/ReagentEffects/ResetNarcolepsy.cs b/Content.Server/EntityEffects/Effects/ResetNarcolepsy.cs similarity index 63% rename from Content.Server/Chemistry/ReagentEffects/ResetNarcolepsy.cs rename to Content.Server/EntityEffects/Effects/ResetNarcolepsy.cs index 2893505fc85..40a9c982515 100644 --- a/Content.Server/Chemistry/ReagentEffects/ResetNarcolepsy.cs +++ b/Content.Server/EntityEffects/Effects/ResetNarcolepsy.cs @@ -1,15 +1,16 @@ using Content.Server.Traits.Assorted; using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects; /// <summary> /// Reset narcolepsy timer /// </summary> [UsedImplicitly] -public sealed partial class ResetNarcolepsy : ReagentEffect +public sealed partial class ResetNarcolepsy : EntityEffect { /// <summary> /// The # of seconds the effect resets the narcolepsy timer to @@ -20,11 +21,12 @@ public sealed partial class ResetNarcolepsy : ReagentEffect protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-reset-narcolepsy", ("chance", Probability)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.Scale != 1f) - return; + if (args is EntityEffectReagentArgs reagentArgs) + if (reagentArgs.Scale != 1f) + return; - args.EntityManager.EntitySysManager.GetEntitySystem<NarcolepsySystem>().AdjustNarcolepsyTimer(args.SolutionEntity, TimerReset); + args.EntityManager.EntitySysManager.GetEntitySystem<NarcolepsySystem>().AdjustNarcolepsyTimer(args.TargetEntity, TimerReset); } } diff --git a/Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs b/Content.Server/EntityEffects/Effects/SatiateHunger.cs similarity index 53% rename from Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs rename to Content.Server/EntityEffects/Effects/SatiateHunger.cs index be772022556..dd58654dffc 100644 --- a/Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs +++ b/Content.Server/EntityEffects/Effects/SatiateHunger.cs @@ -1,31 +1,40 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects +namespace Content.Server.EntityEffects.Effects { /// <summary> /// Attempts to find a HungerComponent on the target, /// and to update it's hunger values. /// </summary> - public sealed partial class SatiateHunger : ReagentEffect + public sealed partial class SatiateHunger : EntityEffect { private const float DefaultNutritionFactor = 3.0f; /// <summary> - /// How much hunger is satiated when 1u of the reagent is metabolized + /// How much hunger is satiated. + /// Is multiplied by quantity if used with EntityEffectReagentArgs. /// </summary> [DataField("factor")] public float NutritionFactor { get; set; } = DefaultNutritionFactor; //Remove reagent at set rate, satiate hunger if a HungerComponent can be found - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { var entman = args.EntityManager; - if (!entman.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger)) + if (!entman.TryGetComponent(args.TargetEntity, out HungerComponent? hunger)) return; - entman.System<HungerSystem>().ModifyHunger(args.SolutionEntity, NutritionFactor * (float) args.Quantity, hunger); + if (args is EntityEffectReagentArgs reagentArgs) + { + entman.System<HungerSystem>().ModifyHunger(reagentArgs.TargetEntity, NutritionFactor * (float) reagentArgs.Quantity, hunger); + } + else + { + entman.System<HungerSystem>().ModifyHunger(args.TargetEntity, NutritionFactor, hunger); + } } protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) diff --git a/Content.Server/EntityEffects/Effects/SatiateThirst.cs b/Content.Server/EntityEffects/Effects/SatiateThirst.cs new file mode 100644 index 00000000000..d0dbe4371b9 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/SatiateThirst.cs @@ -0,0 +1,32 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, +/// and to update it's thirst values. +/// </summary> +public sealed partial class SatiateThirst : EntityEffect +{ + private const float DefaultHydrationFactor = 3.0f; + + /// How much thirst is satiated each tick. Not currently tied to + /// rate or anything. + [DataField("factor")] + public float HydrationFactor { get; set; } = DefaultHydrationFactor; + + /// Satiate thirst if a ThirstComponent can be found + public override void Effect(EntityEffectBaseArgs args) + { + var uid = args.TargetEntity; + if (args.EntityManager.TryGetComponent(uid, out ThirstComponent? thirst)) + args.EntityManager.System<ThirstSystem>().ModifyThirst(uid, thirst, HydrationFactor); + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", HydrationFactor / DefaultHydrationFactor)); +} diff --git a/Content.Server/EntityEffects/Effects/SolutionTemperatureEffects.cs b/Content.Server/EntityEffects/Effects/SolutionTemperatureEffects.cs new file mode 100644 index 00000000000..85c20dbcd11 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/SolutionTemperatureEffects.cs @@ -0,0 +1,144 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +/// <summary> +/// Sets the temperature of the solution involved with the reaction to a new value. +/// </summary> +[DataDefinition] +public sealed partial class SetSolutionTemperatureEffect : EntityEffect +{ + /// <summary> + /// The temperature to set the solution to. + /// </summary> + [DataField("temperature", required: true)] private float _temperature; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-set-solution-temperature-effect", + ("chance", Probability), ("temperature", _temperature)); + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + var solution = reagentArgs.Source; + if (solution == null) + return; + + solution.Temperature = _temperature; + + return; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } +} + +/// <summary> +/// Adjusts the temperature of the solution involved in the reaction. +/// </summary> +[DataDefinition] +public sealed partial class AdjustSolutionTemperatureEffect : EntityEffect +{ + /// <summary> + /// The change in temperature. + /// </summary> + [DataField("delta", required: true)] private float _delta; + + /// <summary> + /// The minimum temperature this effect can reach. + /// </summary> + [DataField("minTemp")] private float _minTemp = 0.0f; + + /// <summary> + /// The maximum temperature this effect can reach. + /// </summary> + [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; + + /// <summary> + /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. + /// </summary> + [DataField("scaled")] private bool _scaled; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", + ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + var solution = reagentArgs.Source; + if (solution == null || solution.Volume == 0) + return; + + var deltaT = _scaled ? _delta * (float) reagentArgs.Quantity : _delta; + solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); + + return; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } +} + +/// <summary> +/// Adjusts the thermal energy of the solution involved in the reaction. +/// </summary> +public sealed partial class AdjustSolutionThermalEnergyEffect : EntityEffect +{ + /// <summary> + /// The change in energy. + /// </summary> + [DataField("delta", required: true)] private float _delta; + + /// <summary> + /// The minimum temperature this effect can reach. + /// </summary> + [DataField("minTemp")] private float _minTemp = 0.0f; + + /// <summary> + /// The maximum temperature this effect can reach. + /// </summary> + [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; + + /// <summary> + /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. + /// </summary> + [DataField("scaled")] private bool _scaled; + + public override void Effect(EntityEffectBaseArgs args) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + var solution = reagentArgs.Source; + if (solution == null || solution.Volume == 0) + return; + + if (_delta > 0 && solution.Temperature >= _maxTemp) + return; + if (_delta < 0 && solution.Temperature <= _minTemp) + return; + + var heatCap = solution.GetHeatCapacity(null); + var deltaT = _scaled + ? _delta / heatCap * (float) reagentArgs.Quantity + : _delta / heatCap; + + solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); + + return; + } + + // TODO: Someone needs to figure out how to do this for non-reagent effects. + throw new NotImplementedException(); + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", + ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); +} diff --git a/Content.Server/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs b/Content.Server/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs new file mode 100644 index 00000000000..bb9bbf34469 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs @@ -0,0 +1,77 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Content.Shared.StatusEffect; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects.StatusEffects; + +/// <summary> +/// Adds a generic status effect to the entity, +/// not worrying about things like how to affect the time it lasts for +/// or component fields or anything. Just adds a component to an entity +/// for a given time. Easy. +/// </summary> +/// <remarks> +/// Can be used for things like adding accents or something. I don't know. Go wild. +/// </remarks> +[UsedImplicitly] +public sealed partial class GenericStatusEffect : EntityEffect +{ + [DataField(required: true)] + public string Key = default!; + + [DataField] + public string Component = String.Empty; + + [DataField] + public float Time = 2.0f; + + /// <remarks> + /// true - refresh status effect time, false - accumulate status effect time + /// </remarks> + [DataField] + public bool Refresh = true; + + /// <summary> + /// Should this effect add the status effect, remove time from it, or set its cooldown? + /// </summary> + [DataField] + public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; + + public override void Effect(EntityEffectBaseArgs args) + { + var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem<StatusEffectsSystem>(); + + var time = Time; + if (args is EntityEffectReagentArgs reagentArgs) + time *= reagentArgs.Scale.Float(); + + if (Type == StatusEffectMetabolismType.Add && Component != String.Empty) + { + statusSys.TryAddStatusEffect(args.TargetEntity, Key, TimeSpan.FromSeconds(time), Refresh, Component); + } + else if (Type == StatusEffectMetabolismType.Remove) + { + statusSys.TryRemoveTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time)); + } + else if (Type == StatusEffectMetabolismType.Set) + { + statusSys.TrySetTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time)); + } + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString( + "reagent-effect-guidebook-status-effect", + ("chance", Probability), + ("type", Type), + ("time", Time), + ("key", $"reagent-effect-status-effect-{Key}")); +} + +public enum StatusEffectMetabolismType +{ + Add, + Remove, + Set +} diff --git a/Content.Server/EntityEffects/Effects/StatusEffects/Jitter.cs b/Content.Server/EntityEffects/Effects/StatusEffects/Jitter.cs new file mode 100644 index 00000000000..dbca4fad588 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/StatusEffects/Jitter.cs @@ -0,0 +1,42 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Content.Shared.Jittering; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects.StatusEffects; + +/// <summary> +/// Adds the jitter status effect to a mob. +/// This doesn't use generic status effects because it needs to +/// take in some parameters that JitterSystem needs. +/// </summary> +public sealed partial class Jitter : EntityEffect +{ + [DataField] + public float Amplitude = 10.0f; + + [DataField] + public float Frequency = 4.0f; + + [DataField] + public float Time = 2.0f; + + /// <remarks> + /// true - refresh jitter time, false - accumulate jitter time + /// </remarks> + [DataField] + public bool Refresh = true; + + public override void Effect(EntityEffectBaseArgs args) + { + var time = Time; + if (args is EntityEffectReagentArgs reagentArgs) + time *= reagentArgs.Scale.Float(); + + args.EntityManager.EntitySysManager.GetEntitySystem<SharedJitteringSystem>() + .DoJitter(args.TargetEntity, TimeSpan.FromSeconds(time), Refresh, Amplitude, Frequency); + } + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("reagent-effect-guidebook-jittering", ("chance", Probability)); +} diff --git a/Content.Server/EntityEffects/Effects/WashCreamPieReaction.cs b/Content.Server/EntityEffects/Effects/WashCreamPieReaction.cs new file mode 100644 index 00000000000..67bfac6335d --- /dev/null +++ b/Content.Server/EntityEffects/Effects/WashCreamPieReaction.cs @@ -0,0 +1,22 @@ +using Content.Server.Nutrition.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Content.Shared.Nutrition.Components; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +[UsedImplicitly] +public sealed partial class WashCreamPieReaction : EntityEffect +{ + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-wash-cream-pie-reaction", ("chance", Probability)); + + public override void Effect(EntityEffectBaseArgs args) + { + if (!args.EntityManager.TryGetComponent(args.TargetEntity, out CreamPiedComponent? creamPied)) return; + + args.EntityManager.System<CreamPieSystem>().SetCreamPied(args.TargetEntity, creamPied, false); + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/WearableReaction.cs b/Content.Server/EntityEffects/Effects/WearableReaction.cs similarity index 50% rename from Content.Server/Chemistry/ReagentEffects/WearableReaction.cs rename to Content.Server/EntityEffects/Effects/WearableReaction.cs index d9f8414995d..9655bbc073f 100644 --- a/Content.Server/Chemistry/ReagentEffects/WearableReaction.cs +++ b/Content.Server/EntityEffects/Effects/WearableReaction.cs @@ -1,17 +1,19 @@ using Content.Shared.Inventory; using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; -namespace Content.Server.Chemistry.ReagentEffects; +namespace Content.Server.EntityEffects.Effects.Effects; /// <summary> -/// A reaction effect that consumes the required amount of reagent and spawns PrototypeID in the -/// entity's Slot. Used to implement the water droplet effect for arachnids. +/// A reaction effect that spawns a PrototypeID in the entity's Slot, and attempts to consume the reagent if EntityEffectReagentArgs. +/// Used to implement the water droplet effect for arachnids. /// </summary> -public sealed partial class WearableReaction : ReagentEffect +public sealed partial class WearableReaction : EntityEffect { /// <summary> /// Minimum quantity of reagent required to trigger this effect. + /// Only used with EntityEffectReagentArgs. /// </summary> [DataField] public float AmountThreshold = 1f; @@ -30,13 +32,17 @@ public sealed partial class WearableReaction : ReagentEffect protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.Reagent == null || args.Quantity < AmountThreshold) - return; - // SpawnItemInSlot returns false if slot is already occupied - if (args.EntityManager.System<InventorySystem>().SpawnItemInSlot(args.SolutionEntity, Slot, PrototypeID)) - args.Source?.RemoveReagent(args.Reagent.ID, AmountThreshold); + if (args.EntityManager.System<InventorySystem>().SpawnItemInSlot(args.TargetEntity, Slot, PrototypeID)) + { + if (args is EntityEffectReagentArgs reagentArgs) + { + if (reagentArgs.Reagent == null || reagentArgs.Quantity < AmountThreshold) + return; + reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, AmountThreshold); + } + } } } diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index c62dece48fc..8000f2ef027 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -12,6 +12,7 @@ using Content.Server.GameTicking; using Content.Server.GhostKick; using Content.Server.GuideGenerator; +using Content.Server.Info; using Content.Server.IoC; using Content.Server.Players.JobWhitelist; using Content.Server.Maps; @@ -144,11 +145,13 @@ public override void PostInit() IoCManager.Resolve<RecipeManager>().Initialize(); IoCManager.Resolve<IAdminManager>().Initialize(); IoCManager.Resolve<IAfkManager>().Initialize(); + IoCManager.Resolve<RulesManager>().Initialize(); _euiManager.Initialize(); IoCManager.Resolve<IGameMapManager>().Initialize(); IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize(); IoCManager.Resolve<IBanManager>().Initialize(); + IoCManager.Resolve<IConnectionManager>().PostInit(); } } diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index d9b81c8e5a5..04c45662272 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -12,7 +12,6 @@ public static class IgnoredComponents "GuideHelp", "Clickable", "Icon", - "HandheldGPS", "CableVisualizer", "SolutionItemStatus", "UIFragment", diff --git a/Content.Server/Execution/ExecutionSystem.cs b/Content.Server/Execution/ExecutionSystem.cs deleted file mode 100644 index 453a05e8039..00000000000 --- a/Content.Server/Execution/ExecutionSystem.cs +++ /dev/null @@ -1,400 +0,0 @@ -using Content.Server.Interaction; -using Content.Server.Kitchen.Components; -using Content.Server.Weapons.Ranged.Systems; -using Content.Shared.ActionBlocker; -using Content.Shared.Damage; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.Execution; -using Content.Shared.Interaction.Components; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Popups; -using Content.Shared.Projectiles; -using Content.Shared.Verbs; -using Content.Shared.Weapons.Melee; -using Content.Shared.Weapons.Ranged; -using Content.Shared.Weapons.Ranged.Components; -using Content.Shared.Weapons.Ranged.Events; -using Content.Shared.Weapons.Ranged.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; - -namespace Content.Server.Execution; - -/// <summary> -/// Verb for violently murdering cuffed creatures. -/// </summary> -public sealed class ExecutionSystem : EntitySystem -{ - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly InteractionSystem _interactionSystem = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IComponentFactory _componentFactory = default!; - [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly GunSystem _gunSystem = default!; - - private const float MeleeExecutionTimeModifier = 5.0f; - private const float GunExecutionTime = 6.0f; - private const float DamageModifier = 9.0f; - - /// <inheritdoc/> - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<SharpComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionVerbsMelee); - SubscribeLocalEvent<GunComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionVerbsGun); - - SubscribeLocalEvent<SharpComponent, ExecutionDoAfterEvent>(OnDoafterMelee); - SubscribeLocalEvent<GunComponent, ExecutionDoAfterEvent>(OnDoafterGun); - } - - private void OnGetInteractionVerbsMelee( - EntityUid uid, - SharpComponent component, - GetVerbsEvent<UtilityVerb> args) - { - if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) - return; - - var attacker = args.User; - var weapon = args.Using!.Value; - var victim = args.Target; - - if (!CanExecuteWithMelee(weapon, victim, attacker)) - return; - - UtilityVerb verb = new() - { - Act = () => - { - TryStartMeleeExecutionDoafter(weapon, victim, attacker); - }, - Impact = LogImpact.High, - Text = Loc.GetString("execution-verb-name"), - Message = Loc.GetString("execution-verb-message"), - }; - - args.Verbs.Add(verb); - } - - private void OnGetInteractionVerbsGun( - EntityUid uid, - GunComponent component, - GetVerbsEvent<UtilityVerb> args) - { - if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) - return; - - var attacker = args.User; - var weapon = args.Using!.Value; - var victim = args.Target; - - if (!CanExecuteWithGun(weapon, victim, attacker)) - return; - - UtilityVerb verb = new() - { - Act = () => - { - TryStartGunExecutionDoafter(weapon, victim, attacker); - }, - Impact = LogImpact.High, - Text = Loc.GetString("execution-verb-name"), - Message = Loc.GetString("execution-verb-message"), - }; - - args.Verbs.Add(verb); - } - - private bool CanExecuteWithAny(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - // No point executing someone if they can't take damage - if (!TryComp<DamageableComponent>(victim, out var damage)) - return false; - - // You can't execute something that cannot die - if (!TryComp<MobStateComponent>(victim, out var mobState)) - return false; - - // You're not allowed to execute dead people (no fun allowed) - if (_mobStateSystem.IsDead(victim, mobState)) - return false; - - // You must be able to attack people to execute - if (!_actionBlockerSystem.CanAttack(attacker, victim)) - return false; - - // The victim must be incapacitated to be executed - if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null)) - return false; - - if (victim == attacker) - return false; // DeltaV - Fucking seriously? - - // All checks passed - return true; - } - - private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user) - { - if (!CanExecuteWithAny(weapon, victim, user)) return false; - - // We must be able to actually hurt people with the weapon - if (!TryComp<MeleeWeaponComponent>(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) - return false; - - return true; - } - - private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user) - { - if (!CanExecuteWithAny(weapon, victim, user)) return false; - - // We must be able to actually fire the gun - if (!TryComp<GunComponent>(weapon, out var gun) && _gunSystem.CanShoot(gun!)) - return false; - - return true; - } - - private void TryStartMeleeExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - if (!CanExecuteWithMelee(weapon, victim, attacker)) - return; - - var executionTime = (1.0f / Comp<MeleeWeaponComponent>(weapon).AttackRate) * MeleeExecutionTimeModifier; - - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - - var doAfter = - new DoAfterArgs(EntityManager, attacker, executionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) - { - BreakOnTargetMove = true, - BreakOnUserMove = true, - BreakOnDamage = true, - NeedHand = true - }; - - _doAfterSystem.TryStartDoAfter(doAfter); - } - - private void TryStartGunExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - if (!CanExecuteWithGun(weapon, victim, attacker)) - return; - - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - - var doAfter = - new DoAfterArgs(EntityManager, attacker, GunExecutionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) - { - BreakOnTargetMove = true, - BreakOnUserMove = true, - BreakOnDamage = true, - NeedHand = true - }; - - _doAfterSystem.TryStartDoAfter(doAfter); - } - - private bool OnDoafterChecks(EntityUid uid, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return false; - - if (!CanExecuteWithAny(args.Used.Value, args.Target.Value, uid)) - return false; - - // All checks passed - return true; - } - - private void OnDoafterMelee(EntityUid uid, SharpComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return; - - var attacker = args.User; - var victim = args.Target!.Value; - var weapon = args.Used!.Value; - - if (!CanExecuteWithMelee(weapon, victim, attacker)) return; - - if (!TryComp<MeleeWeaponComponent>(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) - return; - - _damageableSystem.TryChangeDamage(victim, melee.Damage * DamageModifier, true, origin: attacker); - _audioSystem.PlayEntity(melee.SoundHit, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - } - - // TODO: This repeats a lot of the code of the serverside GunSystem, make it not do that - private void OnDoafterGun(EntityUid uid, GunComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return; - - var attacker = args.User; - var weapon = args.Used!.Value; - var victim = args.Target!.Value; - - if (!CanExecuteWithGun(weapon, victim, attacker)) return; - - // Check if any systems want to block our shot - var prevention = new ShotAttemptedEvent - { - User = attacker, - Used = new Entity<GunComponent>(uid, component) - }; - - RaiseLocalEvent(weapon, ref prevention); - if (prevention.Cancelled) - return; - - RaiseLocalEvent(attacker, ref prevention); - if (prevention.Cancelled) - return; - - // Not sure what this is for but gunsystem uses it so ehhh - var attemptEv = new AttemptShootEvent(attacker, null); - RaiseLocalEvent(weapon, ref attemptEv); - - if (attemptEv.Cancelled) - { - if (attemptEv.Message != null) - { - _popupSystem.PopupClient(attemptEv.Message, weapon, attacker); - return; - } - } - - // Take some ammunition for the shot (one bullet) - var fromCoordinates = Transform(attacker).Coordinates; - var ev = new TakeAmmoEvent(1, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, attacker); - RaiseLocalEvent(weapon, ev); - - // Check if there's any ammo left - if (ev.Ammo.Count <= 0) - { - _audioSystem.PlayEntity(component.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - ShowExecutionPopup("execution-popup-gun-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim, weapon); - return; - } - - // Information about the ammo like damage - DamageSpecifier damage = new DamageSpecifier(); - - // Get some information from IShootable - var ammoUid = ev.Ammo[0].Entity; - switch (ev.Ammo[0].Shootable) - { - case CartridgeAmmoComponent cartridge: - // Get the damage value - var prototype = _prototypeManager.Index<EntityPrototype>(cartridge.Prototype); - prototype.TryGetComponent<ProjectileComponent>(out var projectileA, _componentFactory); // sloth forgive me - if (projectileA != null) - { - damage = projectileA.Damage * cartridge.Count; - } - - // Expend the cartridge - cartridge.Spent = true; - _appearanceSystem.SetData(ammoUid!.Value, AmmoVisuals.Spent, true); - Dirty(ammoUid.Value, cartridge); - - break; - - case AmmoComponent newAmmo: - TryComp<ProjectileComponent>(ammoUid, out var projectileB); - if (projectileB != null) - { - damage = projectileB.Damage; - } - Del(ammoUid); - break; - - case HitscanPrototype hitscan: - damage = hitscan.Damage!; - break; - - default: - throw new ArgumentOutOfRangeException(); - } - - // Clumsy people have a chance to shoot themselves - if (TryComp<ClumsyComponent>(attacker, out var clumsy) && component.ClumsyProof == false) - { - if (_interactionSystem.TryRollClumsy(attacker, 0.33333333f, clumsy)) - { - ShowExecutionPopup("execution-popup-gun-clumsy-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-clumsy-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - - // You shoot yourself with the gun (no damage multiplier) - _damageableSystem.TryChangeDamage(attacker, damage, origin: attacker); - _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - return; - } - } - - // Gun successfully fired, deal damage - _damageableSystem.TryChangeDamage(victim, damage * DamageModifier, true, origin: attacker); - _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, false, AudioParams.Default); - - // Popups - if (attacker != victim) - { - ShowExecutionPopup("execution-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("suicide-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.LargeCaution, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); - } - } - - private void ShowExecutionPopup(string locString, Filter filter, PopupType type, - EntityUid attacker, EntityUid victim, EntityUid weapon) - { - _popupSystem.PopupEntity(Loc.GetString( - locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), - attacker, filter, true, type); - } -} diff --git a/Content.Server/Explosion/Components/AutomatedTimerComponent.cs b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs index 7019c08d43d..c01aeb91e55 100644 --- a/Content.Server/Explosion/Components/AutomatedTimerComponent.cs +++ b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs @@ -1,7 +1,7 @@ namespace Content.Server.Explosion.Components; /// <summary> -/// Disallows starting the timer by hand, must be stuck or triggered by a system. +/// Disallows starting the timer by hand, must be stuck or triggered by a system using <c>StartTimer</c>. /// </summary> [RegisterComponent] public sealed partial class AutomatedTimerComponent : Component diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs index 19f33e3fdde..7dac935da5e 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs @@ -17,11 +17,15 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; namespace Content.Server.Explosion.EntitySystems; public sealed partial class ExplosionSystem { + [Dependency] private readonly FlammableSystem _flammableSystem = default!; + /// <summary> /// Used to limit explosion processing time. See <see cref="MaxProcessingTime"/>. /// </summary> @@ -95,7 +99,7 @@ public override void Update(float frameTime) { // EXPLOSION TODO allow explosion spawning to be interrupted by time limit. In the meantime, ensure that // there is at-least 1ms of time left before creating a new explosion - if (MathF.Max(MaxProcessingTime - 1, 0.1f) < Stopwatch.Elapsed.TotalMilliseconds) + if (MathF.Max(MaxProcessingTime - 1, 0.1f) < Stopwatch.Elapsed.TotalMilliseconds) break; if (!_explosionQueue.TryDequeue(out var queued)) @@ -129,11 +133,11 @@ public override void Update(float frameTime) try { #endif - var processed = _activeExplosion.Process(tilesRemaining); - tilesRemaining -= processed; + var processed = _activeExplosion.Process(tilesRemaining); + tilesRemaining -= processed; - // has the explosion finished processing? - if (_activeExplosion.FinishedProcessing) + // has the explosion finished processing? + if (_activeExplosion.FinishedProcessing) { var comp = EnsureComp<TimedDespawnComponent>(_activeExplosion.VisualEnt); comp.Lifetime = _cfg.GetCVar(CCVars.ExplosionPersistence); @@ -202,7 +206,8 @@ internal bool ExplodeTile(BroadphaseComponent lookup, DamageSpecifier damage, MapCoordinates epicenter, HashSet<EntityUid> processed, - string id) + string id, + float? fireStacks) { var size = grid.Comp.TileSize; var gridBox = new Box2(tile * size, (tile + 1) * size); @@ -221,7 +226,7 @@ internal bool ExplodeTile(BroadphaseComponent lookup, // process those entities foreach (var (uid, xform) in list) { - ProcessEntity(uid, epicenter, damage, throwForce, id, xform); + ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks); } // process anchored entities @@ -231,7 +236,7 @@ internal bool ExplodeTile(BroadphaseComponent lookup, foreach (var entity in _anchored) { processed.Add(entity); - ProcessEntity(entity, epicenter, damage, throwForce, id, null); + ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks); } // Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for @@ -267,7 +272,7 @@ internal bool ExplodeTile(BroadphaseComponent lookup, { // Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but // they should handle their own damage pass-through, with their own damage reduction calculation. - ProcessEntity(uid, epicenter, null, throwForce, id, xform); + ProcessEntity(uid, epicenter, null, throwForce, id, xform, null); } return !tileBlocked; @@ -302,7 +307,8 @@ internal void ExplodeSpace(BroadphaseComponent lookup, DamageSpecifier damage, MapCoordinates epicenter, HashSet<EntityUid> processed, - string id) + string id, + float? fireStacks) { var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize)); var worldBox = spaceMatrix.TransformBox(gridBox); @@ -318,7 +324,7 @@ internal void ExplodeSpace(BroadphaseComponent lookup, foreach (var (uid, xform) in state.Item1) { processed.Add(uid); - ProcessEntity(uid, epicenter, damage, throwForce, id, xform); + ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks); } if (throwForce <= 0) @@ -332,7 +338,7 @@ internal void ExplodeSpace(BroadphaseComponent lookup, foreach (var (uid, xform) in list) { - ProcessEntity(uid, epicenter, null, throwForce, id, xform); + ProcessEntity(uid, epicenter, null, throwForce, id, xform, fireStacks); } } @@ -392,7 +398,7 @@ private void GetEntitiesToDamage(EntityUid uid, DamageSpecifier originalDamage, // don't raise BeforeExplodeEvent if the entity is completely immune to explosions var thisDamage = GetDamage(uid, prototype, originalDamage); - if (!thisDamage.Any()) + if (thisDamage.Empty) return; _toDamage.Add((uid, thisDamage)); @@ -429,7 +435,8 @@ private void ProcessEntity( DamageSpecifier? originalDamage, float throwForce, string id, - TransformComponent? xform) + TransformComponent? xform, + float? fireStacksOnIgnite) { if (originalDamage != null) { @@ -438,6 +445,17 @@ private void ProcessEntity( { // TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin. _damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true); + + } + } + + // ignite + if (fireStacksOnIgnite != null) + { + if (_flammableQuery.TryGetComponent(uid, out var flammable)) + { + flammable.FireStacks += fireStacksOnIgnite.Value; + _flammableSystem.Ignite(uid, uid, flammable); } } @@ -706,14 +724,14 @@ private bool TryGetNextTileEnumerator() { _currentIntensity = _tileSetIntensity[CurrentIteration]; - #if DEBUG +#if DEBUG if (_expectedDamage != null) { // Check that explosion processing hasn't somehow accidentally mutated the damage set. DebugTools.Assert(_expectedDamage.Equals(_currentDamage)); _expectedDamage = ExplosionType.DamagePerIntensity * _currentIntensity; } - #endif +#endif _currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity; @@ -812,7 +830,8 @@ public int Process(int processingTarget) _currentDamage, Epicenter, ProcessedEntities, - ExplosionType.ID); + ExplosionType.ID, + ExplosionType.FireStacks); // If the floor is not blocked by some dense object, damage the floor tiles. if (canDamageFloor) @@ -829,7 +848,8 @@ public int Process(int processingTarget) _currentDamage, Epicenter, ProcessedEntities, - ExplosionType.ID); + ExplosionType.ID, + ExplosionType.FireStacks); } if (!MoveNext()) diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs index 71f3025748a..8734c054d64 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs @@ -52,7 +52,7 @@ public sealed partial class ExplosionSystem : EntitySystem [Dependency] private readonly SharedMapSystem _map = default!; private EntityQuery<TransformComponent> _transformQuery; - private EntityQuery<DamageableComponent> _damageQuery; + private EntityQuery<FlammableComponent> _flammableQuery; private EntityQuery<PhysicsComponent> _physicsQuery; private EntityQuery<ProjectileComponent> _projectileQuery; @@ -104,7 +104,7 @@ public override void Initialize() InitVisuals(); _transformQuery = GetEntityQuery<TransformComponent>(); - _damageQuery = GetEntityQuery<DamageableComponent>(); + _flammableQuery = GetEntityQuery<FlammableComponent>(); _physicsQuery = GetEntityQuery<PhysicsComponent>(); _projectileQuery = GetEntityQuery<ProjectileComponent>(); } diff --git a/Content.Server/Explosion/EntitySystems/SmokeOnTriggerSystem.cs b/Content.Server/Explosion/EntitySystems/SmokeOnTriggerSystem.cs index 17ca9723569..f958373ac74 100644 --- a/Content.Server/Explosion/EntitySystems/SmokeOnTriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/SmokeOnTriggerSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Coordinates.Helpers; using Content.Shared.Maps; +using Robust.Server.GameObjects; using Robust.Shared.Map; namespace Content.Server.Explosion.EntitySystems; @@ -15,6 +16,7 @@ public sealed class SmokeOnTriggerSystem : SharedSmokeOnTriggerSystem { [Dependency] private readonly IMapManager _mapMan = default!; [Dependency] private readonly SmokeSystem _smoke = default!; + [Dependency] private readonly TransformSystem _transform = default!; public override void Initialize() { @@ -26,14 +28,15 @@ public override void Initialize() private void OnTrigger(EntityUid uid, SmokeOnTriggerComponent comp, TriggerEvent args) { var xform = Transform(uid); - if (!_mapMan.TryFindGridAt(xform.MapPosition, out _, out var grid) || + var mapCoords = _transform.GetMapCoordinates(uid, xform); + if (!_mapMan.TryFindGridAt(mapCoords, out _, out var grid) || !grid.TryGetTileRef(xform.Coordinates, out var tileRef) || tileRef.Tile.IsSpace()) { return; } - var coords = grid.MapToGrid(xform.MapPosition); + var coords = grid.MapToGrid(mapCoords); var ent = Spawn(comp.SmokePrototype, coords.SnapToGrid()); if (!TryComp<SmokeComponent>(ent, out var smoke)) { diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs index 45198662ec3..ccd2a6e3df0 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs @@ -38,16 +38,20 @@ private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent c Trigger(uid); } + /// <summary> + /// Checks if the user has any implants that prevent suicide to avoid some cheesy strategies + /// Prevents suicide by handling the event without killing the user + /// </summary> private void OnSuicide(EntityUid uid, TriggerOnMobstateChangeComponent component, SuicideEvent args) { if (args.Handled) return; - if (component.PreventSuicide) - { - _popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim); - args.BlockSuicideAttempt(component.PreventSuicide); - } + if (!component.PreventSuicide) + return; + + _popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim); + args.Handled = true; } private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<SuicideEvent> args) diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index 786d29d94a9..12614dc6367 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -26,13 +26,7 @@ private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, Entity if (!component.StartOnStick) return; - HandleTimerTrigger( - uid, - args.User, - component.Delay, - component.BeepInterval, - component.InitialBeepDelay, - component.BeepSound); + StartTimer((uid, component), args.User); } private void OnExamined(EntityUid uid, OnUseTimerTriggerComponent component, ExaminedEvent args) @@ -54,14 +48,7 @@ private void OnGetAltVerbs(EntityUid uid, OnUseTimerTriggerComponent component, args.Verbs.Add(new AlternativeVerb() { Text = Loc.GetString("verb-start-detonation"), - Act = () => HandleTimerTrigger( - uid, - args.User, - component.Delay, - component.BeepInterval, - component.InitialBeepDelay, - component.BeepSound - ), + Act = () => StartTimer((uid, component), args.User), Priority = 2 }); } @@ -173,13 +160,7 @@ private void OnTimerUse(EntityUid uid, OnUseTimerTriggerComponent component, Use _popupSystem.PopupEntity(Loc.GetString("trigger-activated", ("device", uid)), args.User, args.User); - HandleTimerTrigger( - uid, - args.User, - component.Delay, - component.BeepInterval, - component.InitialBeepDelay, - component.BeepSound); + StartTimer((uid, component), args.User); args.Handled = true; } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 999a85da5aa..b3c071206e3 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -265,6 +265,18 @@ public void TryDelay(EntityUid uid, float amount, ActiveTimerTriggerComponent? c comp.TimeRemaining += amount; } + /// <summary> + /// Start the timer for triggering the device. + /// </summary> + public void StartTimer(Entity<OnUseTimerTriggerComponent?> ent, EntityUid? user) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + var comp = ent.Comp; + HandleTimerTrigger(ent, user, comp.Delay, comp.BeepInterval, comp.InitialBeepDelay, comp.BeepSound); + } + public void HandleTimerTrigger(EntityUid uid, EntityUid? user, float delay, float beepInterval, float? initialBeepDelay, SoundSpecifier? beepSound) { if (delay <= 0) diff --git a/Content.Server/Fax/FaxSystem.cs b/Content.Server/Fax/FaxSystem.cs index e86dbca4a13..229054dce94 100644 --- a/Content.Server/Fax/FaxSystem.cs +++ b/Content.Server/Fax/FaxSystem.cs @@ -29,6 +29,8 @@ using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Content.Shared.Power; +using Content.Shared.NameModifier.Components; namespace Content.Server.Fax; @@ -464,10 +466,11 @@ public void Copy(EntityUid uid, FaxMachineComponent? component, FaxCopyMessage a return; TryComp<LabelComponent>(sendEntity, out var labelComponent); + TryComp<NameModifierComponent>(sendEntity, out var nameMod); // TODO: See comment in 'Send()' about not being able to copy whole entities var printout = new FaxPrintout(paper.Content, - labelComponent?.OriginalName ?? metadata.EntityName, + nameMod?.BaseName ?? metadata.EntityName, labelComponent?.CurrentLabel, metadata.EntityPrototype?.ID ?? DefaultPaperPrototypeId, paper.StampState, @@ -510,12 +513,14 @@ public void Send(EntityUid uid, FaxMachineComponent? component, FaxSendMessage a !TryComp<PaperComponent>(sendEntity, out var paper)) return; + TryComp<NameModifierComponent>(sendEntity, out var nameMod); + TryComp<LabelComponent>(sendEntity, out var labelComponent); var payload = new NetworkPayload() { { DeviceNetworkConstants.Command, FaxConstants.FaxPrintCommand }, - { FaxConstants.FaxPaperNameData, labelComponent?.OriginalName ?? metadata.EntityName }, + { FaxConstants.FaxPaperNameData, nameMod?.BaseName ?? metadata.EntityName }, { FaxConstants.FaxPaperLabelData, labelComponent?.CurrentLabel }, { FaxConstants.FaxPaperContentData, paper.Content }, }; diff --git a/Content.Server/Flight/FlightSystem.cs b/Content.Server/Flight/FlightSystem.cs index 39321b1e66c..4493967fe9e 100644 --- a/Content.Server/Flight/FlightSystem.cs +++ b/Content.Server/Flight/FlightSystem.cs @@ -67,8 +67,7 @@ private void OnToggleFlight(EntityUid uid, FlightComponent component, ToggleFlig new FlightDoAfterEvent(), uid, target: uid) { BlockDuplicate = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = true }; @@ -173,4 +172,4 @@ private void OnSleep(EntityUid uid, FlightComponent component, ref SleepStateCha Dirty(uid, stamina); } #endregion -} \ No newline at end of file +} diff --git a/Content.Server/Fluids/EntitySystems/DrainSystem.cs b/Content.Server/Fluids/EntitySystems/DrainSystem.cs index 27ad2178f93..b79685d83b6 100644 --- a/Content.Server/Fluids/EntitySystems/DrainSystem.cs +++ b/Content.Server/Fluids/EntitySystems/DrainSystem.cs @@ -13,6 +13,7 @@ using Content.Shared.Interaction; using Content.Shared.Tag; using Content.Shared.Verbs; +using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Collections; using Robust.Shared.Prototypes; @@ -31,6 +32,7 @@ public sealed class DrainSystem : SharedDrainSystem [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; [Dependency] private readonly PuddleSystem _puddleSystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -161,7 +163,7 @@ public override void Update(float frameTime) puddles.Clear(); - foreach (var entity in _lookup.GetEntitiesInRange(xform.MapPosition, drain.Range)) + foreach (var entity in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(uid, xform), drain.Range)) { // No InRangeUnobstructed because there's no collision group that fits right now // and these are placed by mappers and not buildable/movable so shouldnt really be a problem... @@ -245,9 +247,8 @@ private void OnInteract(Entity<DrainComponent> entity, ref AfterInteractUsingEve var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.UnclogDuration, new DrainDoAfterEvent(), entity, args.Target, args.Used) { - BreakOnTargetMove = true, - BreakOnUserMove = true, BreakOnDamage = true, + BreakOnMove = true, BreakOnHandChange = true }; diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index d02dd44e81f..6842349828d 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -2,6 +2,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; +using Content.Shared.Chemistry; using Content.Shared.Chemistry.Reagent; using Content.Shared.Clothing; using Content.Shared.CombatMode.Pacification; @@ -27,8 +28,6 @@ protected override void InitializeSpillable() SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand); // Openable handles the event if it's closed SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); - SubscribeLocalEvent<SpillableComponent, ClothingGotEquippedEvent>(OnGotEquipped); - SubscribeLocalEvent<SpillableComponent, ClothingGotUnequippedEvent>(OnGotUnequipped); SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow); SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow); @@ -72,17 +71,22 @@ private void SplashOnMeleeHit(Entity<SpillableComponent> entity, ref MeleeHitEve return; args.Handled = true; + + // First update the hit count so anything that is not reactive wont count towards the total! + foreach (var hit in args.HitEntities) + { + if (!HasComp<ReactiveComponent>(hit)) + hitCount -= 1; + } + foreach (var hit in args.HitEntities) { if (!HasComp<ReactiveComponent>(hit)) - { - hitCount -= 1; // so we don't undershoot solution calculation for actual reactive entities continue; - } var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount); - _adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(entity.Owner):entity} onto {ToPrettyString(hit):target}"); + _adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(entity.Owner):entity} onto {ToPrettyString(hit):target}"); _reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch); _popups.PopupEntity( @@ -97,33 +101,6 @@ private void SplashOnMeleeHit(Entity<SpillableComponent> entity, ref MeleeHitEve } } - private void OnGotEquipped(Entity<SpillableComponent> entity, ref ClothingGotEquippedEvent args) - { - if (!entity.Comp.SpillWorn) - return; - - if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) - return; - - // block access to the solution while worn - AddComp<BlockSolutionAccessComponent>(entity); - - if (solution.Volume == 0) - return; - - // spill all solution on the player - var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume); - TrySplashSpillAt(entity.Owner, Transform(args.Wearer).Coordinates, drainedSolution, out _); - } - - private void OnGotUnequipped(Entity<SpillableComponent> entity, ref ClothingGotUnequippedEvent args) - { - if (!entity.Comp.SpillWorn) - return; - - RemCompDeferred<BlockSolutionAccessComponent>(entity); - } - private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args) { if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) @@ -138,7 +115,7 @@ private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args) if (args.User != null) { _adminLogger.Add(LogType.Landed, - $"{ToPrettyString(entity.Owner):entity} spilled a solution {SolutionContainerSystem.ToPrettyString(solution):solution} on landing"); + $"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing"); } var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume); diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs index ae170842a0c..62d04db492f 100644 --- a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs +++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs @@ -2,7 +2,7 @@ using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Chemistry.ReactionEffects; +using Content.Server.EntityEffects.Effects; using Content.Server.Spreader; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; diff --git a/Content.Server/Forensics/Systems/ForensicPadSystem.cs b/Content.Server/Forensics/Systems/ForensicPadSystem.cs index ad39817f8b0..42512cb1fdc 100644 --- a/Content.Server/Forensics/Systems/ForensicPadSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicPadSystem.cs @@ -83,9 +83,8 @@ private void StartScan(EntityUid used, EntityUid user, EntityUid target, Forensi var doAfterEventArgs = new DoAfterArgs(EntityManager, user, pad.ScanDelay, ev, used, target: target, used: used) { - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true + NeedHand = true, + BreakOnMove = true, }; _doAfterSystem.TryStartDoAfter(doAfterEventArgs); diff --git a/Content.Server/Forensics/Systems/ForensicScannerSystem.cs b/Content.Server/Forensics/Systems/ForensicScannerSystem.cs index e83cde7456c..5e2a562577d 100644 --- a/Content.Server/Forensics/Systems/ForensicScannerSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicScannerSystem.cs @@ -94,8 +94,7 @@ private void StartScan(EntityUid uid, ForensicScannerComponent component, Entity { _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.ScanDelay, new ForensicScannerDoAfterEvent(), uid, target: target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }); } diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs index 8f91ec41e80..fe1844a3ed5 100644 --- a/Content.Server/Forensics/Systems/ForensicsSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs @@ -148,7 +148,7 @@ private void OnAfterInteract(EntityUid uid, CleansForensicsComponent component, BreakOnHandChange = true, NeedHand = true, BreakOnDamage = true, - BreakOnTargetMove = true, + BreakOnMove = true, MovementThreshold = 0.01f, DistanceThreshold = forensicsComp.CleanDistance, }; diff --git a/Content.Server/Forensics/Systems/ScentTrackerSystem.cs b/Content.Server/Forensics/Systems/ScentTrackerSystem.cs index e3542eaba47..14373856121 100644 --- a/Content.Server/Forensics/Systems/ScentTrackerSystem.cs +++ b/Content.Server/Forensics/Systems/ScentTrackerSystem.cs @@ -49,9 +49,9 @@ private void AttemptTrackScent(EntityUid user, EntityUid target, ScentTrackerCom var doAfterEventArgs = new DoAfterArgs(EntityManager, user, component.SniffDelay, new ScentTrackerDoAfterEvent(), user, target: target) { - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, - BreakOnTargetMove = true + }; _popupSystem.PopupEntity(Loc.GetString("start-tracking-scent", ("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))), user); diff --git a/Content.Server/Friends/Components/PettableFriendComponent.cs b/Content.Server/Friends/Components/PettableFriendComponent.cs deleted file mode 100644 index fe68029a663..00000000000 --- a/Content.Server/Friends/Components/PettableFriendComponent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Server.Friends.Systems; - -namespace Content.Server.Friends.Components; - -/// <summary> -/// Pet something to become friends with it (use in hand, press Z) -/// Uses FactionExceptionComponent behind the scenes -/// </summary> -[RegisterComponent, Access(typeof(PettableFriendSystem))] -public sealed partial class PettableFriendComponent : Component -{ - /// <summary> - /// Localized popup sent when petting for the first time - /// </summary> - [DataField("successString", required: true), ViewVariables(VVAccess.ReadWrite)] - public string SuccessString = string.Empty; - - /// <summary> - /// Localized popup sent when petting multiple times - /// </summary> - [DataField("failureString", required: true), ViewVariables(VVAccess.ReadWrite)] - public string FailureString = string.Empty; -} diff --git a/Content.Server/Friends/Systems/PettableFriendSystem.cs b/Content.Server/Friends/Systems/PettableFriendSystem.cs deleted file mode 100644 index 8f70c843e70..00000000000 --- a/Content.Server/Friends/Systems/PettableFriendSystem.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Content.Server.Friends.Components; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Interaction.Events; -using Content.Shared.Popups; - -namespace Content.Server.Friends.Systems; - -public sealed class PettableFriendSystem : EntitySystem -{ - [Dependency] private readonly NpcFactionSystem _factionException = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<PettableFriendComponent, UseInHandEvent>(OnUseInHand); - SubscribeLocalEvent<PettableFriendComponent, GotRehydratedEvent>(OnRehydrated); - } - - private void OnUseInHand(EntityUid uid, PettableFriendComponent comp, UseInHandEvent args) - { - var user = args.User; - if (args.Handled || !TryComp<FactionExceptionComponent>(uid, out var factionException)) - return; - - if (_factionException.IsIgnored(uid, user, factionException)) - { - _popup.PopupEntity(Loc.GetString(comp.FailureString, ("target", uid)), user, user); - return; - } - - // you have made a new friend :) - _popup.PopupEntity(Loc.GetString(comp.SuccessString, ("target", uid)), user, user); - _factionException.IgnoreEntity(uid, user, factionException); - args.Handled = true; - } - - private void OnRehydrated(EntityUid uid, PettableFriendComponent _, ref GotRehydratedEvent args) - { - // can only pet before hydrating, after that the fish cannot be negotiated with - if (!TryComp<FactionExceptionComponent>(uid, out var comp)) - return; - - var targetComp = AddComp<FactionExceptionComponent>(args.Target); - _factionException.IgnoreEntities(args.Target, comp.Ignored, targetComp); - } -} diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs index 82ef8c6012b..42690c20b73 100644 --- a/Content.Server/GameTicking/GameTicker.Lobby.cs +++ b/Content.Server/GameTicking/GameTicker.Lobby.cs @@ -179,5 +179,11 @@ public void ToggleReady(ICommonSession player, bool ready) // update server info to reflect new ready count UpdateInfoText(); } + + public bool UserHasJoinedGame(ICommonSession session) + => UserHasJoinedGame(session.UserId); + + public bool UserHasJoinedGame(NetUserId userId) + => PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame; } -} +} \ No newline at end of file diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs index 54e7f1bf920..a7f7a7fa718 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -20,14 +20,8 @@ namespace Content.Server.GameTicking [UsedImplicitly] public sealed partial class GameTicker { - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IServerDbManager _dbManager = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - - private void InitializePlayer() - { + private void InitializePlayer() => _playerManager.PlayerStatusChanged += PlayerStatusChanged; - } private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs args) { @@ -61,7 +55,7 @@ private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs ar session.Data.ContentDataUncast = data; } - var record = await _dbManager.GetPlayerRecordByUserId(args.Session.UserId); + var record = await _db.GetPlayerRecordByUserId(args.Session.UserId); var firstConnection = record != null && Math.Abs((record.FirstSeenTime - record.LastSeenTime).TotalMinutes) < 1; @@ -72,7 +66,7 @@ private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs ar RaiseNetworkEvent(GetConnectionStatusMsg(), session.Channel); if (firstConnection && _configurationManager.GetCVar(CCVars.AdminNewPlayerJoinSound)) - _audioSystem.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"), + _audio.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"), Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false, audioParams: new AudioParams { Volume = -5f }); @@ -145,13 +139,16 @@ private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs ar async void SpawnWaitDb() { try - {await _userDb.WaitLoadComplete(session);} + { + await _userDb.WaitLoadComplete(session); + } catch (OperationCanceledException) { // Bail, user must've disconnected or something. Log.Debug($"Database load cancelled while waiting to spawn {session}"); return; } + SpawnPlayer(session, EntityUid.Invalid); } diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 889a8a5a2b6..8b4798e7966 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -186,9 +186,6 @@ public int ReadyPlayerCount() if (!_playerManager.TryGetSessionById(userId, out _)) continue; - if (_banManager.GetRoleBans(userId) == null) - continue; - total++; } @@ -232,11 +229,7 @@ public void StartRound(bool force = false) #if DEBUG DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??"); #endif - if (_banManager.GetRoleBans(userId) == null) - { - Logger.ErrorS("RoleBans", $"Role bans for player {session} {userId} have not been loaded yet."); - continue; - } + readyPlayers.Add(session); HumanoidCharacterProfile profile; if (_prefsManager.TryGetCachedPreferences(userId, out var preferences)) @@ -336,8 +329,23 @@ public void EndRound(string text = "") RunLevel = GameRunLevel.PostRound; - ShowRoundEndScoreboard(text); - SendRoundEndDiscordMessage(); + try + { + ShowRoundEndScoreboard(text); + } + catch (Exception e) + { + Log.Error($"Error while showing round end scoreboard: {e}"); + } + + try + { + SendRoundEndDiscordMessage(); + } + catch (Exception e) + { + Log.Error($"Error while sending round end Discord message: {e}"); + } } public void ShowRoundEndScoreboard(string text = "") @@ -570,19 +578,15 @@ private void ResettingCleanup() DisallowLateJoin = false; _playerGameStatuses.Clear(); + foreach (var session in _playerManager.Sessions) - { _playerGameStatuses[session.UserId] = LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay; - } } public bool DelayStart(TimeSpan time) { if (_runLevel != GameRunLevel.PreRoundLobby) - { return false; - } - _roundStartTime += time; RaiseNetworkEvent(new TickerLobbyCountdownEvent(_roundStartTime, Paused)); diff --git a/Content.Server/GameTicking/GameTicker.StatusShell.cs b/Content.Server/GameTicking/GameTicker.StatusShell.cs index 53c94345995..c64a06eabbe 100644 --- a/Content.Server/GameTicking/GameTicker.StatusShell.cs +++ b/Content.Server/GameTicking/GameTicker.StatusShell.cs @@ -49,6 +49,12 @@ private void GetStatusResponse(JsonNode jObject) jObject["players"] = _joinQueue.ActualPlayersCount; jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers); jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled); + + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + + jObject["baby_jail"] = _cfg.GetCVar(CCVars.BabyJailEnabled); jObject["run_level"] = (int) _runLevel; if (preset != null) jObject["preset"] = Loc.GetString(preset.ModeTitle); diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index fa23312268f..33cb7b22f03 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -19,6 +19,7 @@ using Robust.Server; using Robust.Server.GameObjects; using Robust.Server.GameStates; +using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; @@ -71,6 +72,7 @@ public sealed partial class GameTicker : SharedGameTicker [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly ServerDbEntryManager _dbEntryManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; [ViewVariables] private bool _initialized; [ViewVariables] private bool _postInitialized; diff --git a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs new file mode 100644 index 00000000000..c640c912a5f --- /dev/null +++ b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs @@ -0,0 +1,39 @@ +using Content.Server.Antag; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Humanoid; +using Content.Server.Preferences.Managers; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules; + +public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfileRuleComponent> +{ + [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IServerPreferencesManager _prefs = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<AntagLoadProfileRuleComponent, AntagSelectEntityEvent>(OnSelectEntity); + } + + private void OnSelectEntity(Entity<AntagLoadProfileRuleComponent> ent, ref AntagSelectEntityEvent args) + { + if (args.Handled) + return; + + var profile = args.Session != null + ? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile + : HumanoidCharacterProfile.RandomWithSpecies(); + if (profile?.Species is not {} speciesId || !_proto.TryIndex<SpeciesPrototype>(speciesId, out var species)) + species = _proto.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies); + + args.Entity = Spawn(species.Prototype); + _humanoid.LoadProfile(args.Entity.Value, profile!); + } +} diff --git a/Content.Server/GameTicking/Rules/Components/AntagLoadProfileRuleCOmponent.cs b/Content.Server/GameTicking/Rules/Components/AntagLoadProfileRuleCOmponent.cs new file mode 100644 index 00000000000..5e58fd14fc0 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/AntagLoadProfileRuleCOmponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// <summary> +/// Makes this rules antags spawn a humanoid, either from the player's profile or a random one. +/// </summary> +[RegisterComponent] +public sealed partial class AntagLoadProfileRuleComponent : Component; diff --git a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs deleted file mode 100644 index fa352eb320b..00000000000 --- a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Server.Ninja.Systems; -using Content.Shared.Communications; -using Content.Shared.Random; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Server.GameTicking.Rules.Components; - -/// <summary> -/// Stores some configuration used by the ninja system. -/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent"/>. -/// </summary> -[RegisterComponent, Access(typeof(SpaceNinjaSystem))] -public sealed partial class NinjaRuleComponent : Component -{ - /// <summary> - /// List of threats that can be called in. Copied onto <see cref="CommsHackerComponent"/> when gloves are enabled. - /// </summary> - [DataField(required: true)] - public ProtoId<WeightedRandomPrototype> Threats = string.Empty; - - /// <summary> - /// Sound played when making the player a ninja via antag control or ghost role - /// </summary> - [DataField] - public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/ninja_greeting.ogg"); -} diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 7862b38e592..44bfbdffe0a 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -1,7 +1,7 @@ using Content.Server.Maps; -using Content.Server.NPC.Components; using Content.Server.RoundEnd; using Content.Shared.Dataset; +using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; using Robust.Shared.Audio; using Robust.Shared.Prototypes; diff --git a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs index 01a078625ae..6ad1e177755 100644 --- a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs @@ -8,23 +8,4 @@ namespace Content.Server.GameTicking.Rules.Components; /// Stores data for <see cref="ThiefRuleSystem"/>. /// </summary> [RegisterComponent, Access(typeof(ThiefRuleSystem))] -public sealed partial class ThiefRuleComponent : Component -{ - [DataField] - public ProtoId<WeightedRandomPrototype> BigObjectiveGroup = "ThiefBigObjectiveGroups"; - - [DataField] - public ProtoId<WeightedRandomPrototype> SmallObjectiveGroup = "ThiefObjectiveGroups"; - - [DataField] - public ProtoId<WeightedRandomPrototype> EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups"; - - [DataField] - public float BigObjectiveChance = 0.7f; - - [DataField] - public float MaxObjectiveDifficulty = 2.5f; - - [DataField] - public int MaxStealObjectives = 10; -} +public sealed partial class ThiefRuleComponent : Component; diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs index 47a4adeaf34..623da7a1745 100644 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -1,5 +1,6 @@ -using Content.Server.NPC.Components; using Content.Shared.Dataset; +using Content.Shared.NPC.Prototypes; +using Content.Shared.FixedPoint; using Content.Shared.Random; using Content.Shared.Roles; using Robust.Shared.Audio; @@ -22,9 +23,6 @@ public sealed partial class TraitorRuleComponent : Component [DataField] public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate"; - [DataField] - public ProtoId<WeightedRandomPrototype> ObjectiveGroup = "TraitorObjectiveGroups"; - [DataField] public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives"; @@ -34,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component [DataField] public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations"; + /// <summary> + /// Give this traitor an Uplink on spawn. + /// </summary> + [DataField] + public bool GiveUplink = true; + + /// <summary> + /// Give this traitor the codewords. + /// </summary> + [DataField] + public bool GiveCodewords = true; + + /// <summary> + /// Give this traitor a briefing in chat. + /// </summary> + [DataField] + public bool GiveBriefing = true; + public int TotalTraitors => TraitorMinds.Count; public string[] Codewords = new string[3]; @@ -71,8 +87,5 @@ public enum SelectionState /// The amount of TC traitors start with. /// </summary> [DataField] - public int StartingBalance = 20; - - [DataField] - public int MaxDifficulty = 5; + public FixedPoint2 StartingBalance = 20; } diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index d03d040261a..aa9140db893 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.GameTicking.Components; using Content.Shared.Points; using Content.Shared.Storage; +using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Utility; @@ -25,6 +26,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen [Dependency] private readonly RespawnRuleSystem _respawn = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + [Dependency] private readonly TransformSystem _transform = default!; public override void Initialize() { @@ -97,7 +99,7 @@ private void OnKillReported(ref KillReportedEvent ev) _point.AdjustPointValue(assist.PlayerId, 1, uid, point); var spawns = EntitySpawnCollection.GetSpawns(dm.RewardSpawns).Cast<string?>().ToList(); - EntityManager.SpawnEntities(Transform(ev.Entity).MapPosition, spawns); + EntityManager.SpawnEntities(_transform.GetMapCoordinates(ev.Entity), spawns); } } diff --git a/Content.Server/GameTicking/Rules/GenericAntagRuleSystem.cs b/Content.Server/GameTicking/Rules/GenericAntagRuleSystem.cs index 81bdda706bd..0367aa1460c 100644 --- a/Content.Server/GameTicking/Rules/GenericAntagRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GenericAntagRuleSystem.cs @@ -1,6 +1,8 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.Objectives; +using Content.Shared.Mind; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Content.Server.GameTicking.Rules; @@ -47,7 +49,8 @@ public bool StartRule(string rule, EntityUid mindId, [NotNullWhen(true)] out Ent private void OnObjectivesTextGetInfo(EntityUid uid, GenericAntagRuleComponent comp, ref ObjectivesTextGetInfoEvent args) { - args.Minds = comp.Minds; + // just temporary until this is deleted + args.Minds = comp.Minds.Select(mindId => (mindId, Comp<MindComponent>(mindId).CharacterName ?? "?")).ToList(); args.AgentName = Loc.GetString(comp.AgentName); } } diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs index e655bd472c6..f2287219fbe 100644 --- a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Spawners.Components; using Content.Server.GridPreloader; using Content.Shared.GameTicking.Components; +using Content.Shared.Whitelist; using Robust.Server.GameObjects; using Robust.Server.Maps; using Robust.Shared.Map; @@ -19,6 +20,7 @@ public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent> [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly GridPreloaderSystem _gridPreloader = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -99,7 +101,7 @@ private void OnSelectLocation(Entity<LoadMapRuleComponent> ent, ref AntagSelectL if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value)) continue; - if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager)) + if (_whitelistSystem.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid)) continue; args.Coordinates.Add(_transform.GetMapCoordinates(xform)); diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 7cc51db5766..4d02e2311d1 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -1,13 +1,13 @@ using Content.Server.Antag; using Content.Server.Communications; using Content.Server.GameTicking.Rules.Components; +using Content.Server.Ghost.Roles.Components; +using Content.Server.Ghost.Roles.Events; using Content.Server.Humanoid; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; +using Content.Server.Mind; using Content.Server.Nuke; using Content.Server.NukeOps; using Content.Server.Popups; -using Content.Server.Preferences.Managers; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Events; @@ -15,31 +15,29 @@ using Content.Server.Station.Components; using Content.Server.Store.Components; using Content.Server.Store.Systems; -using Content.Shared.GameTicking.Components; -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Prototypes; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; -using Content.Shared.Preferences; using Content.Shared.Store; using Content.Shared.Tag; using Content.Shared.Zombies; using Robust.Shared.Map; -using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; using System.Linq; +using Content.Server.NPC.Components; +using Content.Server.NPC.Systems; +using Content.Shared.GameTicking.Components; +using Content.Shared.Store.Components; namespace Content.Server.GameTicking.Rules; public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent> { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IServerPreferencesManager _prefs = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; @@ -71,7 +69,6 @@ public override void Initialize() SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared); SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt); - SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity); SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected); } @@ -82,7 +79,7 @@ protected override void Started(EntityUid uid, NukeopsRuleComponent component, G var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>(); while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) { - if (!_npcFaction.IsFactionHostile(component.Faction, eligibleUid, member)) + if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) continue; eligible.Add((eligibleUid, eligibleComp, member)); @@ -471,22 +468,6 @@ private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> ent) nukeops.RoundEndBehavior = RoundEndBehavior.Nothing; } - // this should really go anywhere else but im tired. - private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args) - { - if (args.Handled) - return; - - var profile = args.Session != null - ? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile - : HumanoidCharacterProfile.RandomWithSpecies(); - if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) - species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies); - - args.Entity = Spawn(species.Prototype); - _humanoid.LoadProfile(args.Entity.Value, profile!); - } - private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args) { if (ent.Comp.TargetStation is not { } station) diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index 29f69db14a2..c5f88ab6cf1 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -4,8 +4,6 @@ using Content.Server.Flash; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Server.Revolutionary; using Content.Server.Revolutionary.Components; @@ -23,6 +21,8 @@ using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Prototypes; +using Content.Shared.NPC.Systems; using Content.Shared.Revolutionary.Components; using Content.Shared.Stunnable; using Content.Shared.Zombies; diff --git a/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs b/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs index 083085fa0d8..faec4a9e9ca 100644 --- a/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs @@ -24,7 +24,6 @@ public override void Initialize() SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected); SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing); - SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo); } private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args) @@ -33,41 +32,9 @@ private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEn return; //Generate objectives - GenerateObjectives(mindId, mind, ent); _antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null); } - private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule) - { - // Give thieves their objectives - var difficulty = 0f; - - if (_random.Prob(thiefRule.BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal) - { - var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.BigObjectiveGroup); - if (objective != null) - { - _mindSystem.AddObjective(mindId, mind, objective.Value); - difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty; - } - } - - for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives - { - var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.SmallObjectiveGroup); - if (objective == null) - continue; - - _mindSystem.AddObjective(mindId, mind, objective.Value); - difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty; - } - - //Escape target - var escapeObjective = _objectives.GetRandomObjective(mindId, mind, thiefRule.EscapeObjectiveGroup); - if (escapeObjective != null) - _mindSystem.AddObjective(mindId, mind, escapeObjective.Value); - } - //Add mind briefing private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args) { @@ -87,10 +54,4 @@ private string MakeBriefing(EntityUid thief) briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n"; return briefing; } - - private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args) - { - args.Minds = _antag.GetAntagMindEntityUids(ent.Owner); - args.AgentName = Loc.GetString("thief-round-end-agent-name"); - } } diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 3d0a02d6aa9..51545c6ce50 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -1,28 +1,34 @@ using Content.Server.Antag; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; -using Content.Server.NPC.Systems; using Content.Server.Objectives; using Content.Server.PDA.Ringer; using Content.Server.Roles; -using Content.Server.Traitor.Components; using Content.Server.Traitor.Uplink; +using Content.Shared.FixedPoint; using Content.Shared.GameTicking.Components; using Content.Shared.Mind; -using Content.Shared.Mood; +using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Systems; using Content.Shared.Objectives.Components; using Content.Shared.PDA; +using Content.Shared.Radio; using Content.Shared.Roles; using Content.Shared.Roles.Jobs; +using Content.Shared.Roles.RoleCodeword; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; using System.Text; +using Content.Shared.Mood; + namespace Content.Server.GameTicking.Rules; public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent> { + private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b"); + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; @@ -32,8 +38,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent> [Dependency] private readonly SharedRoleSystem _roleSystem = default!; [Dependency] private readonly SharedJobSystem _jobs = default!; [Dependency] private readonly ObjectivesSystem _objectives = default!; - - public const int MaxPicks = 20; + [Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!; public override void Initialize() { @@ -41,7 +46,6 @@ public override void Initialize() SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected); - SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo); SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend); } @@ -69,33 +73,37 @@ private void MakeCodewords(TraitorRuleComponent component) } } - public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true) + public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component) { //Grab the mind if it wasnt provided if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) return false; - var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); - var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values); + var briefing = ""; - if (TryComp<AutoTraitorComponent>(traitor, out var autoTraitorComponent)) - { - giveUplink = autoTraitorComponent.GiveUplink; - giveObjectives = autoTraitorComponent.GiveObjectives; - } + if (component.GiveCodewords) + briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); + + var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values); Note[]? code = null; - if (giveUplink) + + if (component.GiveUplink) { // Calculate the amount of currency on the uplink. var startingBalance = component.StartingBalance; - if (_jobs.MindTryGetJob(mindId, out _, out var prototype)) - startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0); + if (_jobs.MindTryGetJob(mindId, out _, out var job)) + { + if (startingBalance < job.AntagAdvantage) // Can't use Math functions on FixedPoint2 + startingBalance = 0; + else + startingBalance = startingBalance - job.AntagAdvantage; + } // creadth: we need to create uplink for the antag. // PDA should be in place already var pda = _uplink.FindUplinkTarget(traitor); - if (pda == null || !_uplink.AddUplink(traitor, startingBalance)) + if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true)) return false; // Give traitors their codewords and uplink code to keep in their character info menu @@ -108,6 +116,7 @@ public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification); + component.TraitorMinds.Add(mindId); // Assign briefing @@ -116,50 +125,64 @@ public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool Briefing = briefing }, mind, true); - // Don't Change the faction, this was stupid. + // Send codewords to only the traitor client + var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found + + RoleCodewordComponent codewordComp = EnsureComp<RoleCodewordComponent>(mindId); + _roleCodewordSystem.SetRoleCodewords(codewordComp, "traitor", component.Codewords.ToList(), color); + + // Don't change the faction, this was stupid. //_npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false); //_npcFaction.AddFaction(traitor, component.SyndicateFaction); RaiseLocalEvent(traitor, new MoodEffectEvent("TraitorFocused")); - - // Give traitors their objectives - if (giveObjectives) - { - var difficulty = 0f; - for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++) - { - var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup); - if (objective == null) - continue; - - _mindSystem.AddObjective(mindId, mind, objective.Value); - var adding = Comp<ObjectiveComponent>(objective.Value).Difficulty; - difficulty += adding; - Log.Debug($"Added objective {ToPrettyString(objective):objective} with {adding} difficulty"); - } - } - return true; } - private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args) + private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing) { - args.Minds = _antag.GetAntagMindEntityUids(uid); - args.AgentName = Loc.GetString("traitor-round-end-agent-name"); + var pda = _uplink.FindUplinkTarget(traitor); + Note[]? code = null; + + var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true); + + if (pda is not null && uplinked) + { + // Codes are only generated if the uplink is a PDA + code = EnsureComp<RingerUplinkComponent>(pda.Value).Code; + + // If giveUplink is false the uplink code part is omitted + briefing = string.Format("{0}\n{1}", + briefing, + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); + return (code, briefing); + } + else if (pda is null && uplinked) + { + briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short"); + } + + return (null, briefing); } + // TODO: AntagCodewordsComponent private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args) { args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords))); } - private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null) + // TODO: figure out how to handle this? add priority to briefing event? + private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null) { var sb = new StringBuilder(); sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown")))); - sb.AppendLine(Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", codewords)))); + if (codewords != null) + sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords)))); if (uplinkCode != null) - sb.AppendLine(Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", uplinkCode).Replace("sharp", "#")))); + sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#")))); + else + sb.AppendLine(Loc.GetString("traitor-role-uplink-implant")); + return sb.ToString(); } diff --git a/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs index fd94c74ac8c..372de4bbb45 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking.Rules.VariationPass.Components; using Content.Server.Wires; +using Content.Shared.Whitelist; using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules.VariationPass; @@ -11,6 +12,8 @@ namespace Content.Server.GameTicking.Rules.VariationPass; /// </summary> public sealed class CutWireVariationPassSystem : VariationPassSystem<CutWireVariationPassComponent> { + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + protected override void ApplyVariation(Entity<CutWireVariationPassComponent> ent, ref StationVariationPassEvent args) { var wiresCut = 0; @@ -22,7 +25,7 @@ protected override void ApplyVariation(Entity<CutWireVariationPassComponent> ent continue; // Check against blacklist - if (ent.Comp.Blacklist.IsValid(uid)) + if (_whitelistSystem.IsBlacklistPass(ent.Comp.Blacklist, uid)) continue; if (Random.Prob(ent.Comp.WireCutChance)) diff --git a/Content.Server/Gatherable/GatherableSystem.cs b/Content.Server/Gatherable/GatherableSystem.cs index d438fb95e03..84d71cdac21 100644 --- a/Content.Server/Gatherable/GatherableSystem.cs +++ b/Content.Server/Gatherable/GatherableSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Weapons.Melee.Events; using Content.Shared.Whitelist; using Robust.Server.GameObjects; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; diff --git a/Content.Server/Ghost/Ghost.cs b/Content.Server/Ghost/Ghost.cs index 1453bf3faa9..69d81d95929 100644 --- a/Content.Server/Ghost/Ghost.cs +++ b/Content.Server/Ghost/Ghost.cs @@ -1,4 +1,5 @@ using Content.Server.GameTicking; +using Content.Server.Popups; using Content.Shared.Administration; using Content.Shared.Mind; using Robust.Shared.Console; @@ -11,15 +12,25 @@ public sealed class Ghost : IConsoleCommand [Dependency] private readonly IEntityManager _entities = default!; public string Command => "ghost"; - public string Description => "Give up on life and become a ghost."; - public string Help => "ghost"; + public string Description => Loc.GetString("ghost-command-description"); + public string Help => Loc.GetString("ghost-command-help-text"); public void Execute(IConsoleShell shell, string argStr, string[] args) { var player = shell.Player; if (player == null) { - shell.WriteLine("You have no session, you can't ghost."); + shell.WriteLine(Loc.GetString("ghost-command-no-session")); + return; + } + + if (player.AttachedEntity is { Valid: true } frozen && + _entities.HasComponent<AdminFrozenComponent>(frozen)) + { + var deniedMessage = Loc.GetString("ghost-command-denied"); + shell.WriteLine(deniedMessage); + _entities.System<PopupSystem>() + .PopupEntity(deniedMessage, frozen, frozen); return; } @@ -30,9 +41,9 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) mind = _entities.GetComponent<MindComponent>(mindId); } - if (!EntitySystem.Get<GameTicker>().OnGhostAttempt(mindId, true, true, mind)) + if (!_entities.System<GameTicker>().OnGhostAttempt(mindId, true, true, mind)) { - shell.WriteLine("You can't ghost right now."); + shell.WriteLine(Loc.GetString("ghost-command-denied")); } } } diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index effed5cdbe1..43b7b6f5f82 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -55,7 +55,6 @@ public override void Initialize() _ghostQuery = GetEntityQuery<GhostComponent>(); _physicsQuery = GetEntityQuery<PhysicsComponent>(); - SubscribeLocalEvent<GhostComponent, ComponentStartup>(OnGhostStartup); SubscribeLocalEvent<GhostComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<GhostComponent, ComponentShutdown>(OnGhostShutdown); @@ -147,24 +146,6 @@ private void OnRelayMoveInput(EntityUid uid, GhostOnMoveComponent component, ref _ticker.OnGhostAttempt(mindId, component.CanReturn, mind: mind); } - private void OnGhostStartup(EntityUid uid, GhostComponent component, ComponentStartup args) - { - // Allow this entity to be seen by other ghosts. - var visibility = EnsureComp<VisibilityComponent>(uid); - - if (_ticker.RunLevel != GameRunLevel.PostRound) - { - _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); - _visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false); - _visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility); - } - - SetCanSeeGhosts(uid, true); - - var time = _gameTiming.CurTime; - component.TimeOfDeath = time; - } - private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args) { // Perf: If the entity is deleting itself, no reason to change these back. @@ -197,14 +178,22 @@ private void SetCanSeeGhosts(EntityUid uid, bool canSee, EyeComponent? eyeCompon private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args) { - if (_actions.AddAction(uid, ref component.BooActionEntity, out var act, component.BooAction) - && act.UseDelay != null) + // Allow this entity to be seen by other ghosts. + var visibility = EnsureComp<VisibilityComponent>(uid); + + if (_ticker.RunLevel != GameRunLevel.PostRound) { - var start = _gameTiming.CurTime; - var end = start + act.UseDelay.Value; - _actions.SetCooldown(component.BooActionEntity.Value, start, end); + _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); + _visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false); + _visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility); } + SetCanSeeGhosts(uid, true); + + var time = _gameTiming.CurTime; + component.TimeOfDeath = time; + + _actions.AddAction(uid, ref component.BooActionEntity, component.BooAction); _actions.AddAction(uid, ref component.ToggleGhostHearingActionEntity, component.ToggleGhostHearingAction); _actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction); _actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction); diff --git a/Content.Server/Glue/GlueSystem.cs b/Content.Server/Glue/GlueSystem.cs index ff53ef91cac..79249f5bd96 100644 --- a/Content.Server/Glue/GlueSystem.cs +++ b/Content.Server/Glue/GlueSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Interaction; using Content.Shared.Interaction.Components; using Content.Shared.Item; +using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; using Content.Shared.Verbs; @@ -20,9 +21,9 @@ public sealed class GlueSystem : SharedGlueSystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly OpenableSystem _openable = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; public override void Initialize() { @@ -32,6 +33,7 @@ public override void Initialize() SubscribeLocalEvent<GluedComponent, ComponentInit>(OnGluedInit); SubscribeLocalEvent<GlueComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb); SubscribeLocalEvent<GluedComponent, GotEquippedHandEvent>(OnHandPickUp); + SubscribeLocalEvent<GluedComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } // When glue bottle is used on item it will apply the glued and unremoveable components. @@ -95,27 +97,22 @@ public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator<GluedComponent, UnremoveableComponent, MetaDataComponent>(); - while (query.MoveNext(out var uid, out var glue, out var _, out var meta)) + var query = EntityQueryEnumerator<GluedComponent, UnremoveableComponent>(); + while (query.MoveNext(out var uid, out var glue, out var _)) { if (_timing.CurTime < glue.Until) continue; - // Instead of string matching, just reconstruct the expected name and compare - if (meta.EntityName == Loc.GetString("glued-name-prefix", ("target", glue.BeforeGluedEntityName))) - _metaData.SetEntityName(uid, glue.BeforeGluedEntityName); - RemComp<UnremoveableComponent>(uid); RemComp<GluedComponent>(uid); + + _nameMod.RefreshNameModifiers(uid); } } private void OnGluedInit(Entity<GluedComponent> entity, ref ComponentInit args) { - var meta = MetaData(entity); - var name = meta.EntityName; - entity.Comp.BeforeGluedEntityName = meta.EntityName; - _metaData.SetEntityName(entity.Owner, Loc.GetString("glued-name-prefix", ("target", name))); + _nameMod.RefreshNameModifiers(entity.Owner); } private void OnHandPickUp(Entity<GluedComponent> entity, ref GotEquippedHandEvent args) @@ -124,4 +121,9 @@ private void OnHandPickUp(Entity<GluedComponent> entity, ref GotEquippedHandEven comp.DeleteOnDrop = false; entity.Comp.Until = _timing.CurTime + entity.Comp.Duration; } + + private void OnRefreshNameModifiers(Entity<GluedComponent> entity, ref RefreshNameModifiersEvent args) + { + args.AddModifier("glued-name-prefix"); + } } diff --git a/Content.Server/Gravity/GravityGeneratorComponent.cs b/Content.Server/Gravity/GravityGeneratorComponent.cs index f47d3979391..c715a5e5f35 100644 --- a/Content.Server/Gravity/GravityGeneratorComponent.cs +++ b/Content.Server/Gravity/GravityGeneratorComponent.cs @@ -8,45 +8,13 @@ namespace Content.Server.Gravity [Access(typeof(GravityGeneratorSystem))] public sealed partial class GravityGeneratorComponent : SharedGravityGeneratorComponent { - // 1% charge per second. - [ViewVariables(VVAccess.ReadWrite)] [DataField("chargeRate")] public float ChargeRate { get; set; } = 0.01f; - // The gravity generator has two power values. - // Idle power is assumed to be the power needed to run the control systems and interface. - [DataField("idlePower")] public float IdlePowerUse { get; set; } - // Active power is the power needed to keep the gravity field stable. - [DataField("activePower")] public float ActivePowerUse { get; set; } [DataField("lightRadiusMin")] public float LightRadiusMin { get; set; } [DataField("lightRadiusMax")] public float LightRadiusMax { get; set; } - - /// <summary> - /// Is the power switch on? - /// </summary> - [DataField("switchedOn")] - public bool SwitchedOn { get; set; } = true; - - /// <summary> - /// Is the gravity generator intact? - /// </summary> - [DataField("intact")] - public bool Intact { get; set; } = true; - - [DataField("maxCharge")] - public float MaxCharge { get; set; } = 1; - - // 0 -> 1 - [ViewVariables(VVAccess.ReadWrite)] [DataField("charge")] public float Charge { get; set; } = 1; - - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))] - public string MachinePartMaxChargeMultiplier = "Capacitor"; - /// <summary> /// Is the gravity generator currently "producing" gravity? /// </summary> [ViewVariables] public bool GravityActive { get; set; } = false; - - // Do we need a UI update even if the charge doesn't change? Used by power button. - [ViewVariables] public bool NeedUIUpdate { get; set; } } } diff --git a/Content.Server/Gravity/GravityGeneratorSystem.cs b/Content.Server/Gravity/GravityGeneratorSystem.cs index dc31c004c64..5ab2dc89310 100644 --- a/Content.Server/Gravity/GravityGeneratorSystem.cs +++ b/Content.Server/Gravity/GravityGeneratorSystem.cs @@ -1,324 +1,63 @@ -using Content.Server.Administration.Logs; -using Content.Server.Audio; -using Content.Server.Construction; using Content.Server.Power.Components; -using Content.Server.Emp; -using Content.Shared.Database; +using Content.Server.Power.EntitySystems; using Content.Shared.Gravity; -using Content.Shared.Interaction; -using Robust.Server.GameObjects; -using Robust.Shared.Player; -namespace Content.Server.Gravity -{ - public sealed class GravityGeneratorSystem : EntitySystem - { - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly AmbientSoundSystem _ambientSoundSystem = default!; - [Dependency] private readonly GravitySystem _gravitySystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedPointLightSystem _lights = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<GravityGeneratorComponent, ComponentInit>(OnCompInit); - SubscribeLocalEvent<GravityGeneratorComponent, ComponentShutdown>(OnComponentShutdown); - SubscribeLocalEvent<GravityGeneratorComponent, EntParentChangedMessage>(OnParentChanged); // Or just anchor changed? - SubscribeLocalEvent<GravityGeneratorComponent, InteractHandEvent>(OnInteractHand); - SubscribeLocalEvent<GravityGeneratorComponent, RefreshPartsEvent>(OnRefreshParts); - SubscribeLocalEvent<GravityGeneratorComponent, SharedGravityGeneratorComponent.SwitchGeneratorMessage>( - OnSwitchGenerator); - - SubscribeLocalEvent<GravityGeneratorComponent, EmpPulseEvent>(OnEmpPulse); - } - - private void OnParentChanged(EntityUid uid, GravityGeneratorComponent component, ref EntParentChangedMessage args) - { - if (component.GravityActive && TryComp(args.OldParent, out GravityComponent? gravity)) - { - _gravitySystem.RefreshGravity(args.OldParent.Value, gravity); - } - } - - private void OnComponentShutdown(EntityUid uid, GravityGeneratorComponent component, ComponentShutdown args) - { - if (component.GravityActive && - TryComp<TransformComponent>(uid, out var xform) && - TryComp(xform.ParentUid, out GravityComponent? gravity)) - { - component.GravityActive = false; - _gravitySystem.RefreshGravity(xform.ParentUid, gravity); - } - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator<GravityGeneratorComponent, ApcPowerReceiverComponent>(); - while (query.MoveNext(out var uid, out var gravGen, out var powerReceiver)) - { - var ent = (uid, gravGen, powerReceiver); - if (!gravGen.Intact) - continue; - - // Calculate charge rate based on power state and such. - // Negative charge rate means discharging. - float chargeRate; - if (gravGen.SwitchedOn) - { - if (powerReceiver.Powered) - { - chargeRate = gravGen.ChargeRate; - } - else - { - // Scale discharge rate such that if we're at 25% active power we discharge at 75% rate. - var receiving = powerReceiver.PowerReceived; - var mainSystemPower = Math.Max(0, receiving - gravGen.IdlePowerUse); - var ratio = 1 - mainSystemPower / (gravGen.ActivePowerUse - gravGen.IdlePowerUse); - chargeRate = -(ratio * gravGen.ChargeRate); - } - } - else - { - chargeRate = -gravGen.ChargeRate; - } - - var active = gravGen.GravityActive; - var lastCharge = gravGen.Charge; - gravGen.Charge = Math.Clamp(gravGen.Charge + frameTime * chargeRate, 0, gravGen.MaxCharge); - if (chargeRate > 0) - { - // Charging. - if (MathHelper.CloseTo(gravGen.Charge, gravGen.MaxCharge) && !gravGen.GravityActive) - { - gravGen.GravityActive = true; - } - } - else - { - // Discharging - if (MathHelper.CloseTo(gravGen.Charge, 0) && gravGen.GravityActive) - { - gravGen.GravityActive = false; - } - } - - var updateUI = gravGen.NeedUIUpdate; - if (!MathHelper.CloseTo(lastCharge, gravGen.Charge)) - { - UpdateState(ent); - updateUI = true; - } - - if (updateUI) - UpdateUI(ent, chargeRate); - - if (active != gravGen.GravityActive && - TryComp<TransformComponent>(uid, out var xform) && - TryComp<GravityComponent>(xform.ParentUid, out var gravity)) - { - // Force it on in the faster path. - if (gravGen.GravityActive) - { - _gravitySystem.EnableGravity(xform.ParentUid, gravity); - } - else - { - _gravitySystem.RefreshGravity(xform.ParentUid, gravity); - } - } - } - } - - private void SetSwitchedOn(EntityUid uid, GravityGeneratorComponent component, bool on, - ApcPowerReceiverComponent? powerReceiver = null, EntityUid? user = null) - { - if (!Resolve(uid, ref powerReceiver)) - return; - - if (user != null) - _adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user)} set ${ToPrettyString(uid):target} to {(on ? "on" : "off")}"); - - component.SwitchedOn = on; - UpdatePowerState(component, powerReceiver); - component.NeedUIUpdate = true; - } - - private static void UpdatePowerState( - GravityGeneratorComponent component, - ApcPowerReceiverComponent powerReceiver) - { - powerReceiver.Load = component.SwitchedOn ? component.ActivePowerUse : component.IdlePowerUse; - } - - private void UpdateUI(Entity<GravityGeneratorComponent, ApcPowerReceiverComponent> ent, float chargeRate) - { - var (_, component, powerReceiver) = ent; - if (!_uiSystem.IsUiOpen(ent.Owner, SharedGravityGeneratorComponent.GravityGeneratorUiKey.Key)) - return; - - var chargeTarget = chargeRate < 0 ? 0 : component.MaxCharge; - short chargeEta; - var atTarget = false; - if (MathHelper.CloseTo(component.Charge, chargeTarget)) - { - chargeEta = short.MinValue; // N/A - atTarget = true; - } - else - { - var diff = chargeTarget - component.Charge; - chargeEta = (short) Math.Abs(diff / chargeRate); - } - - var status = chargeRate switch - { - > 0 when atTarget => GravityGeneratorPowerStatus.FullyCharged, - < 0 when atTarget => GravityGeneratorPowerStatus.Off, - > 0 => GravityGeneratorPowerStatus.Charging, - < 0 => GravityGeneratorPowerStatus.Discharging, - _ => throw new ArgumentOutOfRangeException() - }; - - var state = new SharedGravityGeneratorComponent.GeneratorState( - component.SwitchedOn, - (byte) (component.Charge * 255), - status, - (short) Math.Round(powerReceiver.PowerReceived), - (short) Math.Round(powerReceiver.Load), - chargeEta - ); - - _uiSystem.SetUiState( - ent.Owner, - SharedGravityGeneratorComponent.GravityGeneratorUiKey.Key, - state); - - component.NeedUIUpdate = false; - } +namespace Content.Server.Gravity; - private void OnCompInit(Entity<GravityGeneratorComponent> ent, ref ComponentInit args) - { - ApcPowerReceiverComponent? powerReceiver = null; - if (!Resolve(ent, ref powerReceiver, false)) - return; - - UpdatePowerState(ent, powerReceiver); - UpdateState((ent, ent.Comp, powerReceiver)); - } - - private void OnInteractHand(EntityUid uid, GravityGeneratorComponent component, InteractHandEvent args) - { - ApcPowerReceiverComponent? powerReceiver = default!; - if (!Resolve(uid, ref powerReceiver)) - return; - - // Do not allow opening UI if broken or unpowered. - if (!component.Intact || powerReceiver.PowerReceived < component.IdlePowerUse) - return; - - _uiSystem.OpenUi(uid, SharedGravityGeneratorComponent.GravityGeneratorUiKey.Key, args.User); - component.NeedUIUpdate = true; - } - - public void UpdateState(Entity<GravityGeneratorComponent, ApcPowerReceiverComponent> ent) - { - var (uid, grav, powerReceiver) = ent; - var appearance = EntityManager.GetComponentOrNull<AppearanceComponent>(uid); - _appearance.SetData(uid, GravityGeneratorVisuals.Charge, grav.Charge, appearance); - - if (_lights.TryGetLight(uid, out var pointLight)) - { - _lights.SetEnabled(uid, grav.Charge > 0, pointLight); - _lights.SetRadius(uid, MathHelper.Lerp(grav.LightRadiusMin, grav.LightRadiusMax, grav.Charge), pointLight); - } - - if (!grav.Intact) - { - MakeBroken((uid, grav), appearance); - } - else if (powerReceiver.PowerReceived < grav.IdlePowerUse) - { - MakeUnpowered((uid, grav), appearance); - } - else if (!grav.SwitchedOn) - { - MakeOff((uid, grav), appearance); - } - else - { - MakeOn((uid, grav), appearance); - } - } - - private void OnRefreshParts(EntityUid uid, GravityGeneratorComponent component, RefreshPartsEvent args) - { - var maxChargeMultipler = args.PartRatings[component.MachinePartMaxChargeMultiplier]; - component.MaxCharge = maxChargeMultipler * 1; - } - - private void MakeBroken(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance) - { - _ambientSoundSystem.SetAmbience(ent, false); - - _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.Broken); - } +public sealed class GravityGeneratorSystem : EntitySystem +{ + [Dependency] private readonly GravitySystem _gravitySystem = default!; + [Dependency] private readonly SharedPointLightSystem _lights = default!; - private void MakeUnpowered(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance) - { - _ambientSoundSystem.SetAmbience(ent, false); + public override void Initialize() + { + base.Initialize(); - _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.Unpowered, appearance); - } + SubscribeLocalEvent<GravityGeneratorComponent, EntParentChangedMessage>(OnParentChanged); + SubscribeLocalEvent<GravityGeneratorComponent, ChargedMachineActivatedEvent>(OnActivated); + SubscribeLocalEvent<GravityGeneratorComponent, ChargedMachineDeactivatedEvent>(OnDeactivated); + } - private void MakeOff(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance) + public override void Update(float frameTime) + { + base.Update(frameTime); + var query = EntityQueryEnumerator<GravityGeneratorComponent, PowerChargeComponent>(); + while (query.MoveNext(out var uid, out var grav, out var charge)) { - _ambientSoundSystem.SetAmbience(ent, false); + if (!_lights.TryGetLight(uid, out var pointLight)) + continue; - _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.Off, appearance); + _lights.SetEnabled(uid, charge.Charge > 0, pointLight); + _lights.SetRadius(uid, MathHelper.Lerp(grav.LightRadiusMin, grav.LightRadiusMax, charge.Charge), + pointLight); } + } - private void MakeOn(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance) + private void OnActivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineActivatedEvent args) + { + ent.Comp.GravityActive = true; + if (TryComp<TransformComponent>(ent, out var xform) && + TryComp(xform.ParentUid, out GravityComponent? gravity)) { - _ambientSoundSystem.SetAmbience(ent, true); - - _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.On, appearance); + _gravitySystem.EnableGravity(xform.ParentUid, gravity); } + } - private void OnSwitchGenerator( - EntityUid uid, - GravityGeneratorComponent component, - SharedGravityGeneratorComponent.SwitchGeneratorMessage args) + private void OnDeactivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args) + { + ent.Comp.GravityActive = false; + if (TryComp<TransformComponent>(ent, out var xform) && + TryComp(xform.ParentUid, out GravityComponent? gravity)) { - SetSwitchedOn(uid, component, args.On, user: args.Actor); + _gravitySystem.RefreshGravity(xform.ParentUid, gravity); } + } - private void OnEmpPulse(EntityUid uid, GravityGeneratorComponent component, EmpPulseEvent args) + private void OnParentChanged(EntityUid uid, GravityGeneratorComponent component, ref EntParentChangedMessage args) + { + if (component.GravityActive && TryComp(args.OldParent, out GravityComponent? gravity)) { - /// i really don't think that the gravity generator should use normalised 0-1 charge - /// as opposed to watts charge that every other battery uses - - ApcPowerReceiverComponent? powerReceiver = null; - if (!Resolve(uid, ref powerReceiver, false)) - return; - - var ent = (uid, component, powerReceiver); - - // convert from normalised energy to watts and subtract - float maxEnergy = component.ActivePowerUse / component.ChargeRate; - float currentEnergy = maxEnergy * component.Charge; - currentEnergy = Math.Max(0, currentEnergy - args.EnergyConsumption); - - // apply renormalised energy to charge variable - component.Charge = currentEnergy / maxEnergy; - - // update power state - UpdateState(ent); + _gravitySystem.RefreshGravity(args.OldParent.Value, gravity); } } } diff --git a/Content.Server/Gravity/GravitySystem.cs b/Content.Server/Gravity/GravitySystem.cs index ea62d4a8195..6807b9df4a4 100644 --- a/Content.Server/Gravity/GravitySystem.cs +++ b/Content.Server/Gravity/GravitySystem.cs @@ -1,7 +1,6 @@ using Content.Shared.Gravity; using JetBrains.Annotations; using Robust.Shared.Map.Components; -using Robust.Shared.Utility; namespace Content.Server.Gravity { diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs index 4ad6c1f8359..203882ed9ef 100644 --- a/Content.Server/Guardian/GuardianSystem.cs +++ b/Content.Server/Guardian/GuardianSystem.cs @@ -194,11 +194,7 @@ private void UseCreator(EntityUid user, EntityUid target, EntityUid injector, Gu return; } - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.InjectionDelay, new GuardianCreatorDoAfterEvent(), injector, target: target, used: injector) - { - BreakOnTargetMove = true, - BreakOnUserMove = true - }); + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.InjectionDelay, new GuardianCreatorDoAfterEvent(), injector, target: target, used: injector){BreakOnMove = true}); } private void OnDoAfter(EntityUid uid, GuardianCreatorComponent component, DoAfterEvent args) @@ -212,7 +208,7 @@ private void OnDoAfter(EntityUid uid, GuardianCreatorComponent component, DoAfte var hostXform = Transform(args.Args.Target.Value); var host = EnsureComp<GuardianHostComponent>(args.Args.Target.Value); // Use map position so it's not inadvertantly parented to the host + if it's in a container it spawns outside I guess. - var guardian = Spawn(component.GuardianProto, hostXform.MapPosition); + var guardian = Spawn(component.GuardianProto, _transform.GetMapCoordinates(args.Args.Target.Value, xform: hostXform)); _container.Insert(guardian, host.GuardianContainer); host.HostedGuardian = guardian; diff --git a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs index 7e107ce1a5a..8fd088408c0 100644 --- a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs +++ b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs @@ -5,6 +5,7 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Robust.Shared.Prototypes; @@ -40,8 +41,8 @@ public static void PublishJson(StreamWriter file) WriteIndented = true, Converters = { - new UniversalJsonConverter<ReagentEffect>(), - new UniversalJsonConverter<ReagentEffectCondition>(), + new UniversalJsonConverter<EntityEffect>(), + new UniversalJsonConverter<EntityEffectCondition>(), new UniversalJsonConverter<ReagentEffectsEntry>(), new UniversalJsonConverter<DamageSpecifier>(), new FixedPointJsonConverter() diff --git a/Content.Server/GuideGenerator/ReactionJsonGenerator.cs b/Content.Server/GuideGenerator/ReactionJsonGenerator.cs index 123dd5f8eb3..06c230d6071 100644 --- a/Content.Server/GuideGenerator/ReactionJsonGenerator.cs +++ b/Content.Server/GuideGenerator/ReactionJsonGenerator.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Text.Json; using Content.Shared.Chemistry.Reaction; -using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; namespace Content.Server.GuideGenerator; @@ -24,7 +24,7 @@ public static void PublishJson(StreamWriter file) WriteIndented = true, Converters = { - new UniversalJsonConverter<ReagentEffect>(), + new UniversalJsonConverter<EntityEffect>(), } }; diff --git a/Content.Server/GuideGenerator/ReagentEntry.cs b/Content.Server/GuideGenerator/ReagentEntry.cs index ab9e7582060..8b597ad61bc 100644 --- a/Content.Server/GuideGenerator/ReagentEntry.cs +++ b/Content.Server/GuideGenerator/ReagentEntry.cs @@ -4,6 +4,7 @@ using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Prototypes; namespace Content.Server.GuideGenerator; @@ -61,7 +62,7 @@ public sealed class ReactionEntry public Dictionary<string, float> Products { get; } [JsonPropertyName("effects")] - public List<ReagentEffect> Effects { get; } + public List<EntityEffect> Effects { get; } public ReactionEntry(ReactionPrototype proto) { diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index d7fdb0aa16b..0062fc8198f 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -237,17 +237,17 @@ hands.ActiveHandEntity is not { } throwEnt || var distance = Math.Clamp(length, minDistance, hands.ThrowRange); direction *= distance / length; - var throwStrength = hands.ThrowForceMultiplier; + var throwSpeed = hands.BaseThrowspeed; // Let other systems change the thrown entity (useful for virtual items) // or the throw strength. - var itemEv = new BeforeGettingThrownEvent(throwEnt, direction, throwStrength, player); - RaiseLocalEvent(throwEnt, ref itemEv); + var itemEv = new BeforeGettingThrownEvent(throwEnt, direction, throwSpeed, player); + RaiseLocalEvent(player, ref itemEv); if (itemEv.Cancelled) return true; - var ev = new BeforeThrowEvent(throwEnt, direction, throwStrength, player); + var ev = new BeforeThrowEvent(throwEnt, direction, throwSpeed, player); RaiseLocalEvent(player, ref ev); if (ev.Cancelled) @@ -257,7 +257,7 @@ hands.ActiveHandEntity is not { } throwEnt || if (IsHolding(player, throwEnt, out _, hands) && !TryDrop(player, throwEnt, handsComp: hands)) return false; - _throwingSystem.TryThrow(ev.ItemUid, ev.Direction, ev.ThrowStrength, ev.PlayerUid); + _throwingSystem.TryThrow(ev.ItemUid, ev.Direction, ev.ThrowSpeed, ev.PlayerUid, compensateFriction: !HasComp<LandAtCursorComponent>(ev.ItemUid)); return true; } diff --git a/Content.Server/Holiday/Christmas/RandomGiftSystem.cs b/Content.Server/Holiday/Christmas/RandomGiftSystem.cs index 48af65cb378..4603f45ed81 100644 --- a/Content.Server/Holiday/Christmas/RandomGiftSystem.cs +++ b/Content.Server/Holiday/Christmas/RandomGiftSystem.cs @@ -1,9 +1,10 @@ -using Content.Server.Administration.Logs; +using Content.Server.Administration.Logs; using Content.Server.Hands.Systems; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.Interaction.Events; using Content.Shared.Item; +using Content.Shared.Whitelist; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Map.Components; @@ -24,6 +25,7 @@ public sealed class RandomGiftSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; private readonly List<string> _possibleGiftsSafe = new(); private readonly List<string> _possibleGiftsUnsafe = new(); @@ -40,7 +42,7 @@ public override void Initialize() private void OnExamined(EntityUid uid, RandomGiftComponent component, ExaminedEvent args) { - if (!component.ContentsViewers.IsValid(args.Examiner, EntityManager) || component.SelectedEntity is null) + if (_whitelistSystem.IsWhitelistFail(component.ContentsViewers, args.Examiner) || component.SelectedEntity is null) return; var name = _prototype.Index<EntityPrototype>(component.SelectedEntity).Name; diff --git a/Content.Server/IP/IPAddressExt.cs b/Content.Server/IP/IPAddressExt.cs index 6bfa4ef5486..a61477e01bc 100644 --- a/Content.Server/IP/IPAddressExt.cs +++ b/Content.Server/IP/IPAddressExt.cs @@ -61,6 +61,12 @@ public static bool IsInSubnet(this System.Net.IPAddress address, (System.Net.IPA public static bool IsInSubnet(this System.Net.IPAddress address, System.Net.IPAddress maskAddress, int maskLength) { + if (maskAddress.AddressFamily != address.AddressFamily) + { + // We got something like an IPV4-Address for an IPv6-Mask. This is not valid. + return false; + } + if (maskAddress.AddressFamily == AddressFamily.InterNetwork) { // Convert the mask address to an unsigned integer. @@ -89,7 +95,7 @@ public static bool IsInSubnet(this System.Net.IPAddress address, System.Net.IPAd if (maskAddressBits.Length != ipAddressBits.Length) { - throw new ArgumentException("Length of IP Address and Subnet Mask do not match."); + return false; } // Compare the prefix bits. diff --git a/Content.Server/IdentityManagement/IdentitySystem.cs b/Content.Server/IdentityManagement/IdentitySystem.cs index 4766b89172f..1a2cdcce511 100644 --- a/Content.Server/IdentityManagement/IdentitySystem.cs +++ b/Content.Server/IdentityManagement/IdentitySystem.cs @@ -165,7 +165,7 @@ private IdentityRepresentation GetIdentityRepresentation(EntityUid target, if (_idCard.TryFindIdCard(target, out var id)) { presumedName = string.IsNullOrWhiteSpace(id.Comp.FullName) ? null : id.Comp.FullName; - presumedJob = id.Comp.JobTitle?.ToLowerInvariant(); + presumedJob = id.Comp.LocalizedJobTitle?.ToLowerInvariant(); } // If it didn't find a job, that's fine. diff --git a/Content.Server/Implants/ImplanterSystem.cs b/Content.Server/Implants/ImplanterSystem.cs index 3cfa3a9f5f8..e441574213e 100644 --- a/Content.Server/Implants/ImplanterSystem.cs +++ b/Content.Server/Implants/ImplanterSystem.cs @@ -99,9 +99,8 @@ public void TryImplant(ImplanterComponent component, EntityUid user, EntityUid t { var args = new DoAfterArgs(EntityManager, user, component.ImplantTime, new ImplantEvent(), implanter, target: target, used: implanter) { - BreakOnUserMove = true, - BreakOnTargetMove = true, BreakOnDamage = true, + BreakOnMove = true, NeedHand = true, }; @@ -126,9 +125,8 @@ public void TryDraw(ImplanterComponent component, EntityUid user, EntityUid targ { var args = new DoAfterArgs(EntityManager, user, component.DrawTime, new DrawEvent(), implanter, target: target, used: implanter) { - BreakOnUserMove = true, - BreakOnTargetMove = true, BreakOnDamage = true, + BreakOnMove = true, NeedHand = true, }; diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index e8af08b2ebb..88c5fb94592 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -20,6 +20,7 @@ using System.Numerics; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Store.Components; using Robust.Shared.Collections; using Robust.Shared.Map.Components; diff --git a/Content.Server/Info/RulesManager.cs b/Content.Server/Info/RulesManager.cs new file mode 100644 index 00000000000..168e5846b96 --- /dev/null +++ b/Content.Server/Info/RulesManager.cs @@ -0,0 +1,44 @@ +using System.Net; +using Content.Server.Database; +using Content.Shared.CCVar; +using Content.Shared.Info; +using Robust.Shared.Configuration; +using Robust.Shared.Network; + +namespace Content.Server.Info; + +public sealed class RulesManager +{ + [Dependency] private readonly IServerDbManager _dbManager = default!; + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private static DateTime LastValidReadTime => DateTime.UtcNow - TimeSpan.FromDays(60); + + public void Initialize() + { + _netManager.Connected += OnConnected; + _netManager.RegisterNetMessage<ShowRulesPopupMessage>(); + _netManager.RegisterNetMessage<RulesAcceptedMessage>(OnRulesAccepted); + } + + private async void OnConnected(object? sender, NetChannelArgs e) + { + if (IPAddress.IsLoopback(e.Channel.RemoteEndPoint.Address) && _cfg.GetCVar(CCVars.RulesExemptLocal)) + return; + + var lastRead = await _dbManager.GetLastReadRules(e.Channel.UserId); + if (lastRead > LastValidReadTime) + return; + + var message = new ShowRulesPopupMessage(); + message.PopupTime = _cfg.GetCVar(CCVars.RulesWaitTime); + _netManager.ServerSendMessage(message, e.Channel); + } + + private async void OnRulesAccepted(RulesAcceptedMessage message) + { + var date = DateTime.UtcNow; + await _dbManager.SetLastReadRules(message.MsgChannel.UserId, date); + } +} diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 914c8589916..f8dc6436c3a 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -11,6 +11,7 @@ using Content.Server.DiscordAuth; using Content.Server.EUI; using Content.Server.GhostKick; +using Content.Server.Info; using Content.Server.Maps; using Content.Server.Players.JobWhitelist; using Content.Server.MoMMI; @@ -54,6 +55,7 @@ public static void Register() IoCManager.Register<IPlayerLocator, PlayerLocator>(); IoCManager.Register<IAfkManager, AfkManager>(); IoCManager.Register<IGameMapManager, GameMapManager>(); + IoCManager.Register<RulesManager, RulesManager>(); IoCManager.Register<IBanManager, BanManager>(); IoCManager.Register<ContentNetworkResourceManager>(); IoCManager.Register<IAdminNotesManager, AdminNotesManager>(); diff --git a/Content.Server/Item/ItemToggle/Components/ItemToggleDisarmMalusComponent.cs b/Content.Server/Item/ItemToggle/Components/ItemToggleDisarmMalusComponent.cs deleted file mode 100644 index 30fa84ed90b..00000000000 --- a/Content.Server/Item/ItemToggle/Components/ItemToggleDisarmMalusComponent.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Content.Server.Item; - -/// <summary> -/// Handles whether this item applies a disarm malus when active. -/// </summary> -[RegisterComponent] -public sealed partial class ItemToggleDisarmMalusComponent : Component -{ - /// <summary> - /// Item has this modifier to the chance to disarm when activated. - /// If null, the value will be inferred from the current malus just before the malus is first deactivated. - /// </summary> - [ViewVariables(VVAccess.ReadOnly), DataField] - public float? ActivatedDisarmMalus = null; - - /// <summary> - /// Item has this modifier to the chance to disarm when deactivated. If none is mentioned, it uses the item's default disarm modifier. - /// If null, the value will be inferred from the current malus just before the malus is first activated. - /// </summary> - [ViewVariables(VVAccess.ReadOnly), DataField] - public float? DeactivatedDisarmMalus = null; -} diff --git a/Content.Server/Item/ItemToggle/Components/ItemToggleSharpComponent.cs b/Content.Server/Item/ItemToggle/Components/ItemToggleSharpComponent.cs deleted file mode 100644 index 227491b16c2..00000000000 --- a/Content.Server/Item/ItemToggle/Components/ItemToggleSharpComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Content.Server.Item; - -/// <summary> -/// Handles whether this item is sharp when toggled on. -/// </summary> -[RegisterComponent] -public sealed partial class ItemToggleSharpComponent : Component -{ -} diff --git a/Content.Server/Item/ItemToggle/ItemToggleSystem.cs b/Content.Server/Item/ItemToggle/ItemToggleSystem.cs deleted file mode 100644 index f98415eb08f..00000000000 --- a/Content.Server/Item/ItemToggle/ItemToggleSystem.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Server.CombatMode.Disarm; -using Content.Server.Kitchen.Components; -using Content.Shared.Item.ItemToggle; -using Content.Shared.Item.ItemToggle.Components; - -namespace Content.Server.Item; - -public sealed class ItemToggleSystem : SharedItemToggleSystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<ItemToggleSharpComponent, ItemToggledEvent>(ToggleSharp); - SubscribeLocalEvent<ItemToggleDisarmMalusComponent, ItemToggledEvent>(ToggleMalus); - } - - private void ToggleSharp(Entity<ItemToggleSharpComponent> ent, ref ItemToggledEvent args) - { - // TODO generalize this into a "ToggleComponentComponent", though probably with a better name - if (args.Activated) - EnsureComp<SharpComponent>(ent); - else - RemCompDeferred<SharpComponent>(ent); - } - - private void ToggleMalus(Entity<ItemToggleDisarmMalusComponent> ent, ref ItemToggledEvent args) - { - if (!TryComp<DisarmMalusComponent>(ent, out var malus)) - return; - - if (args.Activated) - { - ent.Comp.DeactivatedDisarmMalus ??= malus.Malus; - if (ent.Comp.ActivatedDisarmMalus is {} activatedMalus) - malus.Malus = activatedMalus; - return; - } - - ent.Comp.ActivatedDisarmMalus ??= malus.Malus; - if (ent.Comp.DeactivatedDisarmMalus is {} deactivatedMalus) - malus.Malus = deactivatedMalus; - } -} diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs index 1e343e5e332..7e8ddd8b457 100644 --- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs +++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs @@ -105,6 +105,12 @@ public sealed partial class MicrowaveComponent : Component /// </summary> [DataField] public float LightningChance = .75f; + + /// <summary> + /// If this microwave can give ids accesses without exploding + /// </summary> + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool CanMicrowaveIdsSafely = true; } public sealed class BeingMicrowavedEvent : HandledEntityEventArgs diff --git a/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs b/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs index 0419e13d230..2ca7cdbd85d 100644 --- a/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs @@ -2,6 +2,8 @@ using Content.Server.Body.Systems; using Content.Server.Kitchen.Components; using Content.Server.Popups; +using Content.Shared.Chat; +using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.DragDrop; @@ -16,7 +18,6 @@ using Content.Shared.Popups; using Content.Shared.Storage; using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Random; @@ -36,6 +37,7 @@ public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedSuicideSystem _suicide = default!; public override void Initialize() { @@ -48,31 +50,38 @@ public override void Initialize() //DoAfter SubscribeLocalEvent<KitchenSpikeComponent, SpikeDoAfterEvent>(OnDoAfter); - SubscribeLocalEvent<KitchenSpikeComponent, SuicideEvent>(OnSuicide); + SubscribeLocalEvent<KitchenSpikeComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment); SubscribeLocalEvent<ButcherableComponent, CanDropDraggedEvent>(OnButcherableCanDrop); } - private void OnButcherableCanDrop(EntityUid uid, ButcherableComponent component, ref CanDropDraggedEvent args) + private void OnButcherableCanDrop(Entity<ButcherableComponent> entity, ref CanDropDraggedEvent args) { args.Handled = true; - args.CanDrop |= component.Type != ButcheringType.Knife; + args.CanDrop |= entity.Comp.Type != ButcheringType.Knife; } - private void OnSuicide(EntityUid uid, KitchenSpikeComponent component, SuicideEvent args) + /// <summary> + /// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them. + /// </summary> + private void OnSuicideByEnvironment(Entity<KitchenSpikeComponent> entity, ref SuicideByEnvironmentEvent args) { if (args.Handled) return; - args.SetHandled(SuicideKind.Piercing); - var victim = args.Victim; - var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", victim)); - _popupSystem.PopupEntity(othersMessage, victim); + + if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent)) + return; + + _suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Piercing"); + var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", args.Victim)); + _popupSystem.PopupEntity(othersMessage, args.Victim, Filter.PvsExcept(args.Victim), true); var selfMessage = Loc.GetString("comp-kitchen-spike-suicide-self"); - _popupSystem.PopupEntity(selfMessage, victim, victim); + _popupSystem.PopupEntity(selfMessage, args.Victim, args.Victim); + args.Handled = true; } - private void OnDoAfter(EntityUid uid, KitchenSpikeComponent component, DoAfterEvent args) + private void OnDoAfter(Entity<KitchenSpikeComponent> entity, ref SpikeDoAfterEvent args) { if (args.Args.Target == null) return; @@ -82,49 +91,49 @@ private void OnDoAfter(EntityUid uid, KitchenSpikeComponent component, DoAfterEv if (args.Cancelled) { - component.InUse = false; + entity.Comp.InUse = false; return; } if (args.Handled) return; - if (Spikeable(uid, args.Args.User, args.Args.Target.Value, component, butcherable)) - Spike(uid, args.Args.User, args.Args.Target.Value, component); + if (Spikeable(entity, args.Args.User, args.Args.Target.Value, entity.Comp, butcherable)) + Spike(entity, args.Args.User, args.Args.Target.Value, entity.Comp); - component.InUse = false; + entity.Comp.InUse = false; args.Handled = true; } - private void OnDragDrop(EntityUid uid, KitchenSpikeComponent component, ref DragDropTargetEvent args) + private void OnDragDrop(Entity<KitchenSpikeComponent> entity, ref DragDropTargetEvent args) { if (args.Handled) return; args.Handled = true; - if (Spikeable(uid, args.User, args.Dragged, component)) - TrySpike(uid, args.User, args.Dragged, component); + if (Spikeable(entity, args.User, args.Dragged, entity.Comp)) + TrySpike(entity, args.User, args.Dragged, entity.Comp); } - private void OnInteractHand(EntityUid uid, KitchenSpikeComponent component, InteractHandEvent args) + private void OnInteractHand(Entity<KitchenSpikeComponent> entity, ref InteractHandEvent args) { if (args.Handled) return; - if (component.PrototypesToSpawn?.Count > 0) + if (entity.Comp.PrototypesToSpawn?.Count > 0) { - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User); + _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), entity, args.User); args.Handled = true; } } - private void OnInteractUsing(EntityUid uid, KitchenSpikeComponent component, InteractUsingEvent args) + private void OnInteractUsing(Entity<KitchenSpikeComponent> entity, ref InteractUsingEvent args) { if (args.Handled) return; - if (TryGetPiece(uid, args.User, args.Used)) + if (TryGetPiece(entity, args.User, args.Used)) args.Handled = true; } @@ -257,9 +266,8 @@ public bool TrySpike(EntityUid uid, EntityUid userUid, EntityUid victimUid, Kitc var doAfterArgs = new DoAfterArgs(EntityManager, userUid, component.SpikeDelay + butcherable.ButcherDelay, new SpikeDoAfterEvent(), uid, target: victimUid, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, BreakOnDamage = true, + BreakOnMove = true, NeedHand = true }; diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 40be61695fc..84ec850a3e8 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -1,3 +1,4 @@ +using Content.Server.Administration.Logs; using Content.Server.Body.Systems; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Construction; @@ -15,6 +16,7 @@ using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Construction.EntitySystems; +using Content.Shared.Database; using Content.Shared.Destructible; using Content.Shared.FixedPoint; using Content.Shared.Interaction; @@ -33,8 +35,14 @@ using Robust.Shared.Containers; using Robust.Shared.Player; using System.Linq; +using Content.Server.Administration.Logs; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Content.Shared.Stacks; +using Content.Server.Construction.Components; +using Content.Shared.Chat; +using Content.Shared.Damage; +using Robust.Shared.Utility; namespace Content.Server.Kitchen.EntitySystems { @@ -58,6 +66,10 @@ public sealed class MicrowaveSystem : EntitySystem [Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly HandsSystem _handsSystem = default!; [Dependency] private readonly SharedItemSystem _item = default!; + [Dependency] private readonly SharedStackSystem _stack = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedSuicideSystem _suicide = default!; [ValidatePrototypeId<EntityPrototype>] private const string MalfunctionSpark = "Spark"; @@ -75,7 +87,7 @@ public override void Initialize() SubscribeLocalEvent<MicrowaveComponent, BreakageEventArgs>(OnBreak); SubscribeLocalEvent<MicrowaveComponent, PowerChangedEvent>(OnPowerChanged); SubscribeLocalEvent<MicrowaveComponent, AnchorStateChangedEvent>(OnAnchorChanged); - SubscribeLocalEvent<MicrowaveComponent, SuicideEvent>(OnSuicide); + SubscribeLocalEvent<MicrowaveComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment); SubscribeLocalEvent<MicrowaveComponent, RefreshPartsEvent>(OnRefreshParts); SubscribeLocalEvent<MicrowaveComponent, UpgradeExamineEvent>(OnUpgradeExamine); @@ -92,6 +104,8 @@ public override void Initialize() SubscribeLocalEvent<ActiveMicrowaveComponent, EntRemovedFromContainerMessage>(OnActiveMicrowaveRemove); SubscribeLocalEvent<ActivelyMicrowavedComponent, OnConstructionTemperatureEvent>(OnConstructionTemp); + + SubscribeLocalEvent<FoodRecipeProviderComponent, GetSecretRecipesEvent>(OnGetSecretRecipes); } private void OnCookStart(Entity<ActiveMicrowaveComponent> ent, ref ComponentStartup args) @@ -99,7 +113,7 @@ private void OnCookStart(Entity<ActiveMicrowaveComponent> ent, ref ComponentStar if (!TryComp<MicrowaveComponent>(ent, out var microwaveComponent)) return; SetAppearance(ent.Owner, MicrowaveVisualState.Cooking, microwaveComponent); - + var audio = _audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5)); if (audio == null) @@ -233,12 +247,22 @@ private void OnMapInit(Entity<MicrowaveComponent> ent, ref MapInitEvent args) _deviceLink.EnsureSinkPorts(ent, ent.Comp.OnPort); } - private void OnSuicide(Entity<MicrowaveComponent> ent, ref SuicideEvent args) + /// <summary> + /// Kills the user by microwaving their head + /// TODO: Make this not awful, it keeps any items attached to your head still on and you can revive someone and cogni them so you have some dumb headless fuck running around. I've seen it happen. + /// </summary> + private void OnSuicideByEnvironment(Entity<MicrowaveComponent> ent, ref SuicideByEnvironmentEvent args) { if (args.Handled) return; - args.SetHandled(SuicideKind.Heat); + // The act of getting your head microwaved doesn't actually kill you + if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent)) + return; + + // The application of lethal damage is what kills you... + _suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat"); + var victim = args.Victim; var headCount = 0; @@ -268,6 +292,7 @@ private void OnSuicide(Entity<MicrowaveComponent> ent, ref SuicideEvent args) ent.Comp.CurrentCookTimerTime = 10; Wzhzhzh(ent.Owner, ent.Comp, args.Victim); UpdateUserInterfaceState(ent.Owner, ent.Comp); + args.Handled = true; } private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionContainerChangedEvent args) @@ -397,6 +422,23 @@ public static bool HasContents(MicrowaveComponent component) return component.Storage.ContainedEntities.Any(); } + /// <summary> + /// Explodes the microwave internally, turning it into a broken state, destroying its board, and spitting out its machine parts + /// </summary> + /// <param name="ent"></param> + public void Explode(Entity<MicrowaveComponent> ent) + { + ent.Comp.Broken = true; // Make broken so we stop processing stuff + _explosion.TriggerExplosive(ent); + if (TryComp<MachineComponent>(ent, out var machine)) + { + _container.CleanContainer(machine.BoardContainer); + _container.EmptyContainer(machine.PartContainer); + } + + _adminLogger.Add(LogType.Action, LogImpact.Medium, + $"{ToPrettyString(ent)} exploded from unsafe cooking!"); + } /// <summary> /// Handles the attempted cooking of unsafe objects /// </summary> @@ -414,7 +456,7 @@ private void RollMalfunction(Entity<ActiveMicrowaveComponent, MicrowaveComponent ent.Comp1.MalfunctionTime = _gameTiming.CurTime + TimeSpan.FromSeconds(ent.Comp2.MalfunctionInterval); if (_random.Prob(ent.Comp2.ExplosionChance)) { - _explosion.TriggerExplosive(ent); + Explode((ent, ent.Comp2)); return; // microwave is fucked, stop the cooking. } @@ -495,7 +537,12 @@ public void Wzhzhzh(EntityUid uid, MicrowaveComponent component, EntityUid? user } // Check recipes - var portionedRecipe = _recipeManager.Recipes.Select(r => + var getRecipesEv = new GetSecretRecipesEvent(); + RaiseLocalEvent(uid, ref getRecipesEv); + + List<FoodRecipePrototype> recipes = getRecipesEv.Recipes; + recipes.AddRange(_recipeManager.Recipes); + var portionedRecipe = recipes.Select(r => CanSatisfyRecipe(component, r, solidsDict, reagentDict)).FirstOrDefault(r => r.Item2 > 0); _audio.PlayPvs(component.StartCookingSound, uid); @@ -503,7 +550,8 @@ public void Wzhzhzh(EntityUid uid, MicrowaveComponent component, EntityUid? user activeComp.CookTimeRemaining = component.CurrentCookTimerTime * component.CookTimeMultiplier; activeComp.TotalTime = component.CurrentCookTimerTime; //this doesn't scale so that we can have the "actual" time activeComp.PortionedRecipe = portionedRecipe; - component.CurrentCookTimeEnd = _gameTiming.CurTime + TimeSpan.FromSeconds(component.CurrentCookTimerTime); + //Scale tiems with cook times + component.CurrentCookTimeEnd = _gameTiming.CurTime + TimeSpan.FromSeconds(component.CurrentCookTimerTime * component.CookTimeMultiplier); if (malfunctioning) activeComp.MalfunctionTime = _gameTiming.CurTime + TimeSpan.FromSeconds(component.MalfunctionInterval); UpdateUserInterfaceState(uid, component); @@ -599,6 +647,21 @@ public override void Update(float frameTime) } } + /// <summary> + /// This event tries to get secret recipes that the microwave might be capable of. + /// Currently, we only check the microwave itself, but in the future, the user might be able to learn recipes. + /// </summary> + private void OnGetSecretRecipes(Entity<FoodRecipeProviderComponent> ent, ref GetSecretRecipesEvent args) + { + foreach (ProtoId<FoodRecipePrototype> recipeId in ent.Comp.ProvidedRecipes) + { + if (_prototype.TryIndex(recipeId, out var recipeProto)) + { + args.Recipes.Add(recipeProto); + } + } + } + #region ui private void OnEjectMessage(Entity<MicrowaveComponent> ent, ref MicrowaveEjectMessage args) { diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index 0d6fb3fb90c..4a118e696fa 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -22,6 +22,7 @@ using System.Linq; using Content.Server.Jittering; using Content.Shared.Jittering; +using Content.Shared.Power; namespace Content.Server.Kitchen.EntitySystems { diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index 81806a0c818..b7966877214 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Robust.Server.Containers; +using Robust.Server.GameObjects; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -28,6 +29,7 @@ public sealed class SharpSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly ContainerSystem _containerSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; @@ -73,9 +75,8 @@ private bool TryStartButcherDoafter(EntityUid knife, EntityUid target, EntityUid var doAfter = new DoAfterArgs(EntityManager, user, sharp.ButcherDelayModifier * butcher.ButcherDelay, new SharpDoAfterEvent(), knife, target: target, used: knife) { - BreakOnTargetMove = true, - BreakOnUserMove = true, BreakOnDamage = true, + BreakOnMove = true, NeedHand = true }; _doAfterSystem.TryStartDoAfter(doAfter); @@ -102,7 +103,7 @@ private void OnDoAfter(EntityUid uid, SharpComponent component, DoAfterEvent arg } var spawnEntities = EntitySpawnCollection.GetSpawns(butcher.SpawnedEntities, _robustRandom); - var coords = Transform(args.Args.Target.Value).MapPosition; + var coords = _transform.GetMapCoordinates(args.Args.Target.Value); EntityUid popupEnt = default!; foreach (var proto in spawnEntities) { diff --git a/Content.Server/Labels/Label/LabelSystem.cs b/Content.Server/Labels/Label/LabelSystem.cs index 9d235b488b8..145a2aad3de 100644 --- a/Content.Server/Labels/Label/LabelSystem.cs +++ b/Content.Server/Labels/Label/LabelSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Labels; using Content.Shared.Labels.Components; using Content.Shared.Labels.EntitySystems; +using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Tag; using JetBrains.Annotations; using Robust.Shared.Containers; @@ -19,7 +20,7 @@ public sealed class LabelSystem : SharedLabelSystem { [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; [Dependency] private readonly TagSystem _tagSystem = default!; public const string ContainerName = "paper_label"; @@ -45,6 +46,8 @@ private void OnLabelCompMapInit(EntityUid uid, LabelComponent component, MapInit component.CurrentLabel = Loc.GetString(component.CurrentLabel); Dirty(uid, component); } + + _nameMod.RefreshNameModifiers(uid); } /// <summary> @@ -58,30 +61,15 @@ public override void Label(EntityUid uid, string? text, MetaDataComponent? metad { if (!Resolve(uid, ref metadata)) return; + if (_tagSystem.HasTag(uid, PreventTag)) // DeltaV - Prevent labels on certain items return; + if (!Resolve(uid, ref label, false)) label = EnsureComp<LabelComponent>(uid); - if (string.IsNullOrEmpty(text)) - { - if (label.OriginalName is null) - return; - - // Remove label - _metaData.SetEntityName(uid, label.OriginalName, metadata); - label.CurrentLabel = null; - label.OriginalName = null; - - Dirty(uid, label); - - return; - } - - // Update label - label.OriginalName ??= metadata.EntityName; label.CurrentLabel = text; - _metaData.SetEntityName(uid, $"{label.OriginalName} ({text})", metadata); + _nameMod.RefreshNameModifiers(uid); Dirty(uid, label); } diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 5cb5c8cd2e9..22184dec3a7 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -3,12 +3,14 @@ using Content.Server.PowerCell; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Language; using Content.Shared.Language.Components; using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; using Content.Shared.Language.Events; +using Content.Shared.PowerCell.Components; using Robust.Shared.Containers; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -33,6 +35,8 @@ public override void Initialize() SubscribeLocalEvent<HandheldTranslatorComponent, EntParentChangedMessage>(OnTranslatorParentChanged); SubscribeLocalEvent<HandheldTranslatorComponent, ActivateInWorldEvent>(OnTranslatorToggle); SubscribeLocalEvent<HandheldTranslatorComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); + SubscribeLocalEvent<HandheldTranslatorComponent, PowerCellChangedEvent>(OnPowerCellChanged); + SubscribeLocalEvent<HandheldTranslatorComponent, ItemToggledEvent>(OnItemToggled); } private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) @@ -102,7 +106,7 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen var isEnabled = !translatorComp.Enabled && hasPower; translatorComp.Enabled = isEnabled; - _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); + _powerCell.SetDrawEnabled(translator, isEnabled); if (_containers.TryGetContainingContainer(translator, out var holderCont) && holderCont.Owner is var holder @@ -130,13 +134,33 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorComponent component, PowerCellSlotEmptyEvent args) { component.Enabled = false; - _powerCell.SetPowerCellDrawEnabled(translator, false); + _powerCell.SetDrawEnabled(translator, false); OnAppearanceChange(translator, component); if (_containers.TryGetContainingContainer(translator, out var holderCont) && HasComp<LanguageSpeakerComponent>(holderCont.Owner)) _language.UpdateEntityLanguages(holderCont.Owner); } + private void OnPowerCellChanged(EntityUid translator, HandheldTranslatorComponent component, PowerCellChangedEvent args) + { + component.Enabled = !args.Ejected; + _powerCell.SetDrawEnabled(translator, !args.Ejected); + OnAppearanceChange(translator, component); + + if (_containers.TryGetContainingContainer((translator, null, null), out var holderCont) && HasComp<LanguageSpeakerComponent>(holderCont.Owner)) + _language.UpdateEntityLanguages(holderCont.Owner); + } + + private void OnItemToggled(EntityUid translator, HandheldTranslatorComponent component, ItemToggledEvent args) + { + component.Enabled = args.Activated; + _powerCell.SetDrawEnabled(translator, args.Activated); + OnAppearanceChange(translator, component); + + if (_containers.TryGetContainingContainer((translator, null, null), out var holderCont) && HasComp<LanguageSpeakerComponent>(holderCont.Owner)) + _language.UpdateEntityLanguages(holderCont.Owner); + } + private void CopyLanguages(BaseTranslatorComponent from, DetermineEntityLanguagesEvent to, LanguageKnowledgeComponent knowledge) { var addSpoken = CheckLanguagesMatch(from.RequiredLanguages, knowledge.SpokenLanguages, from.RequiresAllLanguages); diff --git a/Content.Server/Lathe/LatheSystem.cs b/Content.Server/Lathe/LatheSystem.cs index 7448a9b84dd..dd5910cb0d0 100644 --- a/Content.Server/Lathe/LatheSystem.cs +++ b/Content.Server/Lathe/LatheSystem.cs @@ -1,23 +1,30 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Administration.Logs; -using Content.Server.Atmos; using Content.Server.Atmos.EntitySystems; +using Content.Server.Fluids.EntitySystems; using Content.Server.Lathe.Components; using Content.Server.Materials; +using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Stack; using Content.Shared.Atmos; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; using Content.Shared.UserInterface; using Content.Shared.Database; using Content.Shared.Emag.Components; +using Content.Shared.Examine; using Content.Shared.Lathe; using Content.Shared.Materials; +using Content.Shared.Power; using Content.Shared.ReagentSpeed; using Content.Shared.Research.Components; using Content.Shared.Research.Prototypes; using JetBrains.Annotations; +using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; @@ -34,9 +41,13 @@ public sealed class LatheSystem : SharedLatheSystem [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly UserInterfaceSystem _uiSys = default!; [Dependency] private readonly MaterialStorageSystem _materialStorage = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly PuddleSystem _puddle = default!; [Dependency] private readonly ReagentSpeedSystem _reagentSpeed = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly TransformSystem _transform = default!; @@ -63,7 +74,6 @@ public override void Initialize() SubscribeLocalEvent<EmagLatheRecipesComponent, LatheGetRecipesEvent>(GetEmagLatheRecipes); SubscribeLocalEvent<LatheHeatProducingComponent, LatheStartPrintingEvent>(OnHeatStartPrinting); } - public override void Update(float frameTime) { var query = EntityQueryEnumerator<LatheProducingComponent, LatheComponent>(); @@ -119,7 +129,7 @@ private void OnGetWhitelist(EntityUid uid, LatheComponent component, ref GetMate { if (!_proto.TryIndex(id, out var proto)) continue; - foreach (var (mat, _) in proto.RequiredMaterials) + foreach (var (mat, _) in proto.Materials) { if (!materialWhitelist.Contains(mat)) { @@ -165,7 +175,7 @@ public bool TryAddToQueue(EntityUid uid, LatheRecipePrototype recipe, LatheCompo if (!CanProduce(uid, recipe, 1, component)) return false; - foreach (var (mat, amount) in recipe.RequiredMaterials) + foreach (var (mat, amount) in recipe.Materials) { var adjustedAmount = recipe.ApplyMaterialDiscount ? (int) (-amount * component.MaterialUseMultiplier) @@ -188,11 +198,11 @@ public bool TryStartProducing(EntityUid uid, LatheComponent? component = null) var recipe = component.Queue.First(); component.Queue.RemoveAt(0); - var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime); + var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime) * component.TimeMultiplier; var lathe = EnsureComp<LatheProducingComponent>(uid); lathe.StartTime = _timing.CurTime; - lathe.ProductionLength = time * component.TimeMultiplier; + lathe.ProductionLength = time; component.CurrentRecipe = recipe; var ev = new LatheStartPrintingEvent(recipe); @@ -201,6 +211,11 @@ public bool TryStartProducing(EntityUid uid, LatheComponent? component = null) _audio.PlayPvs(component.ProducingSound, uid); UpdateRunningAppearance(uid, true); UpdateUserInterfaceState(uid, component); + + if (time == TimeSpan.Zero) + { + FinishProducing(uid, component, lathe); + } return true; } @@ -211,8 +226,31 @@ public void FinishProducing(EntityUid uid, LatheComponent? comp = null, LathePro if (comp.CurrentRecipe != null) { - var result = Spawn(comp.CurrentRecipe.Result, Transform(uid).Coordinates); - _stack.TryMergeToContacts(result); + if (comp.CurrentRecipe.Result is { } resultProto) + { + var result = Spawn(resultProto, Transform(uid).Coordinates); + _stack.TryMergeToContacts(result); + } + + if (comp.CurrentRecipe.ResultReagents is { } resultReagents && + comp.ReagentOutputSlotId is { } slotId) + { + var toAdd = new Solution( + resultReagents.Select(p => new ReagentQuantity(p.Key.Id, p.Value, null))); + + // dispense it in the container if we have it and dump it if we don't + if (_container.TryGetContainer(uid, slotId, out var container) && + container.ContainedEntities.Count == 1 && + _solution.TryGetFitsInDispenser(container.ContainedEntities.First(), out var solution, out _)) + { + _solution.AddSolution(solution.Value, toAdd); + } + else + { + _popup.PopupEntity(Loc.GetString("lathe-reagent-dispense-no-container", ("name", uid)), uid); + _puddle.TrySpillAt(uid, toAdd, out _); + } + } } comp.CurrentRecipe = null; @@ -327,6 +365,7 @@ protected override bool HasRecipe(EntityUid uid, LatheRecipePrototype recipe, La { return GetAvailableRecipes(uid, component).Contains(recipe.ID); } + #region UI Messages private void OnLatheQueueRecipeMessage(EntityUid uid, LatheComponent component, LatheQueueRecipeMessage args) @@ -343,8 +382,9 @@ private void OnLatheQueueRecipeMessage(EntityUid uid, LatheComponent component, } if (count > 0) { - _adminLogger.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(args.Actor):player} queued {count} {recipe.Name} at {ToPrettyString(uid):lathe}"); + _adminLogger.Add(LogType.Action, + LogImpact.Low, + $"{ToPrettyString(args.Actor):player} queued {count} {GetRecipeName(recipe)} at {ToPrettyString(uid):lathe}"); } } TryStartProducing(uid, component); diff --git a/Content.Server/LifeDrainer/LifeDrainerSystem.cs b/Content.Server/LifeDrainer/LifeDrainerSystem.cs index c5955d78039..3c0089b190e 100644 --- a/Content.Server/LifeDrainer/LifeDrainerSystem.cs +++ b/Content.Server/LifeDrainer/LifeDrainerSystem.cs @@ -1,6 +1,5 @@ using Content.Server.Abilities.Psionics; using Content.Server.Carrying; -using Content.Server.NPC.Systems; using Content.Shared.ActionBlocker; using Content.Shared.Damage; using Content.Shared.DoAfter; @@ -14,6 +13,8 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Utility; +using Content.Shared.NPC.Systems; + namespace Content.Server.LifeDrainer; @@ -122,8 +123,7 @@ public bool TryDrain(Entity<LifeDrainerComponent> ent, EntityUid target) var ev = new LifeDrainDoAfterEvent(); var args = new DoAfterArgs(EntityManager, uid, comp.Delay, ev, target: target, eventTarget: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, MovementThreshold = 2f, NeedHand = false }; diff --git a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs index f2c4c7dcc59..498fe3b6f4b 100644 --- a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs +++ b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Examine; using Content.Shared.Light; using Content.Shared.Light.Components; +using Content.Shared.Power; using Robust.Server.GameObjects; using Color = Robust.Shared.Maths.Color; @@ -234,6 +235,6 @@ private void TurnOn(Entity<EmergencyLightComponent> entity, Color color) _pointLight.SetColor(entity.Owner, color); _appearance.SetData(entity.Owner, EmergencyLightVisuals.Color, color); _appearance.SetData(entity.Owner, EmergencyLightVisuals.On, true); - _ambient.SetAmbience(entity.Owner, true); + _ambient.SetAmbience(entity.Owner, true); } } diff --git a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs index 752fb8f5fe6..3c5f7eaecb2 100644 --- a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs +++ b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs @@ -1,6 +1,8 @@ using Content.Server.Light.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; +using Content.Shared.Power; +using Content.Shared.Power.Components; namespace Content.Server.Light.EntitySystems { diff --git a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs index e880cec5bbb..c4a07b56a86 100644 --- a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs +++ b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs @@ -24,6 +24,10 @@ using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Audio.Systems; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; +using Content.Shared.Power; +using Content.Shared.Power.Components; namespace Content.Server.Light.EntitySystems { @@ -150,7 +154,7 @@ private void OnInteractHand(EntityUid uid, PoweredLightComponent light, Interact // removing a working bulb, so require a delay _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, userUid, light.EjectBulbDelay, new PoweredLightDoAfterEvent(), uid, target: uid) { - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, }); diff --git a/Content.Server/Lightning/LightningSystem.cs b/Content.Server/Lightning/LightningSystem.cs index 3c0da8e9149..5a7b85d26ef 100644 --- a/Content.Server/Lightning/LightningSystem.cs +++ b/Content.Server/Lightning/LightningSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Beam.Components; using Content.Server.Lightning.Components; using Content.Shared.Lightning; +using Robust.Server.GameObjects; using Robust.Shared.Random; namespace Content.Server.Lightning; @@ -74,13 +75,10 @@ public void ShootRandomLightnings(EntityUid user, float range, int boltCount, st //To Do: Remove Hardcode LightningTargetComponent (this should be a parameter of the SharedLightningComponent) //To Do: This is still pretty bad for perf but better than before and at least it doesn't re-allocate // several hashsets every time - - var userCoords = _transform.GetMapCoordinates(user); - var targetEnts = _lookup.GetEntitiesInRange<LightningTargetComponent>(userCoords, range); - var targets = targetEnts.Select(x => x.Comp).ToList(); - + var mapCoords = _transform.GetMapCoordinates(user); + var targets = _lookup.GetEntitiesInRange<LightningTargetComponent>(mapCoords, range).ToList(); _random.Shuffle(targets); - targets.Sort((x, y) => y.Priority.CompareTo(x.Priority)); + targets.Sort((x, y) => y.Comp.Priority.CompareTo(x.Comp.Priority)); int shotCount = 0; int count = -1; @@ -93,13 +91,13 @@ public void ShootRandomLightnings(EntityUid user, float range, int boltCount, st var curTarget = targets[count]; // Chance to ignore target - if (!_random.Prob(curTarget.HitProbability)) + if (!_random.Prob(curTarget.Comp.HitProbability)) continue; ShootLightning(user, targets[count].Owner, lightningPrototype, triggerLightningEvents); - if (arcDepth - targets[count].LightningResistance > 0) - ShootRandomLightnings(targets[count].Owner, range, 1, lightningPrototype, arcDepth - targets[count].LightningResistance, triggerLightningEvents); + if (arcDepth - targets[count].Comp.LightningResistance > 0) + ShootRandomLightnings(targets[count].Owner, range, 1, lightningPrototype, arcDepth - targets[count].Comp.LightningResistance, triggerLightningEvents); shotCount++; } diff --git a/Content.Server/Lightning/LightningTargetSystem.cs b/Content.Server/Lightning/LightningTargetSystem.cs index ccaa74e9e26..bc99def9742 100644 --- a/Content.Server/Lightning/LightningTargetSystem.cs +++ b/Content.Server/Lightning/LightningTargetSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Lightning; using Content.Server.Lightning.Components; using Content.Shared.Damage; +using Robust.Server.GameObjects; namespace Content.Server.Tesla.EntitySystems; @@ -12,6 +13,7 @@ public sealed class LightningTargetSystem : EntitySystem { [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly ExplosionSystem _explosionSystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; public override void Initialize() { @@ -29,7 +31,7 @@ private void OnHitByLightning(Entity<LightningTargetComponent> uid, ref HitByLig if (uid.Comp.LightningExplode) { _explosionSystem.QueueExplosion( - Transform(uid).MapPosition, + _transform.GetMapCoordinates(uid), uid.Comp.ExplosionPrototype, uid.Comp.TotalIntensity, uid.Comp.Dropoff, uid.Comp.MaxTileIntensity, diff --git a/Content.Server/Lube/LubedSystem.cs b/Content.Server/Lube/LubedSystem.cs index f786c5f91af..3c536dcceb2 100644 --- a/Content.Server/Lube/LubedSystem.cs +++ b/Content.Server/Lube/LubedSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.IdentityManagement; using Content.Shared.Lube; +using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Popups; using Content.Shared.Throwing; using Robust.Shared.Containers; @@ -9,11 +10,11 @@ namespace Content.Server.Lube; public sealed class LubedSystem : EntitySystem { - [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly ThrowingSystem _throwing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; public override void Initialize() { @@ -21,14 +22,12 @@ public override void Initialize() SubscribeLocalEvent<LubedComponent, ComponentInit>(OnInit); SubscribeLocalEvent<LubedComponent, ContainerGettingInsertedAttemptEvent>(OnHandPickUp); + SubscribeLocalEvent<LubedComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } private void OnInit(EntityUid uid, LubedComponent component, ComponentInit args) { - var meta = MetaData(uid); - var name = meta.EntityName; - component.BeforeLubedEntityName = meta.EntityName; - _metaData.SetEntityName(uid, Loc.GetString("lubed-name-prefix", ("target", name))); + _nameMod.RefreshNameModifiers(uid); } private void OnHandPickUp(EntityUid uid, LubedComponent component, ContainerGettingInsertedAttemptEvent args) @@ -36,7 +35,7 @@ private void OnHandPickUp(EntityUid uid, LubedComponent component, ContainerGett if (component.SlipsLeft <= 0) { RemComp<LubedComponent>(uid); - _metaData.SetEntityName(uid, component.BeforeLubedEntityName); + _nameMod.RefreshNameModifiers(uid); return; } component.SlipsLeft--; @@ -44,7 +43,12 @@ private void OnHandPickUp(EntityUid uid, LubedComponent component, ContainerGett var user = args.Container.Owner; _transform.SetCoordinates(uid, Transform(user).Coordinates); _transform.AttachToGridOrMap(uid); - _throwing.TryThrow(uid, _random.NextVector2(), strength: component.SlipStrength); + _throwing.TryThrow(uid, _random.NextVector2(), baseThrowSpeed: component.SlipStrength); _popup.PopupEntity(Loc.GetString("lube-slip", ("target", Identity.Entity(uid, EntityManager))), user, user, PopupType.MediumCaution); } + + private void OnRefreshNameModifiers(Entity<LubedComponent> entity, ref RefreshNameModifiersEvent args) + { + args.AddModifier("lubed-name-prefix"); + } } diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index 5997c56e240..c25aada3a07 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -18,6 +18,6 @@ public override void Initialize() private void OnSpellSpoken(ref SpeakSpellEvent args) { - _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), args.ChatType, false); + _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), InGameICChatType.Speak, false); } } diff --git a/Content.Server/MagicMirror/MagicMirrorSystem.cs b/Content.Server/MagicMirror/MagicMirrorSystem.cs index b11e8ca707c..8d8a6bfa3b4 100644 --- a/Content.Server/MagicMirror/MagicMirrorSystem.cs +++ b/Content.Server/MagicMirror/MagicMirrorSystem.cs @@ -59,11 +59,9 @@ private void OnMagicMirrorSelect(EntityUid uid, MagicMirrorComponent component, _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, component.SelectSlotTime, doAfter, uid, target: target, used: uid) { DistanceThreshold = SharedInteractionSystem.InteractionRange, - BreakOnTargetMove = true, BreakOnDamage = true, + BreakOnMove = true, BreakOnHandChange = false, - BreakOnUserMove = true, - BreakOnWeightlessMove = false, NeedHand = true }, out var doAfterId); @@ -116,11 +114,9 @@ private void OnTryMagicMirrorChangeColor(EntityUid uid, MagicMirrorComponent com _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, component.ChangeSlotTime, doAfter, uid, target: target, used: uid) { - BreakOnTargetMove = true, BreakOnDamage = true, + BreakOnMove = true, BreakOnHandChange = false, - BreakOnUserMove = true, - BreakOnWeightlessMove = false, NeedHand = true }, out var doAfterId); @@ -172,11 +168,8 @@ private void OnTryMagicMirrorRemoveSlot(EntityUid uid, MagicMirrorComponent comp _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, component.RemoveSlotTime, doAfter, uid, target: target, used: uid) { DistanceThreshold = SharedInteractionSystem.InteractionRange, - BreakOnTargetMove = true, BreakOnDamage = true, BreakOnHandChange = false, - BreakOnUserMove = true, - BreakOnWeightlessMove = false, NeedHand = true }, out var doAfterId); @@ -227,11 +220,9 @@ private void OnTryMagicMirrorAddSlot(EntityUid uid, MagicMirrorComponent compone _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, component.AddSlotTime, doAfter, uid, target: component.Target.Value, used: uid) { - BreakOnTargetMove = true, BreakOnDamage = true, + BreakOnMove = true, BreakOnHandChange = false, - BreakOnUserMove = true, - BreakOnWeightlessMove = false, NeedHand = true }, out var doAfterId); diff --git a/Content.Server/MassMedia/Systems/NewsSystem.cs b/Content.Server/MassMedia/Systems/NewsSystem.cs index 3246de5316c..ea922b66f5a 100644 --- a/Content.Server/MassMedia/Systems/NewsSystem.cs +++ b/Content.Server/MassMedia/Systems/NewsSystem.cs @@ -1,10 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Server.Access.Systems; using Content.Server.Administration.Logs; using Content.Server.CartridgeLoader; using Content.Server.CartridgeLoader.Cartridges; using Content.Server.GameTicking; using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Systems; +using Content.Server.Chat.Managers; using Content.Server.Popups; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; @@ -20,6 +23,8 @@ using Content.Shared.Popups; using Content.Shared.StationRecords; using Robust.Shared.Audio.Systems; +using Content.Shared.IdentityManagement; +using Robust.Shared.Timing; namespace Content.Server.MassMedia.Systems; @@ -35,6 +40,7 @@ public sealed class NewsSystem : SharedNewsSystem [Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly IdCardSystem _idCardSystem = default!; + [Dependency] private readonly IChatManager _chatManager = default!; public override void Initialize() { @@ -135,9 +141,9 @@ private void OnWriteUiPublishMessage(Entity<NewsWriterComponent> ent, ref NewsWr if (!TryGetArticles(ent, out var articles)) return; - string? authorName = null; - if (_idCardSystem.TryFindIdCard(msg.Actor, out var idCard)) - authorName = idCard.Comp.FullName; + var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(ent, msg.Actor); + RaiseLocalEvent(tryGetIdentityShortInfoEvent); + string? authorName = tryGetIdentityShortInfoEvent.Title; var title = msg.Title.Trim(); var content = msg.Content.Trim(); diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs index 3b23308758d..a4039969785 100644 --- a/Content.Server/Materials/MaterialReclaimerSystem.cs +++ b/Content.Server/Materials/MaterialReclaimerSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Chemistry.EntitySystems; using Content.Server.Construction; using Content.Server.Fluids.EntitySystems; @@ -23,6 +23,10 @@ using System.Linq; using Content.Server.Administration.Logs; using Content.Shared.Database; +using Content.Shared.Destructible; +using Content.Shared.Emag.Components; +using Content.Shared.Power; +using Robust.Shared.Prototypes; namespace Content.Server.Materials; @@ -52,7 +56,7 @@ public override void Initialize() SubscribeLocalEvent<MaterialReclaimerComponent, PowerChangedEvent>(OnPowerChanged); SubscribeLocalEvent<MaterialReclaimerComponent, InteractUsingEvent>(OnInteractUsing, before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)}); - SubscribeLocalEvent<MaterialReclaimerComponent, SuicideEvent>(OnSuicide); + SubscribeLocalEvent<MaterialReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment); SubscribeLocalEvent<ActiveMaterialReclaimerComponent, PowerChangedEvent>(OnActivePowerChanged); } private void OnStartup(Entity<MaterialReclaimerComponent> entity, ref ComponentStartup args) @@ -102,12 +106,11 @@ private void OnInteractUsing(Entity<MaterialReclaimerComponent> entity, ref Inte args.Handled = TryStartProcessItem(entity.Owner, args.Used, entity.Comp, args.User); } - private void OnSuicide(Entity<MaterialReclaimerComponent> entity, ref SuicideEvent args) + private void OnSuicideByEnvironment(Entity<MaterialReclaimerComponent> entity, ref SuicideByEnvironmentEvent args) { if (args.Handled) return; - args.SetHandled(SuicideKind.Bloodloss); var victim = args.Victim; if (TryComp(victim, out ActorComponent? actor) && _mind.TryGetMind(actor.PlayerSession, out var mindId, out var mind)) @@ -119,12 +122,15 @@ private void OnSuicide(Entity<MaterialReclaimerComponent> entity, ref SuicideEve } } - _popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others", ("victim", Identity.Entity(victim, EntityManager))), + _popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others", + ("victim", Identity.Entity(victim, EntityManager))), victim, - Filter.PvsExcept(victim, entityManager: EntityManager), true); + Filter.PvsExcept(victim, entityManager: EntityManager), + true); _body.GibBody(victim, true); _appearance.SetData(entity.Owner, RecyclerVisuals.Bloody, true); + args.Handled = true; } private void OnActivePowerChanged(Entity<ActiveMaterialReclaimerComponent> entity, ref PowerChangedEvent args) diff --git a/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs b/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs index 194ef532ba2..ae0495fb278 100644 --- a/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs +++ b/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs @@ -163,8 +163,7 @@ private void OnInteract(EntityUid uid, MechGrabberComponent component, UserActiv component.AudioStream = audio!.Value.Entity; var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.GrabDelay, new GrabberDoAfterEvent(), uid, target: target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true + BreakOnMove = true }; _doAfter.TryStartDoAfter(doAfterArgs, out component.DoAfter); diff --git a/Content.Server/Mech/Systems/MechAssemblySystem.cs b/Content.Server/Mech/Systems/MechAssemblySystem.cs index e5b7bfaac3c..4b408343b7a 100644 --- a/Content.Server/Mech/Systems/MechAssemblySystem.cs +++ b/Content.Server/Mech/Systems/MechAssemblySystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Interaction; using Content.Shared.Tag; using Content.Shared.Tools.Components; +using Content.Shared.Tools.Systems; using Robust.Server.Containers; using Robust.Shared.Containers; @@ -14,6 +15,8 @@ namespace Content.Server.Mech.Systems; public sealed class MechAssemblySystem : EntitySystem { [Dependency] private readonly ContainerSystem _container = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; /// <inheritdoc/> public override void Initialize() @@ -29,7 +32,7 @@ private void OnInit(EntityUid uid, MechAssemblyComponent component, ComponentIni private void OnInteractUsing(EntityUid uid, MechAssemblyComponent component, InteractUsingEvent args) { - if (TryComp<ToolComponent>(args.Used, out var toolComp) && toolComp.Qualities.Contains(component.QualityNeeded)) + if (_toolSystem.HasQuality(args.Used, component.QualityNeeded)) { foreach (var tag in component.RequiredParts.Keys) { @@ -44,7 +47,7 @@ private void OnInteractUsing(EntityUid uid, MechAssemblyComponent component, Int foreach (var (tag, val) in component.RequiredParts) { - if (!val && tagComp.Tags.Contains(tag)) + if (!val && _tag.HasTag(tagComp, tag)) { component.RequiredParts[tag] = true; _container.Insert(args.Used, component.PartsContainer); diff --git a/Content.Server/Mech/Systems/MechEquipmentSystem.cs b/Content.Server/Mech/Systems/MechEquipmentSystem.cs index 5191f53004e..f9fe5e46413 100644 --- a/Content.Server/Mech/Systems/MechEquipmentSystem.cs +++ b/Content.Server/Mech/Systems/MechEquipmentSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Interaction; using Content.Shared.Mech.Components; using Content.Shared.Mech.Equipment.Components; +using Content.Shared.Whitelist; namespace Content.Server.Mech.Systems; @@ -14,6 +15,7 @@ public sealed class MechEquipmentSystem : EntitySystem [Dependency] private readonly MechSystem _mech = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; /// <inheritdoc/> public override void Initialize() @@ -40,15 +42,14 @@ private void OnUsed(EntityUid uid, MechEquipmentComponent component, AfterIntera if (mechComp.EquipmentContainer.ContainedEntities.Count >= mechComp.MaxEquipmentAmount) return; - if (mechComp.EquipmentWhitelist != null && !mechComp.EquipmentWhitelist.IsValid(args.Used)) + if (_whitelistSystem.IsWhitelistFail(mechComp.EquipmentWhitelist, args.Used)) return; _popup.PopupEntity(Loc.GetString("mech-equipment-begin-install", ("item", uid)), mech); var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.InstallDuration, new InsertEquipmentEvent(), uid, target: mech, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true + BreakOnMove = true, }; _doAfter.TryStartDoAfter(doAfterEventArgs); diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs index a728ee7de5e..b738d28b467 100644 --- a/Content.Server/Mech/Systems/MechSystem.cs +++ b/Content.Server/Mech/Systems/MechSystem.cs @@ -17,10 +17,12 @@ using Content.Shared.Verbs; using Content.Shared.Wires; using Content.Server.Body.Systems; +using Content.Shared.Tools.Systems; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.Player; +using Content.Shared.Whitelist; namespace Content.Server.Mech.Systems; @@ -35,6 +37,8 @@ public sealed partial class MechSystem : SharedMechSystem [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; /// <inheritdoc/> public override void Initialize() @@ -87,12 +91,12 @@ private void OnInteractUsing(EntityUid uid, MechComponent component, InteractUsi return; } - if (TryComp<ToolComponent>(args.Used, out var tool) && tool.Qualities.Contains("Prying") && component.BatterySlot.ContainedEntity != null) + if (_toolSystem.HasQuality(args.Used, "Prying") && component.BatterySlot.ContainedEntity != null) { - var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay, new RemoveBatteryEvent(), uid, target: uid, used: args.Target) + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay, + new RemoveBatteryEvent(), uid, target: uid, used: args.Target) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true }; _doAfter.TryStartDoAfter(doAfterEventArgs); @@ -179,7 +183,7 @@ private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsE { var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.EntryDelay, new MechEntryEvent(), uid, target: uid) { - BreakOnUserMove = true, + BreakOnMove = true, }; _doAfter.TryStartDoAfter(doAfterEventArgs); @@ -207,11 +211,8 @@ private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsE return; } - var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.ExitDelay, new MechExitEvent(), uid, target: uid) - { - BreakOnUserMove = true, - BreakOnTargetMove = true, - }; + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.ExitDelay, + new MechExitEvent(), uid, target: uid); _doAfter.TryStartDoAfter(doAfterEventArgs); } @@ -225,7 +226,7 @@ private void OnMechEntry(EntityUid uid, MechComponent component, MechEntryEvent if (args.Cancelled || args.Handled) return; - if (component.PilotWhitelist != null && !component.PilotWhitelist.IsValid(args.User)) + if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User)) { _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User); return; diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index 97a758a5ed3..2e6e0117154 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -24,6 +24,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Throwing; using Robust.Server.Player; using Robust.Shared.Audio.Systems; @@ -103,11 +104,11 @@ public override void Initialize() SubscribeLocalEvent<BiomassReclaimerComponent, RefreshPartsEvent>(OnRefreshParts); SubscribeLocalEvent<BiomassReclaimerComponent, UpgradeExamineEvent>(OnUpgradeExamine); SubscribeLocalEvent<BiomassReclaimerComponent, PowerChangedEvent>(OnPowerChanged); - SubscribeLocalEvent<BiomassReclaimerComponent, SuicideEvent>(OnSuicide); + SubscribeLocalEvent<BiomassReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment); SubscribeLocalEvent<BiomassReclaimerComponent, ReclaimerDoAfterEvent>(OnDoAfter); } - private void OnSuicide(Entity<BiomassReclaimerComponent> ent, ref SuicideEvent args) + private void OnSuicideByEnvironment(Entity<BiomassReclaimerComponent> ent, ref SuicideByEnvironmentEvent args) { if (args.Handled || HasComp<ActiveBiomassReclaimerComponent>(ent) @@ -116,7 +117,7 @@ private void OnSuicide(Entity<BiomassReclaimerComponent> ent, ref SuicideEvent a _popup.PopupEntity(Loc.GetString("biomass-reclaimer-suicide-others", ("victim", args.Victim)), ent, PopupType.LargeCaution); StartProcessing(args.Victim, ent); - args.SetHandled(SuicideKind.Blunt); + args.Handled = true; } private void OnInit(EntityUid uid, ActiveBiomassReclaimerComponent component, ComponentInit args) @@ -157,9 +158,8 @@ private void OnAfterInteractUsing(Entity<BiomassReclaimerComponent> reclaimer, r : 1); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, delay, new ReclaimerDoAfterEvent(), reclaimer, target: args.Target, used: args.Used) { - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true + NeedHand = true, + BreakOnMove = true }); } diff --git a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs index 530b3099fcf..cf397984246 100644 --- a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs +++ b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs @@ -6,6 +6,9 @@ namespace Content.Server.Medical.Components; /// <summary> /// After scanning, retrieves the target Uid to use with its related UI. /// </summary> +/// <remarks> +/// Requires <c>ItemToggleComponent</c>. +/// </remarks> [RegisterComponent, AutoGenerateComponentPause] [Access(typeof(HealthAnalyzerSystem), typeof(CryoPodSystem))] public sealed partial class HealthAnalyzerComponent : Component @@ -57,5 +60,11 @@ public sealed partial class HealthAnalyzerComponent : Component /// Sound played on scanning end /// </summary> [DataField] - public SoundSpecifier? ScanningEndSound; + public SoundSpecifier ScanningEndSound = new SoundPathSpecifier("/Audio/Items/Medical/healthscanner.ogg"); + + /// <summary> + /// Whether to show up the popup + /// </summary> + [DataField] + public bool Silent; } diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 6388ffd9683..6b20567e669 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -29,6 +29,7 @@ using Content.Shared.Interaction; using Content.Shared.Medical.Cryogenics; using Content.Shared.MedicalScanner; +using Content.Shared.Power; using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Containers; @@ -143,8 +144,7 @@ private void HandleDragDropOn(Entity<CryoPodComponent> entity, ref DragDropTarge var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.EntryDelay, new CryoPodDragFinished(), entity, target: args.Dragged, used: entity) { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = false, }; _doAfterSystem.TryStartDoAfter(doAfterArgs); diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index 7b0b4110201..264df0e230b 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Interaction; using Content.Shared.Interaction.Components; using Content.Shared.Interaction.Events; +using Content.Shared.Item.ItemToggle; using Content.Shared.Medical; using Content.Shared.Mind; using Content.Shared.Mobs; @@ -37,6 +38,7 @@ public sealed class DefibrillatorSystem : EntitySystem [Dependency] private readonly DoAfterSystem _doAfter = default!; [Dependency] private readonly ElectrocutionSystem _electrocution = default!; [Dependency] private readonly EuiManager _euiManager = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly RottingSystem _rotting = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; @@ -50,30 +52,10 @@ public sealed class DefibrillatorSystem : EntitySystem /// <inheritdoc/> public override void Initialize() { - SubscribeLocalEvent<DefibrillatorComponent, UseInHandEvent>(OnUseInHand); - SubscribeLocalEvent<DefibrillatorComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); SubscribeLocalEvent<DefibrillatorComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<DefibrillatorComponent, DefibrillatorZapDoAfterEvent>(OnDoAfter); } - private void OnUseInHand(EntityUid uid, DefibrillatorComponent component, UseInHandEvent args) - { - if (args.Handled || !TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay))) - return; - - if (!TryToggle(uid, component, args.User)) - return; - - args.Handled = true; - _useDelay.TryResetDelay((uid, useDelay)); - } - - private void OnPowerCellSlotEmpty(EntityUid uid, DefibrillatorComponent component, ref PowerCellSlotEmptyEvent args) - { - if (!TerminatingOrDeleted(uid)) - TryDisable(uid, component); - } - private void OnAfterInteract(EntityUid uid, DefibrillatorComponent component, AfterInteractEvent args) { if (args.Handled || args.Target is not { } target) @@ -96,54 +78,25 @@ private void OnDoAfter(EntityUid uid, DefibrillatorComponent component, Defibril Zap(uid, target, args.User, component); } - public bool TryToggle(EntityUid uid, DefibrillatorComponent? component = null, EntityUid? user = null) - { - if (!Resolve(uid, ref component)) - return false; - - return component.Enabled - ? TryDisable(uid, component) - : TryEnable(uid, component, user); - } - - public bool TryEnable(EntityUid uid, DefibrillatorComponent? component = null, EntityUid? user = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (component.Enabled) - return false; - - if (!_powerCell.HasActivatableCharge(uid)) - return false; - - component.Enabled = true; - _appearance.SetData(uid, ToggleVisuals.Toggled, true); - _audio.PlayPvs(component.PowerOnSound, uid); - return true; - } - - public bool TryDisable(EntityUid uid, DefibrillatorComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (!component.Enabled) - return false; - - component.Enabled = false; - _appearance.SetData(uid, ToggleVisuals.Toggled, false); - - _audio.PlayPvs(component.PowerOffSound, uid); - return true; - } - - public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null) + /// <summary> + /// Checks if you can actually defib a target. + /// </summary> + /// <param name="uid">Uid of the defib</param> + /// <param name="target">Uid of the target getting defibbed</param> + /// <param name="user">Uid of the entity using the defibrillator</param> + /// <param name="component">Defib component</param> + /// <param name="targetCanBeAlive"> + /// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are. + /// </param> + /// <returns> + /// Returns true if the target is valid to be defibed, false otherwise. + /// </returns> + public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false) { if (!Resolve(uid, ref component)) return false; - if (!component.Enabled) + if (!_toggle.IsActivated(uid)) { if (user != null) _popup.PopupEntity(Loc.GetString("defibrillator-not-on"), uid, user.Value); @@ -159,12 +112,25 @@ public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, Defi if (!_powerCell.HasActivatableCharge(uid, user: user)) return false; - if (_mobState.IsAlive(target, mobState)) + if (!targetCanBeAlive && _mobState.IsAlive(target, mobState)) + return false; + + if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState)) return false; return true; } + /// <summary> + /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after. + /// </summary> + /// <param name="uid">Uid of the defib</param> + /// <param name="target">Uid of the target getting defibbed</param> + /// <param name="user">Uid of the entity using the defibrillator</param> + /// <param name="component">Defib component</param> + /// <returns> + /// Returns true if the defibrillation do-after started, otherwise false. + /// </returns> public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null) { if (!Resolve(uid, ref component)) @@ -178,28 +144,44 @@ public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, Defibri uid, target, uid) { BlockDuplicate = true, - BreakOnUserMove = true, - BreakOnTargetMove = true, BreakOnHandChange = true, - NeedHand = true + NeedHand = true, + BreakOnMove = !component.AllowDoAfterMovement }); } public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null, MobStateComponent? mob = null, MobThresholdsComponent? thresholds = null) { - if (!Resolve(uid, ref component) || !Resolve(target, ref mob, ref thresholds, false)) + if (!Resolve(uid, ref component)) return; - // clowns zap themselves - if (HasComp<ClumsyComponent>(user) && user != target) - { - Zap(uid, user, user, component); + if (!_powerCell.TryUseActivatableCharge(uid, user: user)) return; - } - if (!_powerCell.TryUseActivatableCharge(uid, user: user)) + var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, uid, target); + RaiseLocalEvent(user, selfEvent); + + target = selfEvent.DefibTarget; + + // Ensure thet new target is still valid. + if (selfEvent.Cancelled || !CanZap(uid, target, user, component, true)) + return; + + var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, uid, target); + RaiseLocalEvent(target, targetEvent); + + target = targetEvent.DefibTarget; + + if (targetEvent.Cancelled || !CanZap(uid, target, user, component, true)) return; + if (!TryComp<MobStateComponent>(target, out var mobState) || + !TryComp<MobThresholdsComponent>(target, out var mobThresholds)) + return; + + mob = mobState; + thresholds = mobThresholds; + _audio.PlayPvs(component.ZapSound, uid); _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true); component.NextZapTime = _timing.CurTime + component.ZapDelay; @@ -250,7 +232,11 @@ public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorCo // if we don't have enough power left for another shot, turn it off if (!_powerCell.HasActivatableCharge(uid)) - TryDisable(uid, component); + _toggle.TryDeactivate(uid); + + // TODO clean up this clown show above + var ev = new TargetDefibrillatedEvent(user, (uid, component)); + RaiseLocalEvent(target, ref ev); } public override void Update(float frameTime) diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs index f41922b0891..73cce899903 100644 --- a/Content.Server/Medical/HealingSystem.cs +++ b/Content.Server/Medical/HealingSystem.cs @@ -221,11 +221,11 @@ targetDamage.DamageContainerID is not null && new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: uid) { //Raise the event on the target if it's not self, otherwise raise it on self. - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, // Didn't break on damage as they may be trying to prevent it and // not being able to heal your own ticking damage would be frustrating. NeedHand = true, + BreakOnWeightlessMove = false, }; _doAfter.TryStartDoAfter(doAfterEventArgs); diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 40aafdbd352..91e8b2bf981 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -15,10 +15,10 @@ using Content.Shared.MedicalScanner; using Content.Shared.Mobs.Components; using Content.Shared.PowerCell; +using Content.Shared.Popups; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.Player; using Robust.Shared.Timing; // Shitmed Change @@ -36,16 +36,17 @@ public sealed class HealthAnalyzerSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedBodySystem _bodySystem = default!; // Shitmed Change - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; public override void Initialize() { SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<HealthAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer); - SubscribeLocalEvent<HealthAnalyzerComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); + SubscribeLocalEvent<HealthAnalyzerComponent, ItemToggledEvent>(OnToggled); SubscribeLocalEvent<HealthAnalyzerComponent, DroppedEvent>(OnDropped); // Shitmed Change Start Subs.BuiEvents<HealthAnalyzerComponent>(HealthAnalyzerUiKey.Key, subs => @@ -111,9 +112,8 @@ private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInter _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true + NeedHand = true, + BreakOnMove = true }); } @@ -122,7 +122,8 @@ private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDo if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User)) return; - _audio.PlayPvs(uid.Comp.ScanningEndSound, uid); + if (!uid.Comp.Silent) + _audio.PlayPvs(uid.Comp.ScanningEndSound, uid); OpenUserInterface(args.User, uid); BeginAnalyzingEntity(uid, args.Target.Value); @@ -135,16 +136,16 @@ private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDo private void OnInsertedIntoContainer(Entity<HealthAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args) { if (uid.Comp.ScannedEntity is { } patient) - StopAnalyzingEntity(uid, patient); + _toggle.TryDeactivate(uid.Owner); } /// <summary> - /// Disable continuous updates once battery is dead + /// Disable continuous updates once turned off /// </summary> - private void OnPowerCellSlotEmpty(Entity<HealthAnalyzerComponent> uid, ref PowerCellSlotEmptyEvent args) + private void OnToggled(Entity<HealthAnalyzerComponent> ent, ref ItemToggledEvent args) { - if (uid.Comp.ScannedEntity is { } patient) - StopAnalyzingEntity(uid, patient); + if (!args.Activated && ent.Comp.ScannedEntity is { } patient) + StopAnalyzingEntity(ent, patient); } /// <summary> @@ -153,7 +154,7 @@ private void OnPowerCellSlotEmpty(Entity<HealthAnalyzerComponent> uid, ref Power private void OnDropped(Entity<HealthAnalyzerComponent> uid, ref DroppedEvent args) { if (uid.Comp.ScannedEntity is { } patient) - StopAnalyzingEntity(uid, patient); + _toggle.TryDeactivate(uid.Owner); } private void OpenUserInterface(EntityUid user, EntityUid analyzer) @@ -175,7 +176,7 @@ private void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer //Link the health analyzer to the scanned entity healthAnalyzer.Comp.ScannedEntity = target; healthAnalyzer.Comp.CurrentBodyPart = part; // Shitmed Change - _cell.SetPowerCellDrawEnabled(healthAnalyzer, true); + _cell.SetDrawEnabled(healthAnalyzer.Owner, true); UpdateScannedUser(healthAnalyzer, target, true, part); // Shitmed Change } @@ -190,7 +191,7 @@ private void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, //Unlink the analyzer healthAnalyzer.Comp.ScannedEntity = null; healthAnalyzer.Comp.CurrentBodyPart = null; // Shitmed Change - _cell.SetPowerCellDrawEnabled(target, false); + _cell.SetDrawEnabled(healthAnalyzer.Owner, false); UpdateScannedUser(healthAnalyzer, target, false); } diff --git a/Content.Server/Medical/PenLightSystem.cs b/Content.Server/Medical/PenLightSystem.cs index f1df356cab0..c092b4c5902 100644 --- a/Content.Server/Medical/PenLightSystem.cs +++ b/Content.Server/Medical/PenLightSystem.cs @@ -91,8 +91,7 @@ public bool TryStartExam(EntityUid uid, EntityUid target, EntityUid user, PenLig uid, target, uid) { BlockDuplicate = true, - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, BreakOnHandChange = true, NeedHand = true }); diff --git a/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs b/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs index 96bfc7c904c..b8304c562a4 100644 --- a/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs +++ b/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs @@ -100,9 +100,8 @@ private void StartListening(EntityUid scope, EntityUid user, EntityUid target, S { _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.Delay, new StethoscopeDoAfterEvent(), scope, target: target, used: scope) { - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true + NeedHand = true, + BreakOnMove = true, }); } diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index bd904a03b0d..aee62a58e58 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -326,10 +326,9 @@ public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = n { if (card.Comp.FullName != null) userName = card.Comp.FullName; - if (card.Comp.JobTitle != null) - userJob = card.Comp.JobTitle; - if (card.Comp.JobIcon != null) - userJobIcon = card.Comp.JobIcon; + if (card.Comp.LocalizedJobTitle != null) + userJob = card.Comp.LocalizedJobTitle; + userJobIcon = card.Comp.JobIcon; foreach (var department in card.Comp.JobDepartments) userJobDepartments.Add(Loc.GetString(department)); diff --git a/Content.Server/Mind/Toolshed/MindCommand.cs b/Content.Server/Mind/Toolshed/MindCommand.cs index 917e6fb7f1a..5f82029dc0b 100644 --- a/Content.Server/Mind/Toolshed/MindCommand.cs +++ b/Content.Server/Mind/Toolshed/MindCommand.cs @@ -29,19 +29,10 @@ public sealed class MindCommand : ToolshedCommand } [CommandImplementation("control")] - public EntityUid Control( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid target, - [CommandArgument] ValueRef<ICommonSession> playerRef) + public EntityUid Control(IInvocationContext ctx, [PipedArgument] EntityUid target, ICommonSession player) { _mind ??= GetSys<SharedMindSystem>(); - var player = playerRef.Evaluate(ctx); - if (player is null) - { - ctx.ReportError(new NotForServerConsoleError()); - return target; - } if (!_mind.TryGetMind(player, out var mindId, out var mind)) { diff --git a/Content.Server/Morgue/CrematoriumSystem.cs b/Content.Server/Morgue/CrematoriumSystem.cs index 54b47cff84a..f6859b610af 100644 --- a/Content.Server/Morgue/CrematoriumSystem.cs +++ b/Content.Server/Morgue/CrematoriumSystem.cs @@ -38,7 +38,7 @@ public override void Initialize() SubscribeLocalEvent<CrematoriumComponent, ExaminedEvent>(OnExamine); SubscribeLocalEvent<CrematoriumComponent, GetVerbsEvent<AlternativeVerb>>(AddCremateVerb); - SubscribeLocalEvent<CrematoriumComponent, SuicideEvent>(OnSuicide); + SubscribeLocalEvent<CrematoriumComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment); SubscribeLocalEvent<ActiveCrematoriumComponent, StorageOpenAttemptEvent>(OnAttemptOpen); } @@ -146,11 +146,10 @@ private void FinishCooking(EntityUid uid, CrematoriumComponent component, Entity _audio.PlayPvs(component.CremateFinishSound, uid); } - private void OnSuicide(EntityUid uid, CrematoriumComponent component, SuicideEvent args) + private void OnSuicideByEnvironment(EntityUid uid, CrematoriumComponent component, SuicideByEnvironmentEvent args) { if (args.Handled) return; - args.SetHandled(SuicideKind.Heat); var victim = args.Victim; if (TryComp(victim, out ActorComponent? actor) && _minds.TryGetMind(victim, out var mindId, out var mind)) @@ -179,6 +178,7 @@ private void OnSuicide(EntityUid uid, CrematoriumComponent component, SuicideEve } _entityStorage.CloseStorage(uid); Cremate(uid, component); + args.Handled = true; } public override void Update(float frameTime) diff --git a/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs b/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs deleted file mode 100644 index 804a61b456d..00000000000 --- a/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Server.NPC.Systems; - -namespace Content.Server.NPC.Components; - -/// <summary> -/// This is used for tracking entities stored in <see cref="FactionExceptionComponent"/> -/// </summary> -[RegisterComponent, Access(typeof(NpcFactionSystem))] -public sealed partial class FactionExceptionTrackerComponent : Component -{ - /// <summary> - /// entities with <see cref="FactionExceptionComponent"/> that are tracking this entity. - /// </summary> - [DataField("entities")] - public HashSet<EntityUid> Entities = new(); -} diff --git a/Content.Server/NPC/Components/NPCUseActionOnTargetComponent.cs b/Content.Server/NPC/Components/NPCUseActionOnTargetComponent.cs new file mode 100644 index 00000000000..f022a45eccb --- /dev/null +++ b/Content.Server/NPC/Components/NPCUseActionOnTargetComponent.cs @@ -0,0 +1,27 @@ +using Content.Server.NPC.Systems; +using Content.Shared.Actions; +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.Components; + +/// <summary> +/// This is used for an NPC that constantly tries to use an action on a given target. +/// </summary> +[RegisterComponent, Access(typeof(NPCUseActionOnTargetSystem))] +public sealed partial class NPCUseActionOnTargetComponent : Component +{ + /// <summary> + /// HTN blackboard key for the target entity + /// </summary> + [DataField] + public string TargetKey = "Target"; + + /// <summary> + /// Action that's going to attempt to be used. + /// </summary> + [DataField(required: true)] + public EntProtoId<EntityWorldTargetActionComponent> ActionId; + + [DataField] + public EntityUid? ActionEnt; +} diff --git a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs b/Content.Server/NPC/Components/NpcFactionMemberComponent.cs deleted file mode 100644 index f38d8cca0f4..00000000000 --- a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.NPC.Systems; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; - -namespace Content.Server.NPC.Components -{ - [RegisterComponent] - public sealed partial class NpcFactionMemberComponent : Component - { - /// <summary> - /// Factions this entity is a part of. - /// </summary> - [ViewVariables(VVAccess.ReadWrite), - DataField("factions", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<NpcFactionPrototype>))] - public HashSet<string> Factions = new(); - - /// <summary> - /// Cached friendly factions. - /// </summary> - [ViewVariables] - public readonly HashSet<string> FriendlyFactions = new(); - - /// <summary> - /// Cached hostile factions. - /// </summary> - [ViewVariables] - public readonly HashSet<string> HostileFactions = new(); - - // Nyano - Summary - Begin modified code block: support for specific entities to be friendly. - /// <summary> - /// Permanently friendly specific entities. Our summoner, etc. - /// Would like to separate. Could I do that by extending this method, maybe? - /// </summary> - public HashSet<EntityUid> ExceptionalFriendlies = new(); - // Nyano - End modified code block. - } -} diff --git a/Content.Server/NPC/Components/NpcFactionPrototype.cs b/Content.Server/NPC/Components/NpcFactionPrototype.cs deleted file mode 100644 index fe5774710af..00000000000 --- a/Content.Server/NPC/Components/NpcFactionPrototype.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Server.NPC.Components -{ - /// <summary> - /// Contains data about this faction's relations with other factions. - /// </summary> - [Prototype("npcFaction")] - public sealed partial class NpcFactionPrototype : IPrototype - { - [ViewVariables] - [IdDataField] - public string ID { get; private set; } = default!; - - [ViewVariables(VVAccess.ReadWrite), DataField("friendly", customTypeSerializer:typeof(PrototypeIdListSerializer<NpcFactionPrototype>))] - public List<string> Friendly = new(); - - [ViewVariables(VVAccess.ReadWrite), DataField("hostile", customTypeSerializer:typeof(PrototypeIdListSerializer<NpcFactionPrototype>))] - public List<string> Hostile = new(); - } -} diff --git a/Content.Server/NPC/FactionData.cs b/Content.Server/NPC/FactionData.cs deleted file mode 100644 index b74150acc9d..00000000000 --- a/Content.Server/NPC/FactionData.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Content.Server.NPC; - -/// <summary> -/// Cached data for the faction prototype. Can be modified at runtime. -/// </summary> -public sealed class FactionData -{ - [ViewVariables] - public HashSet<string> Friendly = new(); - - [ViewVariables] - public HashSet<string> Hostile = new(); -} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickDrainTargetOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickDrainTargetOperator.cs index 52c12777aa5..dbb09bb6135 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickDrainTargetOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickDrainTargetOperator.cs @@ -3,6 +3,8 @@ using Content.Server.LifeDrainer; using Content.Server.NPC.Pathfinding; using Content.Server.NPC.Systems; +using NpcFactionSystem = Content.Shared.NPC.Systems.NpcFactionSystem; + namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; diff --git a/Content.Server/NPC/NPCBlackboard.cs b/Content.Server/NPC/NPCBlackboard.cs index 322ff0f85be..ffdb55045d3 100644 --- a/Content.Server/NPC/NPCBlackboard.cs +++ b/Content.Server/NPC/NPCBlackboard.cs @@ -34,6 +34,7 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj {"RangedRange", 10f}, {"RotateSpeed", float.MaxValue}, {"VisionRadius", 10f}, + {"AggroVisionRadius", 10f}, }; /// <summary> @@ -269,6 +270,13 @@ public bool Remove<T>(string key) return _blackboard.Remove(key); } + public string GetVisionRadiusKey(IEntityManager entMan) + { + return TryGetValue<EntityUid>("Target", out _, entMan) + ? AggroVisionRadius + : VisionRadius; + } + // I Ummd and Ahhd about using strings vs enums and decided on tags because // if a fork wants to do their own thing they don't need to touch the enum. @@ -317,9 +325,11 @@ public bool Remove<T>(string key) public const string PathfindKey = "MovementPathfind"; public const string RotateSpeed = "RotateSpeed"; - public const string VisionRadius = "VisionRadius"; public const string UtilityTarget = "UtilityTarget"; + private const string VisionRadius = "VisionRadius"; + private const string AggroVisionRadius = "AggroVisionRadius"; + /// <summary> /// A configurable "order" enum that can be given to an NPC from an external source. /// </summary> diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs index efb85b23ae3..f5591daa476 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs @@ -1,9 +1,9 @@ using Content.Server.NPC.Components; using Content.Shared.CombatMode; using Content.Shared.Interaction; -using Content.Shared.Physics; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; +using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Physics.Components; @@ -13,6 +13,7 @@ public sealed partial class NPCCombatSystem { [Dependency] private readonly SharedCombatModeSystem _combat = default!; [Dependency] private readonly RotateToFaceSystem _rotate = default!; + [Dependency] private readonly MapSystem _map = default!; private EntityQuery<CombatModeComponent> _combatQuery; private EntityQuery<NPCSteeringComponent> _steeringQuery; @@ -134,7 +135,7 @@ private void UpdateRanged(float frameTime) { comp.LOSAccumulator += UnoccludedCooldown; // For consistency with NPC steering. - comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, Transform(comp.Target).Coordinates, distance + 0.1f); + comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f); } if (!comp.TargetInLOS) @@ -188,13 +189,9 @@ private void UpdateRanged(float frameTime) EntityCoordinates targetCordinates; if (_mapManager.TryFindGridAt(xform.MapID, targetPos, out var gridUid, out var mapGrid)) - { - targetCordinates = new EntityCoordinates(gridUid, mapGrid.WorldToLocal(targetSpot)); - } + targetCordinates = new(gridUid, _map.WorldToLocal(comp.Target, mapGrid, targetSpot)); else - { - targetCordinates = new EntityCoordinates(xform.MapUid!.Value, targetSpot); - } + targetCordinates = new(xform.MapUid!.Value, targetSpot); comp.Status = CombatStatus.Normal; @@ -203,7 +200,6 @@ private void UpdateRanged(float frameTime) return; } - _gun.SetTarget(gun, comp.Target); _gun.AttemptShoot(uid, gunUid, gun, targetCordinates); } } diff --git a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs index a855c9915a0..bffd8a4bc41 100644 --- a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs +++ b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs @@ -1,7 +1,9 @@ -using Content.Server.NPC.Components; +using Content.Server.NPC.Components; using Content.Shared.CombatMode; using Content.Shared.Damage; using Content.Shared.Mobs.Components; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Robust.Shared.Collections; using Robust.Shared.Timing; @@ -22,38 +24,35 @@ public override void Initialize() SubscribeLocalEvent<NPCRetaliationComponent, DisarmedEvent>(OnDisarmed); } - private void OnDamageChanged(EntityUid uid, NPCRetaliationComponent component, DamageChangedEvent args) + private void OnDamageChanged(Entity<NPCRetaliationComponent> ent, ref DamageChangedEvent args) { if (!args.DamageIncreased) return; - if (args.Origin is not { } origin) + if (args.Origin is not {} origin) return; - TryRetaliate(uid, origin, component); + TryRetaliate(ent, origin); } - private void OnDisarmed(EntityUid uid, NPCRetaliationComponent component, DisarmedEvent args) + private void OnDisarmed(Entity<NPCRetaliationComponent> ent, ref DisarmedEvent args) { - TryRetaliate(uid, args.Source, component); + TryRetaliate(ent, args.Source); } - public bool TryRetaliate(EntityUid uid, EntityUid target, NPCRetaliationComponent? component = null) + public bool TryRetaliate(Entity<NPCRetaliationComponent> ent, EntityUid target) { - if (!Resolve(uid, ref component)) - return false; - // don't retaliate against inanimate objects. if (!HasComp<MobStateComponent>(target)) return false; - if (!component.RetaliateFriendlies - && _npcFaction.IsEntityFriendly(uid, target)) + if (!ent.Comp.RetaliateFriendlies + && _npcFaction.IsEntityFriendly(ent.Owner, target)) return false; - _npcFaction.AggroEntity(uid, target); - if (component.AttackMemoryLength is { } memoryLength) - component.AttackMemories[target] = _timing.CurTime + memoryLength; + _npcFaction.AggroEntity(ent.Owner, target); + if (ent.Comp.AttackMemoryLength is {} memoryLength) + ent.Comp.AttackMemories[target] = _timing.CurTime + memoryLength; return true; } @@ -65,12 +64,14 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator<NPCRetaliationComponent, FactionExceptionComponent>(); while (query.MoveNext(out var uid, out var retaliationComponent, out var factionException)) { + // TODO: can probably reuse this allocation and clear it foreach (var entity in new ValueList<EntityUid>(retaliationComponent.AttackMemories.Keys)) { if (!TerminatingOrDeleted(entity) && _timing.CurTime < retaliationComponent.AttackMemories[entity]) continue; - _npcFaction.DeAggroEntity(uid, entity, factionException); + _npcFaction.DeAggroEntity((uid, factionException), entity); + // TODO: should probably remove the AttackMemory, thats the whole point of the ValueList right?? } } } diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index ce10d4f5d5e..5f871a6ecfa 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -582,7 +582,7 @@ private void Separation( (mask & otherBody.CollisionLayer) == 0x0 && (layer & otherBody.CollisionMask) == 0x0 || !_factionQuery.TryGetComponent(ent, out var otherFaction) || - !_npcFaction.IsEntityFriendly(uid, ent, ourFaction, otherFaction) || + !_npcFaction.IsEntityFriendly((uid, ourFaction), (ent, otherFaction)) || // Use <= 0 so we ignore stationary friends in case. Vector2.Dot(otherBody.LinearVelocity, ourVelocity) <= 0f) { diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index 8729c9f7d8b..dbe1d4d7ea5 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -13,6 +13,8 @@ using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.NPC; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.NPC.Events; using Content.Shared.Physics; using Content.Shared.Weapons.Melee; @@ -453,7 +455,7 @@ private async void RequestPath(EntityUid uid, NPCSteeringComponent steering, Tra } var targetPos = steering.Coordinates.ToMap(EntityManager, _transform); - var ourPos = xform.MapPosition; + var ourPos = _transform.GetMapCoordinates(uid, xform: xform); PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path); steering.CurrentPath = new Queue<PathPoly>(result.Path); diff --git a/Content.Server/NPC/Systems/NPCUseActionOnTargetSystem.cs b/Content.Server/NPC/Systems/NPCUseActionOnTargetSystem.cs new file mode 100644 index 00000000000..33bc8f9074c --- /dev/null +++ b/Content.Server/NPC/Systems/NPCUseActionOnTargetSystem.cs @@ -0,0 +1,66 @@ +using Content.Server.NPC.Components; +using Content.Server.NPC.HTN; +using Content.Shared.Actions; +using Robust.Shared.Timing; + +namespace Content.Server.NPC.Systems; + +public sealed class NPCUseActionOnTargetSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + + /// <inheritdoc/> + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<NPCUseActionOnTargetComponent, MapInitEvent>(OnMapInit); + } + + private void OnMapInit(Entity<NPCUseActionOnTargetComponent> ent, ref MapInitEvent args) + { + ent.Comp.ActionEnt = _actions.AddAction(ent, ent.Comp.ActionId); + } + + public bool TryUseTentacleAttack(Entity<NPCUseActionOnTargetComponent?> user, EntityUid target) + { + if (!Resolve(user, ref user.Comp, false)) + return false; + + if (!TryComp<EntityWorldTargetActionComponent>(user.Comp.ActionEnt, out var action)) + return false; + + if (!_actions.ValidAction(action)) + return false; + + if (action.Event != null) + { + action.Event.Coords = Transform(target).Coordinates; + } + + _actions.PerformAction(user, + null, + user.Comp.ActionEnt.Value, + action, + action.BaseEvent, + _timing.CurTime, + false); + return true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Tries to use the attack on the current target. + var query = EntityQueryEnumerator<NPCUseActionOnTargetComponent, HTNComponent>(); + while (query.MoveNext(out var uid, out var comp, out var htn)) + { + if (!htn.Blackboard.TryGetValue<EntityUid>(comp.TargetKey, out var target, EntityManager)) + continue; + + TryUseTentacleAttack((uid, comp), target); + } + } +} diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 5075e037eac..eb111baea8a 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Tools.Systems; @@ -259,7 +260,7 @@ private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityCon } case TargetDistanceCon: { - var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager); + var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager); if (!TryComp<TransformComponent>(targetUid, out var targetXform) || !TryComp<TransformComponent>(owner, out var xform)) @@ -298,13 +299,13 @@ private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityCon } case TargetInLOSCon: { - var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager); + var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager); return _examine.InRangeUnOccluded(owner, targetUid, radius + 0.5f, null) ? 1f : 0f; } case TargetInLOSOrCurrentCon: { - var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager); + var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager); const float bufferRange = 0.5f; if (blackboard.TryGetValue<EntityUid>("Target", out var currentTarget, EntityManager) && @@ -364,7 +365,7 @@ private float GetAdjustedScore(float score, int considerations) private void Add(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQuery query) { var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner); - var vision = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager); + var vision = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager); switch (query) { @@ -373,7 +374,7 @@ private void Add(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQ if (compQuery.Components.Count == 0) return; - var mapPos = _xformQuery.GetComponent(owner).MapPosition; + var mapPos = _transform.GetMapCoordinates(owner, xform: _xformQuery.GetComponent(owner)); _compTypes.Clear(); var i = -1; EntityPrototype.ComponentRegistryEntry compZero = default!; diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs b/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs deleted file mode 100644 index acef9005ead..00000000000 --- a/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Linq; -using Content.Server.NPC.Components; - -namespace Content.Server.NPC.Systems; - -/// <summary> -/// Prevents an NPC from attacking some entities from an enemy faction. -/// </summary> -public sealed partial class NpcFactionSystem -{ - private EntityQuery<FactionExceptionComponent> _exceptionQuery; - private EntityQuery<FactionExceptionTrackerComponent> _trackerQuery; - - public void InitializeException() - { - _exceptionQuery = GetEntityQuery<FactionExceptionComponent>(); - _trackerQuery = GetEntityQuery<FactionExceptionTrackerComponent>(); - - SubscribeLocalEvent<FactionExceptionComponent, ComponentShutdown>(OnShutdown); - SubscribeLocalEvent<FactionExceptionTrackerComponent, ComponentShutdown>(OnTrackerShutdown); - } - - private void OnShutdown(EntityUid uid, FactionExceptionComponent component, ComponentShutdown args) - { - foreach (var ent in component.Hostiles) - { - if (!_trackerQuery.TryGetComponent(ent, out var trackerComponent)) - continue; - trackerComponent.Entities.Remove(uid); - } - - foreach (var ent in component.Ignored) - { - if (!_trackerQuery.TryGetComponent(ent, out var trackerComponent)) - continue; - trackerComponent.Entities.Remove(uid); - } - } - - private void OnTrackerShutdown(EntityUid uid, FactionExceptionTrackerComponent component, ComponentShutdown args) - { - foreach (var ent in component.Entities) - { - if (!_exceptionQuery.TryGetComponent(ent, out var exceptionComponent)) - continue; - exceptionComponent.Ignored.Remove(uid); - exceptionComponent.Hostiles.Remove(uid); - } - } - - /// <summary> - /// Returns whether the entity from an enemy faction won't be attacked - /// </summary> - public bool IsIgnored(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return false; - - return comp.Ignored.Contains(target); - } - - /// <summary> - /// Returns the specific hostile entities for a given entity. - /// </summary> - public IEnumerable<EntityUid> GetHostiles(EntityUid uid, FactionExceptionComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return Array.Empty<EntityUid>(); - - return comp.Hostiles; - } - - /// <summary> - /// Prevents an entity from an enemy faction from being attacked - /// </summary> - public void IgnoreEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp<FactionExceptionComponent>(uid); - comp.Ignored.Add(target); - EnsureComp<FactionExceptionTrackerComponent>(target).Entities.Add(uid); - } - - /// <summary> - /// Prevents a list of entities from an enemy faction from being attacked - /// </summary> - public void IgnoreEntities(EntityUid uid, IEnumerable<EntityUid> ignored, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp<FactionExceptionComponent>(uid); - foreach (var ignore in ignored) - { - IgnoreEntity(uid, ignore, comp); - } - } - - /// <summary> - /// Makes an entity always be considered hostile. - /// </summary> - public void AggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp<FactionExceptionComponent>(uid); - comp.Hostiles.Add(target); - EnsureComp<FactionExceptionTrackerComponent>(target).Entities.Add(uid); - } - - /// <summary> - /// Makes an entity no longer be considered hostile, if it was. - /// Doesn't apply to regular faction hostilities. - /// </summary> - public void DeAggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return; - if (!comp.Hostiles.Remove(target) || !_trackerQuery.TryGetComponent(target, out var tracker)) - return; - tracker.Entities.Remove(uid); - } - - /// <summary> - /// Makes a list of entities no longer be considered hostile, if it was. - /// Doesn't apply to regular faction hostilities. - /// </summary> - public void AggroEntities(EntityUid uid, IEnumerable<EntityUid> entities, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp<FactionExceptionComponent>(uid); - foreach (var ent in entities) - { - AggroEntity(uid, ent, comp); - } - } -} diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.cs b/Content.Server/NPC/Systems/NpcFactionSystem.cs deleted file mode 100644 index a96067c5cf3..00000000000 --- a/Content.Server/NPC/Systems/NpcFactionSystem.cs +++ /dev/null @@ -1,272 +0,0 @@ -using System.Collections.Frozen; -using System.Linq; -using Content.Server.NPC.Components; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Server.NPC.Systems; - -/// <summary> -/// Outlines faction relationships with each other. -/// part of psionics rework was making this a partial class. Should've already been handled upstream, based on the linter. -/// </summary> -public sealed partial class NpcFactionSystem : EntitySystem -{ - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - - /// <summary> - /// To avoid prototype mutability we store an intermediary data class that gets used instead. - /// </summary> - private FrozenDictionary<string, FactionData> _factions = FrozenDictionary<string, FactionData>.Empty; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<NpcFactionMemberComponent, ComponentStartup>(OnFactionStartup); - SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnProtoReload); - - InitializeException(); - RefreshFactions(); - } - - private void OnProtoReload(PrototypesReloadedEventArgs obj) - { - if (obj.WasModified<NpcFactionPrototype>()) - RefreshFactions(); - } - - private void OnFactionStartup(EntityUid uid, NpcFactionMemberComponent memberComponent, ComponentStartup args) - { - RefreshFactions(memberComponent); - } - - /// <summary> - /// Refreshes the cached factions for this component. - /// </summary> - private void RefreshFactions(NpcFactionMemberComponent memberComponent) - { - memberComponent.FriendlyFactions.Clear(); - memberComponent.HostileFactions.Clear(); - - foreach (var faction in memberComponent.Factions) - { - // YAML Linter already yells about this - if (!_factions.TryGetValue(faction, out var factionData)) - continue; - - memberComponent.FriendlyFactions.UnionWith(factionData.Friendly); - memberComponent.HostileFactions.UnionWith(factionData.Hostile); - } - } - - /// <summary> - /// Adds this entity to the particular faction. - /// </summary> - public void AddFaction(EntityUid uid, string faction, bool dirty = true) - { - if (!_protoManager.HasIndex<NpcFactionPrototype>(faction)) - { - Log.Error($"Unable to find faction {faction}"); - return; - } - - var comp = EnsureComp<NpcFactionMemberComponent>(uid); - if (!comp.Factions.Add(faction)) - return; - - if (dirty) - { - RefreshFactions(comp); - } - } - - /// <summary> - /// Removes this entity from the particular faction. - /// </summary> - public void RemoveFaction(EntityUid uid, string faction, bool dirty = true) - { - if (!_protoManager.HasIndex<NpcFactionPrototype>(faction)) - { - Log.Error($"Unable to find faction {faction}"); - return; - } - - if (!TryComp<NpcFactionMemberComponent>(uid, out var component)) - return; - - if (!component.Factions.Remove(faction)) - return; - - if (dirty) - { - RefreshFactions(component); - } - } - - /// <summary> - /// Remove this entity from all factions. - /// </summary> - public void ClearFactions(EntityUid uid, bool dirty = true) - { - if (!TryComp<NpcFactionMemberComponent>(uid, out var component)) - return; - - component.Factions.Clear(); - - if (dirty) - RefreshFactions(component); - } - - public IEnumerable<EntityUid> GetNearbyHostiles(EntityUid entity, float range, NpcFactionMemberComponent? component = null) - { - if (!Resolve(entity, ref component, false)) - return Array.Empty<EntityUid>(); - - var hostiles = GetNearbyFactions(entity, range, component.HostileFactions); - if (TryComp<FactionExceptionComponent>(entity, out var factionException)) - { - // ignore anything from enemy faction that we are explicitly friendly towards - return hostiles - .Union(GetHostiles(entity, factionException)) - .Where(target => !IsIgnored(entity, target, factionException)); - } - - return hostiles; - } - - [PublicAPI] - public IEnumerable<EntityUid> GetNearbyFriendlies(EntityUid entity, float range, NpcFactionMemberComponent? component = null) - { - if (!Resolve(entity, ref component, false)) - return Array.Empty<EntityUid>(); - - return GetNearbyFactions(entity, range, component.FriendlyFactions); - } - - private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, HashSet<string> factions) - { - var xformQuery = GetEntityQuery<TransformComponent>(); - - if (!xformQuery.TryGetComponent(entity, out var entityXform)) - yield break; - - foreach (var ent in _lookup.GetEntitiesInRange<NpcFactionMemberComponent>(entityXform.MapPosition, range)) - { - if (ent.Owner == entity) - continue; - - if (!factions.Overlaps(ent.Comp.Factions)) - continue; - - yield return ent.Owner; - } - } - - public bool IsEntityFriendly(EntityUid uidA, EntityUid uidB, NpcFactionMemberComponent? factionA = null, NpcFactionMemberComponent? factionB = null) - { - if (!Resolve(uidA, ref factionA, false) || !Resolve(uidB, ref factionB, false)) - return false; - - return factionA.Factions.Overlaps(factionB.Factions) || factionA.FriendlyFactions.Overlaps(factionB.Factions); - } - - public bool IsFactionFriendly(string target, string with) - { - return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target); - } - - public bool IsFactionFriendly(string target, EntityUid with, NpcFactionMemberComponent? factionWith = null) - { - if (!Resolve(with, ref factionWith, false)) - return false; - - return factionWith.Factions.All(x => IsFactionFriendly(target, x)) || - factionWith.FriendlyFactions.Contains(target); - } - - public bool IsFactionHostile(string target, string with) - { - return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target); - } - - public bool IsFactionHostile(string target, EntityUid with, NpcFactionMemberComponent? factionWith = null) - { - if (!Resolve(with, ref factionWith, false)) - return false; - - return factionWith.Factions.All(x => IsFactionHostile(target, x)) || - factionWith.HostileFactions.Contains(target); - } - - public bool IsFactionNeutral(string target, string with) - { - return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with); - } - - /// <summary> - /// Makes the source faction friendly to the target faction, 1-way. - /// </summary> - public void MakeFriendly(string source, string target) - { - if (!_factions.TryGetValue(source, out var sourceFaction)) - { - Log.Error($"Unable to find faction {source}"); - return; - } - - if (!_factions.ContainsKey(target)) - { - Log.Error($"Unable to find faction {target}"); - return; - } - - sourceFaction.Friendly.Add(target); - sourceFaction.Hostile.Remove(target); - RefreshFactions(); - } - - private void RefreshFactions() - { - - _factions = _protoManager.EnumeratePrototypes<NpcFactionPrototype>().ToFrozenDictionary( - faction => faction.ID, - faction => new FactionData - { - Friendly = faction.Friendly.ToHashSet(), - Hostile = faction.Hostile.ToHashSet() - - }); - - foreach (var comp in EntityQuery<NpcFactionMemberComponent>(true)) - { - comp.FriendlyFactions.Clear(); - comp.HostileFactions.Clear(); - RefreshFactions(comp); - } - } - - /// <summary> - /// Makes the source faction hostile to the target faction, 1-way. - /// </summary> - public void MakeHostile(string source, string target) - { - if (!_factions.TryGetValue(source, out var sourceFaction)) - { - Log.Error($"Unable to find faction {source}"); - return; - } - - if (!_factions.ContainsKey(target)) - { - Log.Error($"Unable to find faction {target}"); - return; - } - - sourceFaction.Friendly.Remove(target); - sourceFaction.Hostile.Add(target); - RefreshFactions(); - } -} - diff --git a/Content.Server/Ninja/Events/BatteryChangedEvent.cs b/Content.Server/Ninja/Events/BatteryChangedEvent.cs index 45bfedfee76..1848e881868 100644 --- a/Content.Server/Ninja/Events/BatteryChangedEvent.cs +++ b/Content.Server/Ninja/Events/BatteryChangedEvent.cs @@ -1,7 +1,7 @@ namespace Content.Server.Ninja.Events; /// <summary> -/// Raised on the ninja when the suit has its powercell changed. +/// Raised on the ninja and suit when the suit has its powercell changed. /// </summary> [ByRefEvent] public record struct NinjaBatteryChangedEvent(EntityUid Battery, EntityUid BatteryHolder); diff --git a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs index 552ee0397fb..4baf0913cec 100644 --- a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs +++ b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs @@ -33,16 +33,17 @@ public override void Initialize() /// Start do after for draining a power source. /// Can't predict PNBC existing so only done on server. /// </summary> - private void OnBeforeInteractHand(EntityUid uid, BatteryDrainerComponent comp, BeforeInteractHandEvent args) + private void OnBeforeInteractHand(Entity<BatteryDrainerComponent> ent, ref BeforeInteractHandEvent args) { + var (uid, comp) = ent; var target = args.Target; - if (args.Handled || comp.BatteryUid == null || !HasComp<PowerNetworkBatteryComponent>(target)) + if (args.Handled || comp.BatteryUid is not {} battery || !HasComp<PowerNetworkBatteryComponent>(target)) return; // handles even if battery is full so you can actually see the poup args.Handled = true; - if (_battery.IsFull(comp.BatteryUid.Value)) + if (_battery.IsFull(battery)) { _popup.PopupEntity(Loc.GetString("battery-drainer-full"), uid, uid, PopupType.Medium); return; @@ -50,9 +51,8 @@ private void OnBeforeInteractHand(EntityUid uid, BatteryDrainerComponent comp, B var doAfterArgs = new DoAfterArgs(EntityManager, uid, comp.DrainTime, new DrainDoAfterEvent(), target: target, eventTarget: uid) { - BreakOnUserMove = true, - BreakOnWeightlessMove = true, // prevent a ninja on a pod remotely draining it MovementThreshold = 0.5f, + BreakOnMove = true, CancelDuplicate = false, AttemptFrequency = AttemptFrequency.StartAndEnd }; @@ -60,23 +60,24 @@ private void OnBeforeInteractHand(EntityUid uid, BatteryDrainerComponent comp, B _doAfter.TryStartDoAfter(doAfterArgs); } - private void OnBatteryChanged(EntityUid uid, BatteryDrainerComponent comp, ref NinjaBatteryChangedEvent args) + private void OnBatteryChanged(Entity<BatteryDrainerComponent> ent, ref NinjaBatteryChangedEvent args) { - SetBattery(uid, args.Battery, comp); + SetBattery((ent, ent.Comp), args.Battery); } /// <inheritdoc/> - protected override void OnDoAfterAttempt(EntityUid uid, BatteryDrainerComponent comp, DoAfterAttemptEvent<DrainDoAfterEvent> args) + protected override void OnDoAfterAttempt(Entity<BatteryDrainerComponent> ent, ref DoAfterAttemptEvent<DrainDoAfterEvent> args) { - base.OnDoAfterAttempt(uid, comp, args); + base.OnDoAfterAttempt(ent, ref args); - if (comp.BatteryUid == null || _battery.IsFull(comp.BatteryUid.Value)) + if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery)) args.Cancel(); } /// <inheritdoc/> - protected override bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp, EntityUid target) + protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target) { + var (uid, comp) = ent; if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery)) return false; @@ -99,6 +100,7 @@ protected override bool TryDrainPower(EntityUid uid, BatteryDrainerComponent com var output = input * comp.DrainEfficiency; _battery.SetCharge(comp.BatteryUid.Value, battery.CurrentCharge + output, battery); + // TODO: create effect message or something Spawn("EffectSparks", Transform(target).Coordinates); _audio.PlayPvs(comp.SparkSound, target); _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid); diff --git a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs new file mode 100644 index 00000000000..d7a7be995db --- /dev/null +++ b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs @@ -0,0 +1,57 @@ +using Content.Server.Ninja.Events; +using Content.Server.Power.EntitySystems; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; + +namespace Content.Server.Ninja.Systems; + +public sealed class ItemCreatorSystem : SharedItemCreatorSystem +{ + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ItemCreatorComponent, CreateItemEvent>(OnCreateItem); + SubscribeLocalEvent<ItemCreatorComponent, NinjaBatteryChangedEvent>(OnBatteryChanged); + } + + private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args) + { + var (uid, comp) = ent; + if (comp.Battery is not {} battery) + return; + + args.Handled = true; + + var user = args.Performer; + if (!_battery.TryUseCharge(battery, comp.Charge)) + { + _popup.PopupEntity(Loc.GetString(comp.NoPowerPopup), user, user); + return; + } + + var ev = new CreateItemAttemptEvent(user); + RaiseLocalEvent(uid, ref ev); + if (ev.Cancelled) + return; + + // try to put throwing star in hand, otherwise it goes on the ground + var star = Spawn(comp.SpawnedPrototype, Transform(user).Coordinates); + _hands.TryPickupAnyHand(user, star); + } + + private void OnBatteryChanged(Entity<ItemCreatorComponent> ent, ref NinjaBatteryChangedEvent args) + { + if (ent.Comp.Battery == args.Battery) + return; + + ent.Comp.Battery = args.Battery; + Dirty(ent, ent.Comp); + } +} diff --git a/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs b/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs index ac76ae6b771..3aaf7c5d58e 100644 --- a/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs +++ b/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs @@ -1,13 +1,8 @@ -using Content.Server.Communications; -using Content.Server.Mind; using Content.Server.Ninja.Events; -using Content.Server.Objectives.Systems; -using Content.Shared.Communications; -using Content.Shared.CriminalRecords.Components; +using Content.Shared.Mind; +using Content.Shared.Objectives.Systems; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; -using Content.Shared.Research.Components; -using Content.Shared.Toggleable; namespace Content.Server.Ninja.Systems; @@ -16,89 +11,44 @@ namespace Content.Server.Ninja.Systems; /// </summary> public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem { - [Dependency] private readonly EmagProviderSystem _emagProvider = default!; - [Dependency] private readonly CodeConditionSystem _codeCondition = default!; - [Dependency] private readonly CommsHackerSystem _commsHacker = default!; - [Dependency] private readonly SharedStunProviderSystem _stunProvider = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedObjectivesSystem _objectives = default!; [Dependency] private readonly SpaceNinjaSystem _ninja = default!; - public override void Initialize() + protected override void EnableGloves(Entity<NinjaGlovesComponent> ent, Entity<SpaceNinjaComponent> user) { - base.Initialize(); + base.EnableGloves(ent, user); - SubscribeLocalEvent<NinjaGlovesComponent, ToggleActionEvent>(OnToggleAction); - } - - /// <summary> - /// Toggle gloves, if the user is a ninja wearing a ninja suit. - /// </summary> - private void OnToggleAction(EntityUid uid, NinjaGlovesComponent comp, ToggleActionEvent args) - { - if (args.Handled) + // can't use abilities if suit is not equipped, this is checked elsewhere but just making sure to satisfy nullability + if (user.Comp.Suit is not {} suit) return; - args.Handled = true; - - var user = args.Performer; - // need to wear suit to enable gloves - if (!TryComp<SpaceNinjaComponent>(user, out var ninja) - || ninja.Suit == null - || !HasComp<NinjaSuitComponent>(ninja.Suit.Value)) - { - Popup.PopupEntity(Loc.GetString("ninja-gloves-not-wearing-suit"), user, user); + if (!_mind.TryGetMind(user, out var mindId, out var mind)) return; - } - - // show its state to the user - var enabling = comp.User == null; - Appearance.SetData(uid, ToggleVisuals.Toggled, enabling); - var message = Loc.GetString(enabling ? "ninja-gloves-on" : "ninja-gloves-off"); - Popup.PopupEntity(message, user, user); - if (enabling) + foreach (var ability in ent.Comp.Abilities) { - EnableGloves(uid, comp, user, ninja); + // non-objective abilities are added in shared already + if (ability.Objective is not {} objId) + continue; + + // prevent doing an objective multiple times by toggling gloves after doing them + // if it's not tied to an objective always add them anyway + if (!_mind.TryFindObjective((mindId, mind), objId, out var obj)) + { + Log.Error($"Ninja glove ability of {ent} referenced missing objective {ability.Objective} of {_mind.MindOwnerLoggingString(mind)}"); + continue; + } + + if (!_objectives.IsCompleted(obj.Value, (mindId, mind))) + EntityManager.AddComponents(user, ability.Components); } - else - { - DisableGloves(uid, comp); - } - } - private void EnableGloves(EntityUid uid, NinjaGlovesComponent comp, EntityUid user, SpaceNinjaComponent ninja) - { - // can't use abilities if suit is not equipped, this is checked elsewhere but just making sure to satisfy nullability - if (ninja.Suit == null) - return; - - comp.User = user; - Dirty(uid, comp); - _ninja.AssignGloves(user, uid, ninja); - - var drainer = EnsureComp<BatteryDrainerComponent>(user); - var stun = EnsureComp<StunProviderComponent>(user); - _stunProvider.SetNoPowerPopup(user, "ninja-no-power", stun); + // let abilities that use battery power work if (_ninja.GetNinjaBattery(user, out var battery, out var _)) { - var ev = new NinjaBatteryChangedEvent(battery.Value, ninja.Suit.Value); + var ev = new NinjaBatteryChangedEvent(battery.Value, suit); RaiseLocalEvent(user, ref ev); } - - var emag = EnsureComp<EmagProviderComponent>(user); - _emagProvider.SetWhitelist(user, comp.DoorjackWhitelist, emag); - - EnsureComp<ResearchStealerComponent>(user); - // prevent calling in multiple threats by toggling gloves after - if (!_codeCondition.IsCompleted(user, ninja.TerrorObjective)) - { - var hacker = EnsureComp<CommsHackerComponent>(user); - var rule = _ninja.NinjaRule(user); - if (rule != null) - _commsHacker.SetThreats(user, rule.Threats, hacker); - } - if (!_codeCondition.IsCompleted(user, ninja.MassArrestObjective)) - { - EnsureComp<CriminalRecordsHackerComponent>(user); - } } } diff --git a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs index 04095b549c6..63054eaad50 100644 --- a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs +++ b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Ninja.Events; using Content.Server.Power.Components; using Content.Server.PowerCell; -using Content.Shared.Clothing.EntitySystems; using Content.Shared.Hands.EntitySystems; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; @@ -29,15 +28,13 @@ public override void Initialize() SubscribeLocalEvent<NinjaSuitComponent, ContainerIsInsertingAttemptEvent>(OnSuitInsertAttempt); SubscribeLocalEvent<NinjaSuitComponent, EmpAttemptEvent>(OnEmpAttempt); - SubscribeLocalEvent<NinjaSuitComponent, AttemptStealthEvent>(OnAttemptStealth); - SubscribeLocalEvent<NinjaSuitComponent, CreateThrowingStarEvent>(OnCreateThrowingStar); SubscribeLocalEvent<NinjaSuitComponent, RecallKatanaEvent>(OnRecallKatana); SubscribeLocalEvent<NinjaSuitComponent, NinjaEmpEvent>(OnEmp); } - protected override void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, SpaceNinjaComponent ninja) + protected override void NinjaEquipped(Entity<NinjaSuitComponent> ent, Entity<SpaceNinjaComponent> user) { - base.NinjaEquippedSuit(uid, comp, user, ninja); + base.NinjaEquipped(ent, user); _ninja.SetSuitPowerAlert(user); } @@ -57,16 +54,15 @@ private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, Contain // can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting) || inserting.MaxCharge <= battery.MaxCharge) - { args.Cancel(); - } // tell ninja abilities that use battery to update it so they don't use charge from the old one var user = Transform(uid).ParentUid; - if (!HasComp<SpaceNinjaComponent>(user)) + if (!_ninja.IsNinja(user)) return; var ev = new NinjaBatteryChangedEvent(args.EntityUid, uid); + RaiseLocalEvent(uid, ref ev); RaiseLocalEvent(user, ref ev); } @@ -77,64 +73,22 @@ private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEven args.Cancel(); } - protected override void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user) + protected override void UserUnequippedSuit(Entity<NinjaSuitComponent> ent, Entity<SpaceNinjaComponent> user) { - base.UserUnequippedSuit(uid, comp, user); + base.UserUnequippedSuit(ent, user); // remove power indicator _ninja.SetSuitPowerAlert(user); } - private void OnAttemptStealth(EntityUid uid, NinjaSuitComponent comp, AttemptStealthEvent args) + private void OnRecallKatana(Entity<NinjaSuitComponent> ent, ref RecallKatanaEvent args) { - var user = args.User; - // need 1 second of charge to turn on stealth - var chargeNeeded = SuitWattage(uid, comp); - // being attacked while cloaked gives no power message since it overloads the power supply or something - if (!_ninja.GetNinjaBattery(user, out _, out var battery) || battery.CurrentCharge < chargeNeeded) - { - Popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user); - args.Cancel(); - return; - } - - if (comp.DisableCooldown > GameTiming.CurTime) - { - Popup.PopupEntity(Loc.GetString("ninja-suit-cooldown"), user, user, PopupType.Medium); - args.Cancel(); - return; - } - - StealthClothing.SetEnabled(uid, user, true); - } - - private void OnCreateThrowingStar(EntityUid uid, NinjaSuitComponent comp, CreateThrowingStarEvent args) - { - args.Handled = true; + var (uid, comp) = ent; var user = args.Performer; - if (!_ninja.TryUseCharge(user, comp.ThrowingStarCharge)) - { - Popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + if (!_ninja.NinjaQuery.TryComp(user, out var ninja) || ninja.Katana == null) return; - } - - if (comp.DisableCooldown > GameTiming.CurTime) - { - Popup.PopupEntity(Loc.GetString("ninja-suit-cooldown"), user, user, PopupType.Medium); - return; - } - - // try to put throwing star in hand, otherwise it goes on the ground - var star = Spawn(comp.ThrowingStarPrototype, Transform(user).Coordinates); - _hands.TryPickupAnyHand(user, star); - } - private void OnRecallKatana(EntityUid uid, NinjaSuitComponent comp, RecallKatanaEvent args) - { args.Handled = true; - var user = args.Performer; - if (!TryComp<SpaceNinjaComponent>(user, out var ninja) || ninja.Katana == null) - return; var katana = ninja.Katana.Value; var coords = _transform.GetWorldPosition(katana); @@ -146,11 +100,8 @@ private void OnRecallKatana(EntityUid uid, NinjaSuitComponent comp, RecallKatana return; } - if (comp.DisableCooldown > GameTiming.CurTime) - { - Popup.PopupEntity(Loc.GetString("ninja-suit-cooldown"), user, user, PopupType.Medium); + if (CheckDisabled(ent, user)) return; - } // TODO: teleporting into belt slot var message = _hands.TryPickupAnyHand(user, katana) @@ -159,9 +110,11 @@ private void OnRecallKatana(EntityUid uid, NinjaSuitComponent comp, RecallKatana Popup.PopupEntity(Loc.GetString(message), user, user); } - private void OnEmp(EntityUid uid, NinjaSuitComponent comp, NinjaEmpEvent args) + private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args) { + var (uid, comp) = ent; args.Handled = true; + var user = args.Performer; if (!_ninja.TryUseCharge(user, comp.EmpCharge)) { @@ -169,13 +122,9 @@ private void OnEmp(EntityUid uid, NinjaSuitComponent comp, NinjaEmpEvent args) return; } - if (comp.DisableCooldown > GameTiming.CurTime) - { - Popup.PopupEntity(Loc.GetString("ninja-suit-cooldown"), user, user, PopupType.Medium); + if (CheckDisabled(ent, user)) return; - } - // I don't think this affects the suit battery, but if it ever does in the future add a blacklist for it var coords = _transform.GetMapCoordinates(user); _emp.EmpPulse(coords, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration); } diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs index 0c1e88653fa..28ab6332276 100644 --- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs +++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Chat.Managers; using Content.Server.CriminalRecords.Systems; using Content.Server.GameTicking.Rules.Components; -using Content.Server.GenericAntag; using Content.Server.Objectives.Components; using Content.Server.Objectives.Systems; using Content.Server.Power.Components; @@ -11,7 +10,6 @@ using Content.Server.Research.Systems; using Content.Server.Roles; using Content.Shared.Alert; -using Content.Shared.Clothing.EntitySystems; using Content.Shared.Doors.Components; using Content.Shared.IdentityManagement; using Content.Shared.Mind; @@ -26,11 +24,6 @@ namespace Content.Server.Ninja.Systems; -// TODO: when syndiborgs are a thing have a borg converter with 6 second doafter -// engi -> saboteur -// medi -> idk reskin it -// other -> assault - /// <summary> /// Main ninja system that handles ninja setup, provides helper methods for the rest of the code to use. /// </summary> @@ -44,13 +37,11 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem [Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly StealthClothingSystem _stealthClothing = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<SpaceNinjaComponent, GenericAntagCreatedEvent>(OnNinjaCreated); SubscribeLocalEvent<SpaceNinjaComponent, EmaggedSomethingEvent>(OnDoorjack); SubscribeLocalEvent<SpaceNinjaComponent, ResearchStolenEvent>(OnResearchStolen); SubscribeLocalEvent<SpaceNinjaComponent, ThreatCalledInEvent>(OnThreatCalledIn); @@ -62,7 +53,7 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator<SpaceNinjaComponent>(); while (query.MoveNext(out var uid, out var ninja)) { - UpdateNinja(uid, ninja, frameTime); + SetSuitPowerAlert((uid, ninja)); } } @@ -80,31 +71,13 @@ private int Download(EntityUid uid, List<string> ids) return newCount - oldCount; } - /// <summary> - /// Returns a ninja's gamerule config data. - /// If the gamerule was not started then it will be started automatically. - /// </summary> - public NinjaRuleComponent? NinjaRule(EntityUid uid, GenericAntagComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return null; - - // mind not added yet so no rule - if (comp.RuleEntity == null) - return null; - - return CompOrNull<NinjaRuleComponent>(comp.RuleEntity); - } - // TODO: can probably copy paste borg code here /// <summary> /// Update the alert for the ninja's suit power indicator. /// </summary> - public void SetSuitPowerAlert(EntityUid uid, SpaceNinjaComponent? comp = null) + public void SetSuitPowerAlert(Entity<SpaceNinjaComponent> ent) { - if (!Resolve(uid, ref comp, false)) - return; - + var (uid, comp) = ent; if (comp.Deleted || comp.Suit == null) { _alerts.ClearAlert(uid, comp.SuitPowerAlert); @@ -145,53 +118,6 @@ public override bool TryUseCharge(EntityUid user, float charge) return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge(uid.Value, charge, battery); } - /// <summary> - /// Set up everything for ninja to work and send the greeting message/sound. - /// Objectives are added by <see cref="GenericAntagSystem"/>. - /// </summary> - private void OnNinjaCreated(EntityUid uid, SpaceNinjaComponent comp, ref GenericAntagCreatedEvent args) - { - var mindId = args.MindId; - var mind = args.Mind; - - if (mind.Session == null) - return; - - var config = NinjaRule(uid); - if (config == null) - return; - - var role = new NinjaRoleComponent - { - PrototypeId = "SpaceNinja" - }; - _role.MindAddRole(mindId, role, mind); - _role.MindPlaySound(mindId, config.GreetingSound, mind); - - var session = mind.Session; - _audio.PlayGlobal(config.GreetingSound, Filter.Empty().AddPlayer(session), false, AudioParams.Default); - _chatMan.DispatchServerMessage(session, Loc.GetString("ninja-role-greeting")); - } - - // TODO: PowerCellDraw, modify when cloak enabled - /// <summary> - /// Handle constant power drains from passive usage and cloak. - /// </summary> - private void UpdateNinja(EntityUid uid, SpaceNinjaComponent ninja, float frameTime) - { - if (ninja.Suit == null) - return; - - float wattage = Suit.SuitWattage(ninja.Suit.Value); - - SetSuitPowerAlert(uid, ninja); - if (!TryUseCharge(uid, wattage * frameTime)) - { - // ran out of power, uncloak ninja - _stealthClothing.SetEnabled(ninja.Suit.Value, uid, false); - } - } - /// <summary> /// Increment greentext when emagging a door. /// </summary> diff --git a/Content.Server/Ninja/Systems/SpiderChargeSystem.cs b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs index 64c958d6f1a..c262651f27a 100644 --- a/Content.Server/Ninja/Systems/SpiderChargeSystem.cs +++ b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs @@ -7,6 +7,7 @@ using Content.Server.Sticky.Events; using Content.Shared.Interaction; using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; using Robust.Shared.GameObjects; namespace Content.Server.Ninja.Systems; @@ -14,7 +15,7 @@ namespace Content.Server.Ninja.Systems; /// <summary> /// Prevents planting a spider charge outside of its location and handles greentext. /// </summary> -public sealed class SpiderChargeSystem : EntitySystem +public sealed class SpiderChargeSystem : SharedSpiderChargeSystem { [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly PopupSystem _popup = default!; diff --git a/Content.Server/Ninja/Systems/StunProviderSystem.cs b/Content.Server/Ninja/Systems/StunProviderSystem.cs index 970ca78e2cc..7061dd6e3b2 100644 --- a/Content.Server/Ninja/Systems/StunProviderSystem.cs +++ b/Content.Server/Ninja/Systems/StunProviderSystem.cs @@ -6,9 +6,12 @@ using Content.Shared.Ninja.Systems; using Content.Shared.Popups; using Content.Shared.Stunnable; -using Robust.Shared.Prototypes; +using Content.Shared.Timing; +using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; +using Content.Shared.Whitelist; +using Robust.Shared.Prototypes; namespace Content.Server.Ninja.Systems; @@ -19,11 +22,12 @@ public sealed class StunProviderSystem : SharedStunProviderSystem { [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; public override void Initialize() { @@ -36,16 +40,18 @@ public override void Initialize() /// <summary> /// Stun clicked mobs on the whitelist, if there is enough power. /// </summary> - private void OnBeforeInteractHand(EntityUid uid, StunProviderComponent comp, BeforeInteractHandEvent args) + private void OnBeforeInteractHand(Entity<StunProviderComponent> ent, ref BeforeInteractHandEvent args) { // TODO: generic check + var (uid, comp) = ent; if (args.Handled || comp.BatteryUid == null || !_gloves.AbilityCheck(uid, args, out var target)) return; - if (target == uid || !comp.Whitelist.IsValid(target, EntityManager)) + if (target == uid || _whitelist.IsWhitelistFail(comp.Whitelist, target)) return; - if (_timing.CurTime < comp.NextStun) + var useDelay = EnsureComp<UseDelayComponent>(uid); + if (_useDelay.IsDelayed((uid, useDelay), id: comp.DelayId)) return; // take charge from battery @@ -61,13 +67,14 @@ private void OnBeforeInteractHand(EntityUid uid, StunProviderComponent comp, Bef _stun.TryParalyze(target, comp.StunTime, refresh: false); // short cooldown to prevent instant stunlocking - comp.NextStun = _timing.CurTime + comp.Cooldown; + _useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId); + _useDelay.TryResetDelay((uid, useDelay), id: comp.DelayId); args.Handled = true; } - private void OnBatteryChanged(EntityUid uid, StunProviderComponent comp, ref NinjaBatteryChangedEvent args) + private void OnBatteryChanged(Entity<StunProviderComponent> ent, ref NinjaBatteryChangedEvent args) { - SetBattery(uid, args.Battery, comp); + SetBattery((ent, ent.Comp), args.Battery); } } diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index 9cd24b9af6c..e60a7bde331 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -462,7 +462,7 @@ public void ArmBomb(EntityUid uid, NukeComponent? component = null) Color.Red, stationUid ?? uid, null, - ("time", (int) component.RemainingTime), ("position", FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((uid, nukeXform)))) + ("time", (int) component.RemainingTime), ("location", FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((uid, nukeXform)))) ); _sound.PlayGlobalOnStation(uid, _audio.GetSound(component.ArmSound)); @@ -591,8 +591,7 @@ private void DisarmBombDoafter(EntityUid uid, EntityUid user, NukeComponent nuke var doAfter = new DoAfterArgs(EntityManager, user, nuke.DisarmDoafterLength, new NukeDisarmDoAfterEvent(), uid, target: uid) { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }; diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index 5ead67a12b2..cfb69f53f0c 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -7,7 +7,7 @@ namespace Content.Server.Nutrition.Components; -[RegisterComponent, Access(typeof(FoodSystem))] +[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))] public sealed partial class FoodComponent : Component { [DataField] @@ -17,7 +17,7 @@ public sealed partial class FoodComponent : Component public SoundSpecifier UseSound = new SoundCollectionSpecifier("eating"); [DataField] - public EntProtoId? Trash; + public List<EntProtoId> Trash = new(); [DataField] public FixedPoint2? TransferAmount = FixedPoint2.New(5); diff --git a/Content.Server/Nutrition/Components/SliceableFoodComponent.cs b/Content.Server/Nutrition/Components/SliceableFoodComponent.cs index 2b74d0c67ec..7877e91d031 100644 --- a/Content.Server/Nutrition/Components/SliceableFoodComponent.cs +++ b/Content.Server/Nutrition/Components/SliceableFoodComponent.cs @@ -11,21 +11,27 @@ public sealed partial class SliceableFoodComponent : Component /// Prototype to spawn after slicing. /// If null then it can't be sliced. /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntProtoId? Slice; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Items/Culinary/chop.ogg"); /// <summary> /// Number of slices the food starts with. /// </summary> - [DataField("count"), ViewVariables(VVAccess.ReadWrite)] + [DataField("count")] public ushort TotalCount = 5; /// <summary> - /// Number of slices left. + /// how long it takes for this food to be sliced /// </summary> - [ViewVariables(VVAccess.ReadWrite)] - public ushort Count; + [DataField] + public float SliceTime = 1f; + + /// <summary> + /// all the pieces will be shifted in random directions. + /// </summary> + [DataField] + public float SpawnOffset = 0.5f; } diff --git a/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs b/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs index d0fcd113595..68d8258c6bb 100644 --- a/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs +++ b/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs @@ -1,14 +1,16 @@ -using Content.Server.Administration.Logs; +using Content.Server.Administration.Logs; using Content.Server.Interaction.Components; using Content.Server.Popups; using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.Mind.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Storage; +using Content.Shared.Whitelist; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -28,11 +30,12 @@ public sealed class AnimalHusbandrySystem : EntitySystem [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; private readonly HashSet<EntityUid> _failedAttempts = new(); private readonly HashSet<EntityUid> _birthQueue = new(); @@ -41,8 +44,7 @@ public sealed class AnimalHusbandrySystem : EntitySystem public override void Initialize() { SubscribeLocalEvent<ReproductiveComponent, MindAddedMessage>(OnMindAdded); - SubscribeLocalEvent<InfantComponent, ComponentStartup>(OnInfantStartup); - SubscribeLocalEvent<InfantComponent, ComponentShutdown>(OnInfantShutdown); + SubscribeLocalEvent<InfantComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } // we express EZ-pass terminate the pregnancy if a player takes the role @@ -52,16 +54,11 @@ private void OnMindAdded(EntityUid uid, ReproductiveComponent component, MindAdd component.GestationEndTime = null; } - private void OnInfantStartup(EntityUid uid, InfantComponent component, ComponentStartup args) + private void OnRefreshNameModifiers(Entity<InfantComponent> entity, ref RefreshNameModifiersEvent args) { - var meta = MetaData(uid); - component.OriginalName = meta.EntityName; - _metaData.SetEntityName(uid, Loc.GetString("infant-name-prefix", ("name", meta.EntityName)), meta); - } - - private void OnInfantShutdown(EntityUid uid, InfantComponent component, ComponentShutdown args) - { - _metaData.SetEntityName(uid, component.OriginalName); + // This check may seem redundant, but it makes sure that the prefix is removed before the component is removed + if (_timing.CurTime < entity.Comp.InfantEndTime) + args.AddModifier("infant-name-prefix"); } /// <summary> @@ -174,7 +171,7 @@ public bool IsValidPartner(EntityUid uid, EntityUid partner, ReproductiveCompone if (!CanReproduce(partner)) return false; - return component.PartnerWhitelist.IsValid(partner); + return _whitelistSystem.IsWhitelistPass(component.PartnerWhitelist, partner); } /// <summary> @@ -200,6 +197,8 @@ public void Birth(EntityUid uid, ReproductiveComponent? component = null) { var infant = AddComp<InfantComponent>(offspring); infant.InfantEndTime = _timing.CurTime + infant.InfantDuration; + // Make sure the name prefix is applied + _nameMod.RefreshNameModifiers(offspring); } _adminLog.Add(LogType.Action, $"{ToPrettyString(uid)} gave birth to {ToPrettyString(offspring)}."); } @@ -247,6 +246,8 @@ public override void Update(float frameTime) if (_timing.CurTime < infant.InfantEndTime) continue; RemCompDeferred(uid, infant); + // Make sure the name prefix gets removed + _nameMod.RefreshNameModifiers(uid); } } } diff --git a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs index a28679ddbc9..50b9297ca73 100644 --- a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs @@ -48,9 +48,12 @@ protected override void SplattedCreamPie(EntityUid uid, CreamPieComponent creamP { _puddle.TrySpillAt(uid, solution, out _, false); } - if (!string.IsNullOrEmpty(foodComp.Trash)) + if (foodComp.Trash.Count == 0) { - EntityManager.SpawnEntity(foodComp.Trash, Transform(uid).Coordinates); + foreach (var trash in foodComp.Trash) + { + EntityManager.SpawnEntity(trash, Transform(uid).Coordinates); + } } } ActivatePayload(uid); diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index cd05adc7948..3b4afc2d6ad 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Chemistry.ReagentEffects; +using Content.Server.EntityEffects.Effects; using Content.Server.Fluids.EntitySystems; using Content.Server.Forensics; using Content.Server.Inventory; @@ -17,6 +17,7 @@ using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.DoAfter; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -213,9 +214,8 @@ private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, En target: target, used: item) { - BreakOnUserMove = forceDrink, + BreakOnMove = forceDrink, BreakOnDamage = true, - BreakOnTargetMove = forceDrink, MovementThreshold = 0.01f, DistanceThreshold = 1.0f, // Mice and the like can eat without hands. diff --git a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs index dc1f67c7400..3eaadfa0dec 100644 --- a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Emag.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Power; using Content.Shared.Storage.Components; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; diff --git a/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs index 56ade987629..84c81c45956 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Client.Chemistry.EntitySystems; using Content.Server.Chemistry.ReactionEffects; +using Content.Server.EntityEffects.Effects; using Content.Server.Nutrition.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Reaction; diff --git a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs new file mode 100644 index 00000000000..d084dcf943b --- /dev/null +++ b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs @@ -0,0 +1,264 @@ +using System.Numerics; +using System.Text; +using Content.Server.Nutrition.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Mobs.Systems; +using Content.Shared.Nutrition; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Nutrition.EntitySystems; + +public sealed class FoodSequenceSystem : SharedFoodSequenceSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<FoodSequenceStartPointComponent, InteractUsingEvent>(OnInteractUsing); + + SubscribeLocalEvent<FoodMetamorphableByAddingComponent, FoodSequenceIngredientAddedEvent>(OnIngredientAdded); + } + + private void OnInteractUsing(Entity<FoodSequenceStartPointComponent> ent, ref InteractUsingEvent args) + { + if (TryComp<FoodSequenceElementComponent>(args.Used, out var sequenceElement)) + TryAddFoodElement(ent, (args.Used, sequenceElement), args.User); + } + + private void OnIngredientAdded(Entity<FoodMetamorphableByAddingComponent> ent, ref FoodSequenceIngredientAddedEvent args) + { + if (!TryComp<FoodSequenceStartPointComponent>(args.Start, out var start)) + return; + + if (!_proto.TryIndex(args.Proto, out var elementProto)) + return; + + if (!ent.Comp.OnlyFinal || elementProto.Final || start.FoodLayers.Count == start.MaxLayers) + { + TryMetamorph((ent, start)); + } + } + + private bool TryMetamorph(Entity<FoodSequenceStartPointComponent> start) + { + List<MetamorphRecipePrototype> availableRecipes = new(); + foreach (var recipe in _proto.EnumeratePrototypes<MetamorphRecipePrototype>()) + { + if (recipe.Key != start.Comp.Key) + continue; + + bool allowed = true; + foreach (var rule in recipe.Rules) + { + if (!rule.Check(_proto, EntityManager, start, start.Comp.FoodLayers)) + { + allowed = false; + break; + } + } + if (allowed) + availableRecipes.Add(recipe); + } + + if (availableRecipes.Count <= 0) + return true; + + Metamorf(start, _random.Pick(availableRecipes)); //In general, if there's more than one recipe, the yml-guys screwed up. Maybe some kind of unit test is needed. + QueueDel(start); + return true; + } + + private void Metamorf(Entity<FoodSequenceStartPointComponent> start, MetamorphRecipePrototype recipe) + { + var result = SpawnAtPosition(recipe.Result, Transform(start).Coordinates); + + //Try putting in container + _transform.DropNextTo(result, (start, Transform(start))); + + if (!_solutionContainer.TryGetSolution(result, start.Comp.Solution, out var resultSoln, out var resultSolution)) + return; + + if (!_solutionContainer.TryGetSolution(start.Owner, start.Comp.Solution, out var startSoln, out var startSolution)) + return; + + _solutionContainer.RemoveAllSolution(resultSoln.Value); //Remove all YML reagents + resultSoln.Value.Comp.Solution.MaxVolume = startSoln.Value.Comp.Solution.MaxVolume; + _solutionContainer.TryAddSolution(resultSoln.Value, startSolution); + + MergeFlavorProfiles(start, result); + MergeTrash(start, result); + MergeTags(start, result); + } + + private bool TryAddFoodElement(Entity<FoodSequenceStartPointComponent> start, Entity<FoodSequenceElementComponent> element, EntityUid? user = null) + { + // we can't add a live mouse to a burger. + if (!TryComp<FoodComponent>(element, out var elementFood)) + return false; + if (elementFood.RequireDead && _mobState.IsAlive(element)) + return false; + + //looking for a suitable FoodSequence prototype + ProtoId<FoodSequenceElementPrototype> elementProto = string.Empty; + foreach (var pair in element.Comp.Entries) + { + if (pair.Key == start.Comp.Key) + { + elementProto = pair.Value; + } + } + if (!_proto.TryIndex(elementProto, out var elementIndexed)) + return false; + + //if we run out of space, we can still put in one last, final finishing element. + if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementIndexed.Final || start.Comp.Finished) + { + if (user is not null) + _popup.PopupEntity(Loc.GetString("food-sequence-no-space"), start, user.Value); + return false; + } + + //Generate new visual layer + var flip = start.Comp.AllowHorizontalFlip && _random.Prob(0.5f); + var layer = new FoodSequenceVisualLayer(elementIndexed, + _random.Pick(elementIndexed.Sprites), + new Vector2(flip ? -1 : 1, 1), + new Vector2( + _random.NextFloat(start.Comp.MinLayerOffset.X, start.Comp.MaxLayerOffset.X), + _random.NextFloat(start.Comp.MinLayerOffset.Y, start.Comp.MaxLayerOffset.Y)) + ); + + start.Comp.FoodLayers.Add(layer); + Dirty(start); + + if (elementIndexed.Final) + start.Comp.Finished = true; + + UpdateFoodName(start); + MergeFoodSolutions(start, element); + MergeFlavorProfiles(start, element); + MergeTrash(start, element); + MergeTags(start, element); + + var ev = new FoodSequenceIngredientAddedEvent(start, element, elementProto, user); + RaiseLocalEvent(start, ev); + + QueueDel(element); + return true; + } + + private void UpdateFoodName(Entity<FoodSequenceStartPointComponent> start) + { + if (start.Comp.NameGeneration is null) + return; + + var content = new StringBuilder(); + var separator = ""; + if (start.Comp.ContentSeparator is not null) + separator = Loc.GetString(start.Comp.ContentSeparator); + + HashSet<ProtoId<FoodSequenceElementPrototype>> existedContentNames = new(); + foreach (var layer in start.Comp.FoodLayers) + { + if (!existedContentNames.Contains(layer.Proto)) + existedContentNames.Add(layer.Proto); + } + + var nameCounter = 1; + foreach (var proto in existedContentNames) + { + if (!_proto.TryIndex(proto, out var protoIndexed)) + continue; + + if (protoIndexed.Name is null) + continue; + + content.Append(Loc.GetString(protoIndexed.Name.Value)); + + if (nameCounter < existedContentNames.Count) + content.Append(separator); + nameCounter++; + } + + var newName = Loc.GetString(start.Comp.NameGeneration.Value, + ("prefix", start.Comp.NamePrefix is not null ? Loc.GetString(start.Comp.NamePrefix) : ""), + ("content", content), + ("suffix", start.Comp.NameSuffix is not null ? Loc.GetString(start.Comp.NameSuffix) : "")); + + _metaData.SetEntityName(start, newName); + } + + private void MergeFoodSolutions(EntityUid start, EntityUid element) + { + if (!TryComp<FoodComponent>(start, out var startFood)) + return; + + if (!TryComp<FoodComponent>(element, out var elementFood)) + return; + + if (!_solutionContainer.TryGetSolution(start, startFood.Solution, out var startSolutionEntity, out var startSolution)) + return; + + if (!_solutionContainer.TryGetSolution(element, elementFood.Solution, out _, out var elementSolution)) + return; + + startSolution.MaxVolume += elementSolution.MaxVolume; + _solutionContainer.TryAddSolution(startSolutionEntity.Value, elementSolution); + } + + private void MergeFlavorProfiles(EntityUid start, EntityUid element) + { + if (!TryComp<FlavorProfileComponent>(start, out var startProfile)) + return; + + if (!TryComp<FlavorProfileComponent>(element, out var elementProfile)) + return; + + foreach (var flavor in elementProfile.Flavors) + { + if (startProfile != null && !startProfile.Flavors.Contains(flavor)) + startProfile.Flavors.Add(flavor); + } + } + + private void MergeTrash(EntityUid start, EntityUid element) + { + if (!TryComp<FoodComponent>(start, out var startFood)) + return; + + if (!TryComp<FoodComponent>(element, out var elementFood)) + return; + + foreach (var trash in elementFood.Trash) + { + startFood.Trash.Add(trash); + } + } + + private void MergeTags(EntityUid start, EntityUid element) + { + if (!TryComp<TagComponent>(element, out var elementTags)) + return; + + EnsureComp<TagComponent>(start); + + _tag.TryAddTags(start, elementTags.Tags); + } +} \ No newline at end of file diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index 84355f03c16..f99cecc8e7a 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -1,6 +1,5 @@ using Content.Server.Body.Components; using Content.Server.Body.Systems; -using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Inventory; using Content.Server.Nutrition.Components; using Content.Shared.Nutrition.Components; @@ -33,6 +32,8 @@ using Robust.Shared.Utility; using System.Linq; using Content.Shared.CCVar; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Whitelist; using Robust.Shared.Configuration; namespace Content.Server.Nutrition.EntitySystems; @@ -54,11 +55,13 @@ public sealed class FoodSystem : EntitySystem [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly StomachSystem _stomach = default!; [Dependency] private readonly UtensilSystem _utensil = default!; [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; public const float MaxFeedDistance = 1.0f; @@ -68,7 +71,7 @@ public override void Initialize() // TODO add InteractNoHandEvent for entities like mice. // run after openable for wrapped/peelable foods - SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(ServerInventorySystem) }); + SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: [ typeof(OpenableSystem), typeof(ServerInventorySystem), ]); SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood); SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb); SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter); @@ -154,7 +157,7 @@ private void OnFeedFood(Entity<FoodComponent> entity, ref AfterInteractEvent arg return (false, true); // TODO make do-afters account for fixtures in the range check. - if (!Transform(user).MapPosition.InRange(Transform(target).MapPosition, MaxFeedDistance)) + if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance)) { var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); _popup.PopupEntity(message, user, user); @@ -165,23 +168,26 @@ private void OnFeedFood(Entity<FoodComponent> entity, ref AfterInteractEvent arg if (forceFeed) { var userName = Identity.Entity(user, EntityManager); - _popup.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)), - user, target); + _popup.PopupEntity( + Loc.GetString("food-system-force-feed", ("user", userName)), + user, + target); // logging - _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}"); + _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}"); } else { // log voluntary eating - _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}"); + _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}"); } var foodDelay = foodComp.Delay; if (TryComp<ConsumeDelayModifierComponent>(target, out var delayModifier)) foodDelay *= delayModifier.FoodDelayMultiplier; - var doAfterArgs = new DoAfterArgs(EntityManager, + var doAfterArgs = new DoAfterArgs( + EntityManager, user, forceFeed ? foodComp.ForceFeedDelay : foodDelay, new ConsumeDoAfterEvent(foodComp.Solution, flavors), @@ -189,14 +195,13 @@ private void OnFeedFood(Entity<FoodComponent> entity, ref AfterInteractEvent arg target: target, used: food) { - BreakOnUserMove = forceFeed, + BreakOnMove = forceFeed, BreakOnDamage = true, - BreakOnTargetMove = forceFeed, MovementThreshold = 0.01f, DistanceThreshold = MaxFeedDistance, // Mice and the like can eat without hands. // TODO maybe set this based on some CanEatWithoutHands event or component? - NeedHand = forceFeed, + NeedHand = forceFeed }; _doAfter.TryStartDoAfter(doAfterArgs); @@ -327,27 +332,31 @@ public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityU if (ev.Cancelled) return; - if (string.IsNullOrEmpty(component.Trash)) + if (component.Trash.Count == 0) { QueueDel(food); return; } //We're empty. Become trash. - var position = Transform(food).MapPosition; - var finisher = Spawn(component.Trash, position); + //cache some data as we remove food, before spawning trash and passing it to the hand. - // If the user is holding the item - if (_hands.IsHolding(user, food, out var hand)) + var position = _transform.GetMapCoordinates(food); + var trashes = component.Trash; + var tryPickup = _hands.IsHolding(user, food, out _); + + Del(food); + foreach (var trash in trashes) { - Del(food); + var spawnedTrash = Spawn(trash, position); - // Put the trash in the user's hand - _hands.TryPickup(user, finisher, hand); - return; + // If the user is holding the item + if (tryPickup) + { + // Put the trash in the user's hand + _hands.TryPickupAnyHand(user, spawnedTrash); + } } - - QueueDel(food); } private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev) @@ -415,8 +424,9 @@ private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(Stoma if (comp.SpecialDigestible == null) continue; // Check if the food is in the whitelist - if (comp.SpecialDigestible.IsValid(food, EntityManager)) + if (_whitelist.IsWhitelistPass(comp.SpecialDigestible, food)) return true; + // They can only eat whitelist food and the food isn't in the whitelist. It's not edible. return false; } @@ -504,10 +514,11 @@ private void OnInventoryIngestAttempt(Entity<InventoryComponent> entity, ref Ing public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null) { var attempt = new IngestionAttemptEvent(); - RaiseLocalEvent(uid, attempt, false); + RaiseLocalEvent(uid, attempt); if (attempt.Cancelled && attempt.Blocker != null && popupUid != null) { - _popup.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)), + _popup.PopupEntity( + Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)), uid, popupUid.Value); } diff --git a/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs b/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs index abb6f393ac5..71360c1cb47 100644 --- a/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs @@ -1,191 +1,179 @@ -using Content.Server.Nutrition; // DeltaV -using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.DoAfter; using Content.Server.Nutrition.Components; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; using Content.Shared.Chemistry.Components; -using Content.Shared.Examine; +using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.Interaction; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; using Robust.Shared.Containers; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; -namespace Content.Server.Nutrition.EntitySystems +namespace Content.Server.Nutrition.EntitySystems; + +public sealed class SliceableFoodSystem : EntitySystem { - public sealed class SliceableFoodSystem : EntitySystem + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly DoAfterSystem _doAfter = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + public override void Initialize() { - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly TransformSystem _xformSystem = default!; + base.Initialize(); + + SubscribeLocalEvent<SliceableFoodComponent, InteractUsingEvent>(OnInteractUsing); + SubscribeLocalEvent<SliceableFoodComponent, SliceFoodDoAfterEvent>(OnSlicedoAfter); + SubscribeLocalEvent<SliceableFoodComponent, ComponentStartup>(OnComponentStartup); + } - public override void Initialize() + private void OnInteractUsing(Entity<SliceableFoodComponent> entity, ref InteractUsingEvent args) + { + if (args.Handled) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, + args.User, + entity.Comp.SliceTime, + new SliceFoodDoAfterEvent(), + entity, + entity, + args.Used) { - base.Initialize(); + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + }; + _doAfter.TryStartDoAfter(doAfterArgs); + } - SubscribeLocalEvent<SliceableFoodComponent, ExaminedEvent>(OnExamined); - SubscribeLocalEvent<SliceableFoodComponent, InteractUsingEvent>(OnInteractUsing); - SubscribeLocalEvent<SliceableFoodComponent, ComponentStartup>(OnComponentStartup); - } + private void OnSlicedoAfter(Entity<SliceableFoodComponent> entity, ref SliceFoodDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; - private void OnInteractUsing(Entity<SliceableFoodComponent> entity, ref InteractUsingEvent args) - { - if (args.Handled) - return; + if (TrySliceFood(entity, args.User, args.Used, entity.Comp)) + args.Handled = true; + } - if (TrySliceFood(entity, args.User, args.Used, entity.Comp)) - args.Handled = true; - } + private bool TrySliceFood(EntityUid uid, + EntityUid user, + EntityUid? usedItem, + SliceableFoodComponent? component = null, + FoodComponent? food = null, + TransformComponent? transform = null) + { + if (!Resolve(uid, ref component, ref food, ref transform) || + string.IsNullOrEmpty(component.Slice)) + return false; - private bool TrySliceFood(EntityUid uid, EntityUid user, EntityUid usedItem, - SliceableFoodComponent? component = null, FoodComponent? food = null, TransformComponent? transform = null) - { - if (!Resolve(uid, ref component, ref food, ref transform) || - string.IsNullOrEmpty(component.Slice)) - { - return false; - } - - if (!_solutionContainerSystem.TryGetSolution(uid, food.Solution, out var soln, out var solution)) - { - return false; - } - - if (!TryComp<UtensilComponent>(usedItem, out var utensil) || (utensil.Types & UtensilType.Knife) == 0) - { - return false; - } + if (!_solutionContainer.TryGetSolution(uid, food.Solution, out var soln, out var solution)) + return false; + if (!TryComp<UtensilComponent>(usedItem, out var utensil) || (utensil.Types & UtensilType.Knife) == 0) + return false; + + var sliceVolume = solution.Volume / FixedPoint2.New(component.TotalCount); + for (int i = 0; i < component.TotalCount; i++) + { var sliceUid = Slice(uid, user, component, transform); - var lostSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume / FixedPoint2.New(component.Count)); + var lostSolution = + _solutionContainer.SplitSolution(soln.Value, sliceVolume); // Fill new slice FillSlice(sliceUid, lostSolution); + } - _audio.PlayPvs(component.Sound, transform.Coordinates, AudioParams.Default.WithVolume(-2)); - var ev = new SliceFoodEvent(uid, user, usedItem); - RaiseLocalEvent(ev); - - // Decrease size of item based on count - Could implement in the future - // Bug with this currently is the size in a container is not updated - // if (TryComp(uid, out ItemComponent? itemComp) && TryComp(sliceUid, out ItemComponent? sliceComp)) - // { - // itemComp.Size -= sliceComp.Size; - // } - - component.Count--; + _audio.PlayPvs(component.Sound, transform.Coordinates, AudioParams.Default.WithVolume(-2)); + var ev = new SliceFoodEvent(); + RaiseLocalEvent(uid, ref ev); - // If someone makes food proto with 1 slice... - if (component.Count < 1) - { - DeleteFood(uid, user, food); - return true; - } + DeleteFood(uid, user, food); + return true; + } - // Split last slice - if (component.Count > 1) - return true; + /// <summary> + /// Create a new slice in the world and returns its entity. + /// The solutions must be set afterwards. + /// </summary> + public EntityUid Slice(EntityUid uid, + EntityUid user, + SliceableFoodComponent? comp = null, + TransformComponent? transform = null) + { + if (!Resolve(uid, ref comp, ref transform)) + return EntityUid.Invalid; - sliceUid = Slice(uid, user, component, transform); + var sliceUid = Spawn(comp.Slice, _transform.GetMapCoordinates(uid)); - // Fill last slice with the rest of the solution - FillSlice(sliceUid, solution); + // try putting the slice into the container if the food being sliced is in a container! + // this lets you do things like slice a pizza up inside of a hot food cart without making a food-everywhere mess + _transform.DropNextTo(sliceUid, (uid, transform)); + _transform.SetLocalRotation(sliceUid, 0); - DeleteFood(uid, user, food); - return true; - } + // DeltaV - start of deep frier stuff + var slicedEv = new FoodSlicedEvent(user, uid, sliceUid); + RaiseLocalEvent(uid, ref slicedEv); + // DeltaV - end of deep frier stuff - /// <summary> - /// Create a new slice in the world and returns its entity. - /// The solutions must be set afterwards. - /// </summary> - public EntityUid Slice(EntityUid uid, EntityUid user, SliceableFoodComponent? comp = null, TransformComponent? transform = null) + if (!_container.IsEntityOrParentInContainer(sliceUid)) { - if (!Resolve(uid, ref comp, ref transform)) - return EntityUid.Invalid; - - var sliceUid = Spawn(comp.Slice, _xformSystem.GetMapCoordinates(uid)); - - // try putting the slice into the container if the food being sliced is in a container! - // this lets you do things like slice a pizza up inside of a hot food cart without making a food-everywhere mess - if (_containerSystem.TryGetContainingContainer(uid, out var container) && _containerSystem.CanInsert(sliceUid, container)) - { - _containerSystem.Insert(sliceUid, container); - } - else // puts it down "right-side up" - { - _xformSystem.AttachToGridOrMap(sliceUid); - _xformSystem.SetLocalRotation(sliceUid, 0); - } - - // DeltaV - Begin deep frier related code - var sliceEvent = new SliceFoodEvent(user, uid, sliceUid); - RaiseLocalEvent(uid, sliceEvent); - // DeltaV - End deep frier related code - - return sliceUid; + var randVect = _random.NextVector2(2.0f, 2.5f); + if (TryComp<PhysicsComponent>(sliceUid, out var physics)) + _physics.SetLinearVelocity(sliceUid, randVect, body: physics); } - private void DeleteFood(EntityUid uid, EntityUid user, FoodComponent foodComp) + return sliceUid; + } + + private void DeleteFood(EntityUid uid, EntityUid user, FoodComponent foodComp) + { + var ev = new BeforeFullySlicedEvent + { + User = user + }; + RaiseLocalEvent(uid, ev); + if (ev.Cancelled) + return; + + // Locate the sliced food and spawn its trash + foreach (var trash in foodComp.Trash) { - var ev = new BeforeFullySlicedEvent - { - User = user - }; - RaiseLocalEvent(uid, ev); - if (ev.Cancelled) - return; - - if (string.IsNullOrEmpty(foodComp.Trash)) - { - QueueDel(uid); - return; - } - - // Locate the sliced food and spawn its trash - var trashUid = Spawn(foodComp.Trash, _xformSystem.GetMapCoordinates(uid)); + var trashUid = Spawn(trash, _transform.GetMapCoordinates(uid)); // try putting the trash in the food's container too, to be consistent with slice spawning? - if (_containerSystem.TryGetContainingContainer(uid, out var container) && _containerSystem.CanInsert(trashUid, container)) - { - _containerSystem.Insert(trashUid, container); - } - else // puts it down "right-side up" - { - _xformSystem.AttachToGridOrMap(trashUid); - _xformSystem.SetLocalRotation(trashUid, 0); - } - - QueueDel(uid); + _transform.DropNextTo(trashUid, uid); + _transform.SetLocalRotation(trashUid, 0); } - private void FillSlice(EntityUid sliceUid, Solution solution) - { - // Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition) - if (TryComp<FoodComponent>(sliceUid, out var sliceFoodComp) && - _solutionContainerSystem.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSoln, out var itsSolution)) - { - _solutionContainerSystem.RemoveAllSolution(itsSoln.Value); - - var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume); - _solutionContainerSystem.TryAddSolution(itsSoln.Value, lostSolutionPart); - } - } + QueueDel(uid); + } - private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args) + private void FillSlice(EntityUid sliceUid, Solution solution) + { + // Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition) + if (TryComp<FoodComponent>(sliceUid, out var sliceFoodComp) && + _solutionContainer.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSoln, out var itsSolution)) { - entity.Comp.Count = entity.Comp.TotalCount; + _solutionContainer.RemoveAllSolution(itsSoln.Value); - var foodComp = EnsureComp<FoodComponent>(entity); - _solutionContainerSystem.EnsureSolution(entity.Owner, foodComp.Solution); + var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume); + _solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart); } + } - private void OnExamined(Entity<SliceableFoodComponent> entity, ref ExaminedEvent args) - { - args.PushMarkup(Loc.GetString("sliceable-food-component-on-examine-remaining-slices-text", ("remainingCount", entity.Comp.Count))); - } + private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args) + { + var foodComp = EnsureComp<FoodComponent>(entity); + _solutionContainer.EnsureSolution(entity.Owner, foodComp.Solution, out _); } } diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs index d5bff967b3c..f7650f599b4 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs @@ -114,8 +114,7 @@ private void OnVapeInteraction(Entity<VapeComponent> entity, ref AfterInteractEv var vapeDoAfterEvent = new VapeDoAfterEvent(solution, forced); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, delay, vapeDoAfterEvent, entity.Owner, target: args.Target, used: entity.Owner) { - BreakOnTargetMove = true, - BreakOnUserMove = false, + BreakOnMove = false, BreakOnDamage = true }); } diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs index a33c944c994..7f8253efedf 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs @@ -142,7 +142,7 @@ public override void Update(float frameTime) // This is awful. I hate this so much. // TODO: Please, someone refactor containers and free me from this bullshit. - if (!_container.TryGetContainingContainer(uid, out var containerManager) || + if (!_container.TryGetContainingContainer((uid, null, null), out var containerManager) || !(_inventorySystem.TryGetSlotEntity(containerManager.Owner, "mask", out var inMaskSlotUid) && inMaskSlotUid == uid) || !TryComp(containerManager.Owner, out BloodstreamComponent? bloodstream)) { diff --git a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs index 43087214a45..df528433c70 100644 --- a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs @@ -43,7 +43,7 @@ private void OnAfterInteract(EntityUid uid, UtensilComponent component, AfterInt public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, UtensilComponent component) { if (!EntityManager.TryGetComponent(target, out FoodComponent? food)) - return (false, true); + return (false, false); //Prevents food usage with a wrong utensil if ((food.Utensil & component.Types) == 0) diff --git a/Content.Server/Nyanotrasen/Abilities/Oni/OniSystem.cs b/Content.Server/Nyanotrasen/Abilities/Oni/OniSystem.cs index a7e2295b751..bb3a91e598f 100644 --- a/Content.Server/Nyanotrasen/Abilities/Oni/OniSystem.cs +++ b/Content.Server/Nyanotrasen/Abilities/Oni/OniSystem.cs @@ -30,7 +30,7 @@ private void OnEntInserted(EntityUid uid, OniComponent component, EntInsertedInt heldComp.Holder = uid; if (TryComp<ToolComponent>(args.Entity, out var tool) && _toolSystem.HasQuality(args.Entity, "Prying", tool)) - tool.SpeedModifier *= 1.66f; + _toolSystem.SetSpeedModifier((args.Entity, tool), tool.SpeedModifier * 1.66f); if (_gunSystem.TryGetGun(args.Entity, out _, out var gun)) { @@ -43,7 +43,7 @@ private void OnEntInserted(EntityUid uid, OniComponent component, EntInsertedInt private void OnEntRemoved(EntityUid uid, OniComponent component, EntRemovedFromContainerMessage args) { if (TryComp<ToolComponent>(args.Entity, out var tool) && _toolSystem.HasQuality(args.Entity, "Prying", tool)) - tool.SpeedModifier /= 1.66f; + _toolSystem.SetSpeedModifier((args.Entity, tool), tool.SpeedModifier / 1.66f); if (_gunSystem.TryGetGun(args.Entity, out _, out var gun)) { diff --git a/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs index e500e246d5b..f21c88d0834 100644 --- a/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs +++ b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs @@ -1,5 +1,5 @@ -using Content.Shared.Chemistry.Reagent; using Content.Server.Abilities.Psionics; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -9,19 +9,22 @@ namespace Content.Server.Chemistry.ReagentEffects /// Rerolls psionics once. /// </summary> [UsedImplicitly] - public sealed partial class ChemRemovePsionic : ReagentEffect + public sealed partial class ChemRemovePsionic : EntityEffect { protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-chem-remove-psionic", ("chance", Probability)); - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - if (args.Scale != 1f) + if (args is not EntityEffectReagentArgs effectArgs) + return; + + if (effectArgs.Scale != 1f) return; var psySys = args.EntityManager.EntitySysManager.GetEntitySystem<PsionicAbilitiesSystem>(); - psySys.MindBreak(args.SolutionEntity); + psySys.MindBreak(effectArgs.TargetEntity); } } } diff --git a/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs index 987a41c04a1..c3357938ac4 100644 --- a/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs +++ b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs @@ -1,5 +1,6 @@ using Content.Shared.Chemistry.Reagent; using Content.Server.Psionics; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -9,7 +10,7 @@ namespace Content.Server.Chemistry.ReagentEffects /// Rerolls psionics once. /// </summary> [UsedImplicitly] - public sealed partial class ChemRerollPsionic : ReagentEffect + public sealed partial class ChemRerollPsionic : EntityEffect { protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-chem-reroll-psionic", ("chance", Probability)); @@ -20,11 +21,13 @@ public sealed partial class ChemRerollPsionic : ReagentEffect [DataField("bonusMultiplier")] public float BonusMuliplier = 1f; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - var psySys = args.EntityManager.EntitySysManager.GetEntitySystem<PsionicsSystem>(); + if (args is not EntityEffectReagentArgs _) + return; - psySys.RerollPsionics(args.SolutionEntity, bonusMuliplier: BonusMuliplier); + var psySys = args.EntityManager.EntitySysManager.GetEntitySystem<PsionicsSystem>(); + psySys.RerollPsionics(args.TargetEntity, bonusMuliplier: BonusMuliplier); } } } diff --git a/Content.Server/Nyanotrasen/Chemistry/ReactionEffects/ChangeGlimmerReactionEffect.cs b/Content.Server/Nyanotrasen/Chemistry/EntityEffects/ChangeGlimmerReactionEffect.cs similarity index 61% rename from Content.Server/Nyanotrasen/Chemistry/ReactionEffects/ChangeGlimmerReactionEffect.cs rename to Content.Server/Nyanotrasen/Chemistry/EntityEffects/ChangeGlimmerReactionEffect.cs index 65aaf350cba..36c9e62b9d3 100644 --- a/Content.Server/Nyanotrasen/Chemistry/ReactionEffects/ChangeGlimmerReactionEffect.cs +++ b/Content.Server/Nyanotrasen/Chemistry/EntityEffects/ChangeGlimmerReactionEffect.cs @@ -1,11 +1,12 @@ using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.Psionics.Glimmer; using Robust.Shared.Prototypes; namespace Content.Server.Chemistry.ReactionEffects; [DataDefinition] -public sealed partial class ChangeGlimmerReactionEffect : ReagentEffect +public sealed partial class ChangeGlimmerReactionEffect : EntityEffect { protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-change-glimmer-reaction-effect", ("chance", Probability), @@ -17,10 +18,13 @@ public sealed partial class ChangeGlimmerReactionEffect : ReagentEffect [DataField("count")] public int Count = 1; - public override void Effect(ReagentEffectArgs args) + public override void Effect(EntityEffectBaseArgs args) { - var glimmersys = args.EntityManager.EntitySysManager.GetEntitySystem<GlimmerSystem>(); + if (args is not EntityEffectReagentArgs _) + return; - glimmersys.Glimmer += Count; + var glimmerSystem = args.EntityManager.EntitySysManager.GetEntitySystem<GlimmerSystem>(); + + glimmerSystem.Glimmer += Count; } } diff --git a/Content.Server/Nyanotrasen/Kitchen/Components/DeepFryerComponent.cs b/Content.Server/Nyanotrasen/Kitchen/Components/DeepFryerComponent.cs index 72c3297feb6..c06bc7cfd89 100644 --- a/Content.Server/Nyanotrasen/Kitchen/Components/DeepFryerComponent.cs +++ b/Content.Server/Nyanotrasen/Kitchen/Components/DeepFryerComponent.cs @@ -1,6 +1,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Construction.Prototypes; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Content.Shared.Nutrition; using Content.Shared.Nyanotrasen.Kitchen; @@ -187,7 +188,7 @@ public sealed partial class DeepFryerComponent : SharedDeepFryerComponent [ViewVariables(VVAccess.ReadWrite)] [DataField("unsafeOilVolumeEffects")] - public List<ReagentEffect> UnsafeOilVolumeEffects = new(); + public List<EntityEffect> UnsafeOilVolumeEffects = new(); /// <summary> /// What is the temperature of the vat when the deep fryer is powered? diff --git a/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.Update.cs b/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.Update.cs index 0978cffeb4b..ceab74801b2 100644 --- a/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.Update.cs +++ b/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.Update.cs @@ -4,6 +4,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Content.Shared.Popups; using Robust.Shared.Player; @@ -54,14 +55,15 @@ public override void Update(float frameTime) foreach (var effect in component.UnsafeOilVolumeEffects) { - effect.Effect(new ReagentEffectArgs(uid, - null, - component.Solution, - proto!, - reagent.Quantity, - EntityManager, - null, - 1f)); + var effectsArgs = new EntityEffectReagentArgs(uid, + EntityManager, + null, + component.Solution, + reagent.Quantity, + proto!, + null, + 1f); + effect.Effect(effectsArgs); } } diff --git a/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.cs b/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.cs index 8bfb4aec8cc..ec1b81cfb34 100644 --- a/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.cs +++ b/Content.Server/Nyanotrasen/Kitchen/EntitySystems/DeepFryerSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Audio; @@ -28,6 +28,7 @@ using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.DragDrop; +using Content.Shared.EntityEffects; using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.Hands.Components; @@ -38,10 +39,12 @@ using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Events; +using Content.Shared.Nutrition; using Content.Shared.Nyanotrasen.Kitchen; using Content.Shared.Nyanotrasen.Kitchen.Components; using Content.Shared.Nyanotrasen.Kitchen.UI; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Throwing; using Content.Shared.UserInterface; using Content.Shared.Whitelist; @@ -123,7 +126,7 @@ public override void Initialize() SubscribeLocalEvent<DeepFriedComponent, ComponentInit>(OnInitDeepFried); SubscribeLocalEvent<DeepFriedComponent, ExaminedEvent>(OnExamineFried); SubscribeLocalEvent<DeepFriedComponent, PriceCalculationEvent>(OnPriceCalculation); - SubscribeLocalEvent<DeepFriedComponent, SliceFoodEvent>(OnSliceDeepFried); + SubscribeLocalEvent<DeepFriedComponent, FoodSlicedEvent>(OnSliceDeepFried); } private void UpdateUserInterface(EntityUid uid, DeepFryerComponent component) @@ -397,12 +400,12 @@ private void OnInitDeepFryer(EntityUid uid, DeepFryerComponent component, Compon { //JJ Comment - not sure this works. Need to check if Reagent.ToString is correct. _prototypeManager.TryIndex<ReagentPrototype>(reagent.Reagent.ToString(), out var proto); - var effectsArgs = new ReagentEffectArgs(uid, + var effectsArgs = new EntityEffectReagentArgs(uid, + EntityManager, null, component.Solution, - proto!, reagent.Quantity, - EntityManager, + proto!, null, 1f); foreach (var effect in component.UnsafeOilVolumeEffects) @@ -644,8 +647,7 @@ private void OnClearSlagStart(EntityUid uid, DeepFryerComponent component, DeepF var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, uid, heldItem) { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, MovementThreshold = 0.25f, NeedHand = true }; @@ -723,7 +725,7 @@ private void OnPriceCalculation(EntityUid uid, DeepFriedComponent component, ref args.Price *= component.PriceCoefficient; } - private void OnSliceDeepFried(EntityUid uid, DeepFriedComponent component, SliceFoodEvent args) + private void OnSliceDeepFried(EntityUid uid, DeepFriedComponent component, FoodSlicedEvent args) { MakeCrispy(args.Slice); diff --git a/Content.Server/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs b/Content.Server/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs index 88da3093d74..15df39eed8c 100644 --- a/Content.Server/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs +++ b/Content.Server/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs @@ -7,6 +7,9 @@ using Content.Server.Power.Components; using Content.Server.Construction; using Content.Server.Popups; +using Content.Shared.Power; +using Content.Shared.Research.Prototypes; +using Content.Shared.Research.TechnologyDisk.Components; using Content.Shared.UserInterface; using Robust.Shared.Containers; using Robust.Shared.Random; @@ -14,6 +17,7 @@ using Robust.Shared.Timing; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; namespace Content.Server.ReverseEngineering; @@ -324,7 +328,7 @@ private void FinishProbe(EntityUid uid, ReverseEngineeringMachineComponent? comp UpdateUserInterface(uid, component); } - private void CreateDisk(EntityUid uid, string diskPrototype, List<string>? recipes) + private void CreateDisk(EntityUid uid, string diskPrototype, List<ProtoId<LatheRecipePrototype>>? recipes) { var disk = Spawn(diskPrototype, Transform(uid).Coordinates); diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs index bf013bc0402..18077b413ad 100644 --- a/Content.Server/Objectives/ObjectivesSystem.cs +++ b/Content.Server/Objectives/ObjectivesSystem.cs @@ -36,14 +36,14 @@ public override void Initialize() private void OnRoundEndText(RoundEndTextAppendEvent ev) { // go through each gamerule getting data for the roundend summary. - var summaries = new Dictionary<string, Dictionary<string, List<EntityUid>>>(); + var summaries = new Dictionary<string, Dictionary<string, List<(EntityUid, string)>>>(); var query = EntityQueryEnumerator<GameRuleComponent>(); while (query.MoveNext(out var uid, out var gameRule)) { if (!_gameTicker.IsGameRuleAdded(uid, gameRule)) continue; - var info = new ObjectivesTextGetInfoEvent(new List<EntityUid>(), string.Empty); + var info = new ObjectivesTextGetInfoEvent(new List<(EntityUid, string)>(), string.Empty); RaiseLocalEvent(uid, ref info); if (info.Minds.Count == 0) continue; @@ -51,7 +51,7 @@ private void OnRoundEndText(RoundEndTextAppendEvent ev) // first group the gamerules by their agents, for example 2 different dragons var agent = info.AgentName; if (!summaries.ContainsKey(agent)) - summaries[agent] = new Dictionary<string, List<EntityUid>>(); + summaries[agent] = new Dictionary<string, List<(EntityUid, string)>>(); var prepend = new ObjectivesTextPrependEvent(""); RaiseLocalEvent(uid, ref prepend); @@ -79,7 +79,7 @@ private void OnRoundEndText(RoundEndTextAppendEvent ev) foreach (var (_, minds) in summary) { total += minds.Count; - totalInCustody += minds.Where(m => IsInCustody(m)).Count(); + totalInCustody += minds.Where(pair => IsInCustody(pair.Item1)).Count(); } var result = new StringBuilder(); @@ -104,19 +104,16 @@ private void OnRoundEndText(RoundEndTextAppendEvent ev) } } - private void AddSummary(StringBuilder result, string agent, List<EntityUid> minds) + private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds) { var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>(); - foreach (var mindId in minds) + foreach (var (mindId, name) in minds) { - if (!TryComp(mindId, out MindComponent? mind)) - continue; - - var title = GetTitle(mindId, mind); - if (title == null) + if (!TryComp<MindComponent>(mindId, out var mind)) continue; + var title = GetTitle((mindId, mind), name); var custody = IsInCustody(mindId, mind) ? Loc.GetString("objectives-in-custody") : string.Empty; var objectives = mind.Objectives; @@ -238,34 +235,18 @@ private bool IsInCustody(EntityUid mindId, MindComponent? mind = null) /// <summary> /// Get the title for a player's mind used in round end. + /// Pass in the original entity name which is shown alongside username. /// </summary> - public string? GetTitle(EntityUid mindId, MindComponent? mind = null) + public string GetTitle(Entity<MindComponent?> mind, string name) { - if (!Resolve(mindId, ref mind)) - return null; - - var name = mind.CharacterName; - var username = (string?) null; - - if (mind.OriginalOwnerUserId != null && - _player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData)) + if (Resolve(mind, ref mind.Comp) && + mind.Comp.OriginalOwnerUserId != null && + _player.TryGetPlayerData(mind.Comp.OriginalOwnerUserId.Value, out var sessionData)) { - username = sessionData.UserName; + var username = sessionData.UserName; + return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name)); } - - if (username != null) - { - if (name != null) - return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name)); - - return Loc.GetString("objectives-player-user", ("user", username)); - } - - // nothing to identify the player by, just give up - if (name == null) - return null; - return Loc.GetString("objectives-player-named", ("name", name)); } } @@ -279,7 +260,7 @@ private bool IsInCustody(EntityUid mindId, MindComponent? mind = null) /// The objectives system already checks if the game rule is added so you don't need to check that in this event's handler. /// </remarks> [ByRefEvent] -public record struct ObjectivesTextGetInfoEvent(List<EntityUid> Minds, string AgentName); +public record struct ObjectivesTextGetInfoEvent(List<(EntityUid, string)> Minds, string AgentName); /// <summary> /// Raised on the game rule before text for each agent's objectives is added, letting you prepend something. diff --git a/Content.Server/Objectives/Systems/CodeConditionSystem.cs b/Content.Server/Objectives/Systems/CodeConditionSystem.cs index 7ba312f4bb9..fbc58dafe82 100644 --- a/Content.Server/Objectives/Systems/CodeConditionSystem.cs +++ b/Content.Server/Objectives/Systems/CodeConditionSystem.cs @@ -35,20 +35,6 @@ public bool IsCompleted(Entity<CodeConditionComponent?> ent) return ent.Comp.Completed; } - /// <summary> - /// Returns true if a mob's objective with a certain prototype is completed. - /// </summary> - public bool IsCompleted(Entity<MindContainerComponent?> mob, string prototype) - { - if (_mind.GetMind(mob, mob.Comp) is not {} mindId) - return false; - - if (!_mind.TryFindObjective(mindId, prototype, out var obj)) - return false; - - return IsCompleted(obj.Value); - } - /// <summary> /// Sets an objective's completed field. /// </summary> diff --git a/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs b/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs index 56b245ce847..8fe7e7e2031 100644 --- a/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs +++ b/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Objectives.Components; using Content.Shared.Objectives.Components; +using Content.Shared.Whitelist; namespace Content.Server.Objectives.Systems; @@ -8,6 +9,8 @@ namespace Content.Server.Objectives.Systems; /// </summary> public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem { + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + public override void Initialize() { base.Initialize(); @@ -22,7 +25,7 @@ private void OnCheck(EntityUid uid, ObjectiveBlacklistRequirementComponent comp, foreach (var objective in args.Mind.AllObjectives) { - if (comp.Blacklist.IsValid(objective, EntityManager)) + if (_whitelistSystem.IsBlacklistPass(comp.Blacklist, objective)) { args.Cancelled = true; return; diff --git a/Content.Server/Objectives/Systems/RoleRequirementSystem.cs b/Content.Server/Objectives/Systems/RoleRequirementSystem.cs index 97aee218f06..8421987bae9 100644 --- a/Content.Server/Objectives/Systems/RoleRequirementSystem.cs +++ b/Content.Server/Objectives/Systems/RoleRequirementSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Objectives.Components; using Content.Shared.Objectives.Components; +using Content.Shared.Whitelist; namespace Content.Server.Objectives.Systems; @@ -8,6 +9,7 @@ namespace Content.Server.Objectives.Systems; /// </summary> public sealed class RoleRequirementSystem : EntitySystem { + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { base.Initialize(); @@ -22,7 +24,7 @@ private void OnCheck(EntityUid uid, RoleRequirementComponent comp, ref Requireme // this whitelist trick only works because roles are components on the mind and not entities // if that gets reworked then this will need changing - if (!comp.Roles.IsValid(args.MindId, EntityManager)) + if (_whitelistSystem.IsWhitelistFail(comp.Roles, args.MindId)) args.Cancelled = true; } } diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index d4934ee24e5..9401d68de3e 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -8,6 +8,7 @@ using Content.Server.Station.Systems; using Content.Server.Store.Components; using Content.Server.Store.Systems; +using Content.Server.Traitor.Uplink; using Content.Shared.Access.Components; using Content.Shared.CartridgeLoader; using Content.Shared.Chat; @@ -15,6 +16,7 @@ using Content.Shared.Light.Components; using Content.Shared.Light.EntitySystems; using Content.Shared.PDA; +using Content.Shared.Store.Components; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Containers; @@ -121,7 +123,7 @@ private void OnNotification(Entity<PdaComponent> ent, ref CartridgeLoaderNotific { _ringer.RingerPlayRingtone(ent.Owner); - if (!_containerSystem.TryGetContainingContainer(ent, out var container) + if (!_containerSystem.TryGetContainingContainer((ent, null, null), out var container) || !TryComp<ActorComponent>(container.Owner, out var actor)) return; @@ -152,7 +154,7 @@ public void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null) var address = GetDeviceNetAddress(uid); var hasInstrument = HasComp<InstrumentComponent>(uid); - var showUplink = HasComp<StoreComponent>(uid) && IsUnlocked(uid); + var showUplink = HasComp<UplinkComponent>(uid) && IsUnlocked(uid); UpdateStationName(uid, pda); UpdateAlertLevel(uid, pda); @@ -175,7 +177,7 @@ public void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null) { ActualOwnerName = pda.OwnerName, IdOwner = id?.FullName, - JobTitle = id?.JobTitle, + JobTitle = id?.LocalizedJobTitle, StationAlertLevel = pda.StationAlertLevel, StationAlertColor = pda.StationAlertColor }, @@ -237,8 +239,8 @@ private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowUplinkMessage m return; // check if its locked again to prevent malicious clients opening locked uplinks - if (TryComp<StoreComponent>(uid, out var store) && IsUnlocked(uid)) - _store.ToggleUi(msg.Actor, uid, store); + if (HasComp<UplinkComponent>(uid) && IsUnlocked(uid)) + _store.ToggleUi(msg.Actor, uid); } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage msg) diff --git a/Content.Server/PDA/Ringer/RingerSystem.cs b/Content.Server/PDA/Ringer/RingerSystem.cs index a10544d6966..e15dcfaa2bc 100644 --- a/Content.Server/PDA/Ringer/RingerSystem.cs +++ b/Content.Server/PDA/Ringer/RingerSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.PDA.Ringer; using Content.Shared.Popups; using Content.Shared.Store; +using Content.Shared.Store.Components; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Network; @@ -25,6 +26,7 @@ public sealed class RingerSystem : SharedRingerSystem [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; private readonly Dictionary<NetUserId, TimeSpan> _lastSetRingtoneAt = new(); @@ -210,7 +212,7 @@ public bool ToggleRingerUI(EntityUid uid, EntityUid actor) _audio.PlayEntity( GetSound(ringer.Ringtone[ringer.NoteCount]), - Filter.Empty().AddInRange(ringerXform.MapPosition, ringer.Range), + Filter.Empty().AddInRange(_transform.GetMapCoordinates(uid, ringerXform), ringer.Range), uid, true, AudioParams.Default.WithMaxDistance(ringer.Range).WithVolume(ringer.Volume) diff --git a/Content.Server/Paint/PaintSystem.cs b/Content.Server/Paint/PaintSystem.cs index e8ef4d21e12..89d39e9e49d 100644 --- a/Content.Server/Paint/PaintSystem.cs +++ b/Content.Server/Paint/PaintSystem.cs @@ -3,18 +3,15 @@ using Content.Shared.Sprite; using Content.Shared.DoAfter; using Content.Shared.Interaction; -using Content.Server.Chemistry.Containers.EntitySystems; using Robust.Shared.Audio.Systems; using Content.Shared.Humanoid; using Robust.Shared.Utility; using Content.Shared.Verbs; using Content.Shared.SubFloor; -using Content.Server.Nutrition.Components; using Content.Shared.Inventory; -using Content.Server.Nutrition.EntitySystems; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Whitelist; -using Robust.Shared.Audio; namespace Content.Server.Paint; @@ -25,11 +22,12 @@ public sealed class PaintSystem : SharedPaintSystem { [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly OpenableSystem _openable = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; public override void Initialize() @@ -57,7 +55,6 @@ private void OnPaintVerb(EntityUid uid, PaintComponent component, GetVerbsEvent< return; var paintText = Loc.GetString("paint-verb"); - var verb = new UtilityVerb() { Act = () => @@ -68,12 +65,13 @@ private void OnPaintVerb(EntityUid uid, PaintComponent component, GetVerbsEvent< Text = paintText, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/paint.svg.192dpi.png")) }; + args.Verbs.Add(verb); } private void PrepPaint(EntityUid uid, PaintComponent component, EntityUid target, EntityUid user) => _doAfterSystem.TryStartDoAfter( - new DoAfterArgs( + new( EntityManager, user, component.Delay, @@ -82,8 +80,7 @@ private void PrepPaint(EntityUid uid, PaintComponent component, EntityUid target target: target, used: uid) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, NeedHand = true, BreakOnHandChange = true, }); @@ -111,8 +108,8 @@ public void Paint(Entity<PaintComponent> entity, EntityUid target, EntityUid use return; } - if (!entity.Comp.Whitelist?.IsValid(target, EntityManager) == true - || !entity.Comp.Blacklist?.IsValid(target, EntityManager) == false + if (_whitelist.IsWhitelistFail(entity.Comp.Whitelist, target) + || _whitelist.IsBlacklistPass(entity.Comp.Blacklist, target) || HasComp<HumanoidAppearanceComponent>(target) || HasComp<SubFloorHideComponent>(target)) { _popup.PopupEntity(Loc.GetString("paint-failure", ("target", target)), user, user, PopupType.Medium); @@ -131,12 +128,13 @@ public void Paint(Entity<PaintComponent> entity, EntityUid target, EntityUid use // Paint any clothing the target is wearing if (HasComp<InventoryComponent>(target) && _inventory.TryGetSlots(target, out var slotDefinitions)) + { foreach (var slot in slotDefinitions) { if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt) || HasComp<PaintedComponent>(slotEnt.Value) - || !entity.Comp.Whitelist?.IsValid(slotEnt.Value, EntityManager) != true - || !entity.Comp.Blacklist?.IsValid(slotEnt.Value, EntityManager) != false + || _whitelist.IsWhitelistFail(entity.Comp.Whitelist, slotEnt.Value) + || _whitelist.IsBlacklistPass(entity.Comp.Blacklist, slotEnt.Value) || HasComp<RandomSpriteComponent>(slotEnt.Value) || HasComp<HumanoidAppearanceComponent>(slotEnt.Value)) continue; @@ -147,6 +145,7 @@ public void Paint(Entity<PaintComponent> entity, EntityUid target, EntityUid use _appearanceSystem.SetData(slotEnt.Value, PaintVisuals.Painted, true); Dirty(slotEnt.Value, slotToPaint); } + } _popup.PopupEntity(Loc.GetString("paint-success", ("target", target)), user, user, PopupType.Medium); _appearanceSystem.SetData(target, PaintVisuals.Painted, true); @@ -160,8 +159,8 @@ public void Paint(Entity<PaintComponent> entity, EntityUid target, EntityUid use public void Paint(EntityWhitelist? whitelist, EntityWhitelist? blacklist, EntityUid target, Color color) { - if (!whitelist?.IsValid(target, EntityManager) != true - || !blacklist?.IsValid(target, EntityManager) != false) + if (_whitelist.IsWhitelistFail(whitelist, target) + || _whitelist.IsBlacklistPass(blacklist, target)) return; EnsureComp<PaintedComponent>(target, out var paint); @@ -172,11 +171,12 @@ public void Paint(EntityWhitelist? whitelist, EntityWhitelist? blacklist, Entity if (HasComp<InventoryComponent>(target) && _inventory.TryGetSlots(target, out var slotDefinitions)) + { foreach (var slot in slotDefinitions) { if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt) - || !whitelist?.IsValid(slotEnt.Value, EntityManager) != true - || !blacklist?.IsValid(slotEnt.Value, EntityManager) != false) + || _whitelist.IsWhitelistFail(whitelist, slotEnt.Value) + || _whitelist.IsBlacklistPass(blacklist, slotEnt.Value)) continue; EnsureComp<PaintedComponent>(slotEnt.Value, out var slotToPaint); @@ -185,6 +185,7 @@ public void Paint(EntityWhitelist? whitelist, EntityWhitelist? blacklist, Entity _appearanceSystem.SetData(slotEnt.Value, PaintVisuals.Painted, true); Dirty(slotEnt.Value, slotToPaint); } + } _appearanceSystem.SetData(target, PaintVisuals.Painted, true); Dirty(target, paint); diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs index f3cff9f2e72..14dca2c7bb2 100644 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs +++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using Content.Server.Administration.Managers; using Content.Shared.CCVar; +using Content.Shared.Power; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; diff --git a/Content.Server/Payload/EntitySystems/PayloadSystem.cs b/Content.Server/Payload/EntitySystems/PayloadSystem.cs index 85cf303d5d7..15966956d4f 100644 --- a/Content.Server/Payload/EntitySystems/PayloadSystem.cs +++ b/Content.Server/Payload/EntitySystems/PayloadSystem.cs @@ -10,6 +10,7 @@ using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; using System.Linq; +using Robust.Server.GameObjects; namespace Content.Server.Payload.EntitySystems; @@ -17,6 +18,7 @@ public sealed class PayloadSystem : EntitySystem { [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly ISerializationManager _serializationManager = default!; @@ -158,7 +160,7 @@ private void HandleChemicalPayloadTrigger(Entity<ChemicalPayloadComponent> entit var solStringB = SolutionContainerSystem.ToPrettyString(solutionB); _adminLogger.Add(LogType.ChemicalReaction, - $"Chemical bomb payload {ToPrettyString(entity.Owner):payload} at {Transform(entity.Owner).MapPosition:location} is combining two solutions: {solStringA:solutionA} and {solStringB:solutionB}"); + $"Chemical bomb payload {ToPrettyString(entity.Owner):payload} at {_transform.GetMapCoordinates(entity.Owner):location} is combining two solutions: {solStringA:solutionA} and {solStringB:solutionB}"); solutionA.MaxVolume += solutionB.MaxVolume; _solutionContainerSystem.TryAddSolution(solnA.Value, solutionB); diff --git a/Content.Server/Physics/Controllers/ConveyorController.cs b/Content.Server/Physics/Controllers/ConveyorController.cs index b3508025cb9..e459d7e87e0 100644 --- a/Content.Server/Physics/Controllers/ConveyorController.cs +++ b/Content.Server/Physics/Controllers/ConveyorController.cs @@ -6,6 +6,7 @@ using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Physics.Controllers; +using Content.Shared.Power; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; diff --git a/Content.Server/Pinpointer/StationMapComponent.cs b/Content.Server/Pinpointer/StationMapUserComponent.cs similarity index 77% rename from Content.Server/Pinpointer/StationMapComponent.cs rename to Content.Server/Pinpointer/StationMapUserComponent.cs index 942ea1aba8e..358e26d82e8 100644 --- a/Content.Server/Pinpointer/StationMapComponent.cs +++ b/Content.Server/Pinpointer/StationMapUserComponent.cs @@ -1,11 +1,5 @@ namespace Content.Server.Pinpointer; -[RegisterComponent] -public sealed partial class StationMapComponent : Component -{ - -} - /// <summary> /// Added to an entity using station map so when its parent changes we reset it. /// </summary> diff --git a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs index 6e0e0c503a9..7882522d30b 100644 --- a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs +++ b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs @@ -7,7 +7,7 @@ using Content.Shared.Interaction; using Content.Shared.PneumaticCannon; using Content.Shared.StatusEffect; -using Content.Shared.Tools.Components; +using Content.Shared.Tools.Systems; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; @@ -22,6 +22,7 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem [Dependency] private readonly GunSystem _gun = default!; [Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; public override void Initialize() { @@ -38,10 +39,7 @@ private void OnInteractUsing(EntityUid uid, PneumaticCannonComponent component, if (args.Handled) return; - if (!TryComp<ToolComponent>(args.Used, out var tool)) - return; - - if (!tool.Qualities.Contains(component.ToolModifyPower)) + if (!_toolSystem.HasQuality(args.Used, component.ToolModifyPower)) return; var val = (int) component.Power; diff --git a/Content.Server/Pointing/EntitySystems/PointingSystem.cs b/Content.Server/Pointing/EntitySystems/PointingSystem.cs index c9e86772fb9..3acf5d26964 100644 --- a/Content.Server/Pointing/EntitySystems/PointingSystem.cs +++ b/Content.Server/Pointing/EntitySystems/PointingSystem.cs @@ -183,7 +183,7 @@ bool ViewerPredicate(ICommonSession playerSession) (eyeComp.VisibilityMask & layer) == 0) return false; - return Transform(ent).MapPosition.InRange(Transform(player).MapPosition, PointingRange); + return _transform.GetMapCoordinates(ent).InRange(_transform.GetMapCoordinates(player), PointingRange); } var viewers = Filter.Empty() diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.Collide.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.Collide.cs index b4240d409ad..b29f46e09e9 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.Collide.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.Collide.cs @@ -1,6 +1,7 @@ using Content.Server.Polymorph.Components; using Content.Shared.Polymorph; using Content.Shared.Projectiles; +using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.Physics.Events; using Robust.Shared.Prototypes; @@ -9,6 +10,8 @@ namespace Content.Server.Polymorph.Systems; public partial class PolymorphSystem { + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + /// <summary> /// Need to do this so we don't get a collection enumeration error in physics by polymorphing /// an entity we're colliding with @@ -39,8 +42,8 @@ private void OnPolymorphCollide(EntityUid uid, PolymorphOnCollideComponent compo return; var other = args.OtherEntity; - if (!component.Whitelist.IsValid(other, EntityManager) - || component.Blacklist != null && component.Blacklist.IsValid(other, EntityManager)) + if (_whitelistSystem.IsWhitelistFail(component.Whitelist, other) || + _whitelistSystem.IsBlacklistPass(component.Blacklist, other)) return; _queuedPolymorphUpdates.Enqueue(new (other, component.Sound, component.Polymorph)); diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index 5c970b1a748..913299eea91 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -226,7 +226,7 @@ private void OnDestruction(Entity<PolymorphedEntityComponent> ent, ref Destructi var childXform = Transform(child); _transform.SetLocalRotation(child, targetTransformComp.LocalRotation, childXform); - if (_container.TryGetContainingContainer(uid, out var cont)) + if (_container.TryGetContainingContainer((uid, targetTransformComp, null), out var cont)) _container.Insert(child, cont); //Transfers all damage from the original to the new one diff --git a/Content.Server/Polymorph/Toolshed/PolymorphCommand.cs b/Content.Server/Polymorph/Toolshed/PolymorphCommand.cs index 5654c84722f..db1e1faad6b 100644 --- a/Content.Server/Polymorph/Toolshed/PolymorphCommand.cs +++ b/Content.Server/Polymorph/Toolshed/PolymorphCommand.cs @@ -20,7 +20,7 @@ public sealed class PolymorphCommand : ToolshedCommand [CommandImplementation] public EntityUid? Polymorph( [PipedArgument] EntityUid input, - [CommandArgument] ProtoId<PolymorphPrototype> protoId + ProtoId<PolymorphPrototype> protoId ) { _system ??= GetSys<PolymorphSystem>(); @@ -34,7 +34,7 @@ [CommandArgument] ProtoId<PolymorphPrototype> protoId [CommandImplementation] public IEnumerable<EntityUid> Polymorph( [PipedArgument] IEnumerable<EntityUid> input, - [CommandArgument] ProtoId<PolymorphPrototype> protoId + ProtoId<PolymorphPrototype> protoId ) => input.Select(x => Polymorph(x, protoId)).Where(x => x is not null).Select(x => (EntityUid)x!); } diff --git a/Content.Server/Power/Components/ApcPowerReceiverComponent.cs b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs index b8340d0be4b..ebb3c6b42f3 100644 --- a/Content.Server/Power/Components/ApcPowerReceiverComponent.cs +++ b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs @@ -1,5 +1,6 @@ using Content.Server.Power.NodeGroups; using Content.Server.Power.Pow3r; +using Content.Shared.Power.Components; namespace Content.Server.Power.Components { @@ -8,11 +9,8 @@ namespace Content.Server.Power.Components /// so that it can receive power from a <see cref="IApcNet"/>. /// </summary> [RegisterComponent] - public sealed partial class ApcPowerReceiverComponent : Component + public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent { - [ViewVariables] - public bool Powered => (MathHelper.CloseToPercent(NetworkLoad.ReceivingPower, Load) || !NeedsPower) && !PowerDisabled; - /// <summary> /// Amount of charge this needs from an APC per second to function. /// </summary> @@ -33,7 +31,7 @@ public bool NeedsPower { _needsPower = value; // Reset this so next tick will do a power update. - PoweredLastUpdate = null; + Recalculate = true; } } @@ -50,7 +48,8 @@ public bool PowerDisabled { set => NetworkLoad.Enabled = !value; } - public bool? PoweredLastUpdate; + // TODO Is this needed? It forces a PowerChangedEvent when NeedsPower is toggled even if it changes to the same state. + public bool Recalculate; [ViewVariables] public PowerState.Load NetworkLoad { get; } = new PowerState.Load @@ -60,16 +59,4 @@ public bool PowerDisabled { public float PowerReceived => NetworkLoad.ReceivingPower; } - - /// <summary> - /// Raised whenever an ApcPowerReceiver becomes powered / unpowered. - /// Does nothing on the client. - /// </summary> - [ByRefEvent] - public readonly record struct PowerChangedEvent(bool Powered, float ReceivingPower) - { - public readonly bool Powered = Powered; - public readonly float ReceivingPower = ReceivingPower; - } - } diff --git a/Content.Server/Power/Components/BatterySelfRechargerComponent.cs b/Content.Server/Power/Components/BatterySelfRechargerComponent.cs index 4798beb7573..1cb92d9cd63 100644 --- a/Content.Server/Power/Components/BatterySelfRechargerComponent.cs +++ b/Content.Server/Power/Components/BatterySelfRechargerComponent.cs @@ -1,3 +1,5 @@ +using System; + namespace Content.Server.Power.Components { /// <summary> @@ -6,8 +8,29 @@ namespace Content.Server.Power.Components [RegisterComponent] public sealed partial class BatterySelfRechargerComponent : Component { - [ViewVariables(VVAccess.ReadWrite)] [DataField("autoRecharge")] public bool AutoRecharge { get; set; } + /// <summary> + /// Does the entity auto recharge? + /// </summary> + [DataField] public bool AutoRecharge; + + /// <summary> + /// At what rate does the entity automatically recharge? + /// </summary> + [DataField] public float AutoRechargeRate; + + /// <summary> + /// Should this entity stop automatically recharging if a charge is used? + /// </summary> + [DataField] public bool AutoRechargePause = false; + + /// <summary> + /// How long should the entity stop automatically recharging if a charge is used? + /// </summary> + [DataField] public float AutoRechargePauseTime = 0f; - [ViewVariables(VVAccess.ReadWrite)] [DataField("autoRechargeRate")] public float AutoRechargeRate { get; set; } + /// <summary> + /// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system. + /// </summary> + [DataField] public TimeSpan NextAutoRecharge = TimeSpan.FromSeconds(0f); } } diff --git a/Content.Server/Power/Components/CableComponent.cs b/Content.Server/Power/Components/CableComponent.cs index a2a02a60f68..7398bc0616e 100644 --- a/Content.Server/Power/Components/CableComponent.cs +++ b/Content.Server/Power/Components/CableComponent.cs @@ -4,6 +4,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using System.Diagnostics.Tracing; +using Content.Shared.Tools.Systems; namespace Content.Server.Power.Components; @@ -14,11 +15,11 @@ namespace Content.Server.Power.Components; [Access(typeof(CableSystem))] public sealed partial class CableComponent : Component { - [DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string CableDroppedOnCutPrototype = "CableHVStack1"; + [DataField] + public EntProtoId CableDroppedOnCutPrototype = "CableHVStack1"; - [DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))] - public string CuttingQuality = "Cutting"; + [DataField] + public ProtoId<ToolQualityPrototype> CuttingQuality = SharedToolSystem.CutQuality; /// <summary> /// Checked by <see cref="CablePlacerComponent"/> to determine if there is diff --git a/Content.Server/Power/Components/PowerChargeComponent.cs b/Content.Server/Power/Components/PowerChargeComponent.cs new file mode 100644 index 00000000000..03c6e8e2bca --- /dev/null +++ b/Content.Server/Power/Components/PowerChargeComponent.cs @@ -0,0 +1,66 @@ +using Content.Server.Power.EntitySystems; +using Content.Shared.Power; + +namespace Content.Server.Power.Components; + +/// <inheritdoc cref="Content.Shared.Power.SharedPowerChargeComponent" /> +[RegisterComponent] +[Access(typeof(PowerChargeSystem))] +public sealed partial class PowerChargeComponent : SharedPowerChargeComponent +{ + /// <summary> + /// Change in charge per second. + /// </summary> + [DataField] + public float ChargeRate { get; set; } = 0.01f; + + /// <summary> + /// Baseline power that this machine consumes. + /// </summary> + [DataField("idlePower")] + public float IdlePowerUse { get; set; } + + /// <summary> + /// Power consumed when <see cref="SwitchedOn"/> is true. + /// </summary> + [DataField("activePower")] + public float ActivePowerUse { get; set; } + + /// <summary> + /// Is the gravity generator intact? + /// </summary> + [DataField] + public bool Intact { get; set; } = true; + + /// <summary> + /// Is the power switch on? + /// </summary> + [DataField] + public bool SwitchedOn { get; set; } = true; + + /// <summary> + /// Whether or not the power is switched on and the entity has charged up. + /// </summary> + [DataField] + public bool Active { get; set; } + + [DataField] + public float MaxCharge { get; set; } = 1; + + /// <summary> + /// The UI key of the UI that's used with this machine.<br/> + /// This is used to allow machine power charging to be integrated into any ui + /// </summary> + [DataField, ViewVariables(VVAccess.ReadOnly)] + public Enum UiKey { get; set; } = PowerChargeUiKey.Key; + + /// <summary> + /// Current charge value. + /// Goes from 0 to 1. + /// </summary> + [DataField] + public float Charge { get; set; } = 1; + + [ViewVariables] + public bool NeedUIUpdate { get; set; } +} diff --git a/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs b/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs index 72843a65b84..e1c223c01e4 100644 --- a/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs +++ b/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs @@ -1,5 +1,8 @@ using Content.Shared.Popups; using Content.Server.Power.Components; +using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.UserInterface; using JetBrains.Annotations; using Content.Shared.Wires; diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs index cbe61f66717..8d261622152 100644 --- a/Content.Server/Power/EntitySystems/BatterySystem.cs +++ b/Content.Server/Power/EntitySystems/BatterySystem.cs @@ -4,14 +4,18 @@ using Content.Server.Power.Components; using Content.Shared.Examine; using Content.Shared.Rejuvenate; +using Content.Shared.Timing; using JetBrains.Annotations; using Robust.Shared.Utility; +using Robust.Shared.Timing; namespace Content.Server.Power.EntitySystems { [UsedImplicitly] public sealed class BatterySystem : EntitySystem { + [Dependency] protected readonly IGameTiming Timing = default!; + public override void Initialize() { base.Initialize(); @@ -86,7 +90,14 @@ public override void Update(float frameTime) { if (!comp.AutoRecharge) continue; if (batt.IsFullyCharged) continue; - TrySetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt); + + if (comp.AutoRechargePause) + { + if (comp.NextAutoRecharge > Timing.CurTime) + continue; + } + + SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt); } } @@ -103,6 +114,8 @@ private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseE args.Affected = true; args.Disabled = true; UseCharge(uid, args.EnergyConsumption, component); + // Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP. + TrySetChargeCooldown(uid); } public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null) @@ -113,6 +126,10 @@ public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = n var newValue = Math.Clamp(0, battery.CurrentCharge - value, battery.MaxCharge); var delta = newValue - battery.CurrentCharge; battery.CurrentCharge = newValue; + + // Apply a cooldown to the entity's self recharge if needed. + TrySetChargeCooldown(uid); + var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge); RaiseLocalEvent(uid, ref ev); return delta; @@ -146,6 +163,40 @@ public void SetCharge(EntityUid uid, float value, BatteryComponent? battery = nu var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge); RaiseLocalEvent(uid, ref ev); } + /// <summary> + /// Checks if the entity has a self recharge and puts it on cooldown if applicable. + /// </summary> + public void TrySetChargeCooldown(EntityUid uid, float value = -1) + { + if (!TryComp<BatterySelfRechargerComponent>(uid, out var batteryself)) + return; + + if (!batteryself.AutoRechargePause) + return; + + // If no answer or a negative is given for value, use the default from AutoRechargePauseTime. + if (value < 0) + value = batteryself.AutoRechargePauseTime; + + if (Timing.CurTime + TimeSpan.FromSeconds(value) <= batteryself.NextAutoRecharge) + return; + + SetChargeCooldown(uid, batteryself.AutoRechargePauseTime, batteryself); + } + + /// <summary> + /// Puts the entity's self recharge on cooldown for the specified time. + /// </summary> + public void SetChargeCooldown(EntityUid uid, float value, BatterySelfRechargerComponent? batteryself = null) + { + if (!Resolve(uid, ref batteryself)) + return; + + if (value >= 0) + batteryself.NextAutoRecharge = Timing.CurTime + TimeSpan.FromSeconds(value); + else + batteryself.NextAutoRecharge = Timing.CurTime; + } /// <summary> /// If sufficient charge is available on the battery, use it. Otherwise, don't. diff --git a/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs b/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs index 15b967bb1d5..4a63be894ef 100644 --- a/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs +++ b/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Tools; using Content.Shared.Examine; using Content.Shared.Interaction; +using Content.Shared.Tools.Systems; using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Utility; @@ -27,7 +28,7 @@ public override void Initialize() private void OnAfterInteractUsing(EntityUid uid, CableComponent component, AfterInteractUsingEvent args) { - if (args.Handled || args.Target == null || !args.CanReach || !_toolSystem.HasQuality(args.Used, "Pulsing")) + if (args.Handled || args.Target == null || !args.CanReach || !_toolSystem.HasQuality(args.Used, SharedToolSystem.PulseQuality)) return; var markup = FormattedMessage.FromMarkup(GenerateCableMarkup(uid)); @@ -45,7 +46,7 @@ private void OnGetExamineVerbs(EntityUid uid, CableComponent component, GetVerbs // Pulsing is hardcoded here because I don't think it needs to be more complex than that right now. // Update if I'm wrong. - var enabled = held != null && _toolSystem.HasQuality(held.Value, "Pulsing"); + var enabled = held != null && _toolSystem.HasQuality(held.Value, SharedToolSystem.PulseQuality); var verb = new ExamineVerb { Disabled = !enabled, diff --git a/Content.Server/Power/EntitySystems/ChargerSystem.cs b/Content.Server/Power/EntitySystems/ChargerSystem.cs index 038295eac11..2aa69024df3 100644 --- a/Content.Server/Power/EntitySystems/ChargerSystem.cs +++ b/Content.Server/Power/EntitySystems/ChargerSystem.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using Robust.Shared.Containers; using System.Diagnostics.CodeAnalysis; +using Content.Shared.Power.Components; using Content.Shared.Storage.Components; using Robust.Server.Containers; using Content.Shared.Whitelist; diff --git a/Content.Server/Power/EntitySystems/PowerChargeSystem.cs b/Content.Server/Power/EntitySystems/PowerChargeSystem.cs new file mode 100644 index 00000000000..7935af155a7 --- /dev/null +++ b/Content.Server/Power/EntitySystems/PowerChargeSystem.cs @@ -0,0 +1,283 @@ +using Content.Server.Administration.Logs; +using Content.Server.Audio; +using Content.Server.Power.Components; +using Content.Shared.Database; +using Content.Shared.Power; +using Content.Shared.UserInterface; +using Robust.Server.GameObjects; +using Robust.Shared.Player; + +namespace Content.Server.Power.EntitySystems; + +public sealed class PowerChargeSystem : EntitySystem +{ + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly AmbientSoundSystem _ambientSoundSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<PowerChargeComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<PowerChargeComponent, ComponentShutdown>(OnComponentShutdown); + SubscribeLocalEvent<PowerChargeComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt); + SubscribeLocalEvent<PowerChargeComponent, AfterActivatableUIOpenEvent>(OnAfterUiOpened); + SubscribeLocalEvent<PowerChargeComponent, AnchorStateChangedEvent>(OnAnchorStateChange); + + // This needs to be ui key agnostic + SubscribeLocalEvent<PowerChargeComponent, SwitchChargingMachineMessage>(OnSwitchGenerator); + } + + private void OnAnchorStateChange(EntityUid uid, PowerChargeComponent component, AnchorStateChangedEvent args) + { + if (args.Anchored || !TryComp<ApcPowerReceiverComponent>(uid, out var powerReceiverComponent)) + return; + + component.Active = false; + component.Charge = 0; + UpdateState(new Entity<PowerChargeComponent, ApcPowerReceiverComponent>(uid, component, powerReceiverComponent)); + } + + private void OnAfterUiOpened(EntityUid uid, PowerChargeComponent component, AfterActivatableUIOpenEvent args) + { + if (!TryComp<ApcPowerReceiverComponent>(uid, out var apcPowerReceiver)) + return; + + UpdateUI((uid, component, apcPowerReceiver), component.ChargeRate); + } + + private void OnSwitchGenerator(EntityUid uid, PowerChargeComponent component, SwitchChargingMachineMessage args) + { + SetSwitchedOn(uid, component, args.On, user: args.Actor); + } + + private void OnUIOpenAttempt(EntityUid uid, PowerChargeComponent component, ActivatableUIOpenAttemptEvent args) + { + if (!component.Intact) + args.Cancel(); + } + + private void OnComponentShutdown(EntityUid uid, PowerChargeComponent component, ComponentShutdown args) + { + if (!component.Active) + return; + + component.Active = false; + + var eventArgs = new ChargedMachineDeactivatedEvent(); + RaiseLocalEvent(uid, ref eventArgs); + } + + private void OnMapInit(Entity<PowerChargeComponent> ent, ref MapInitEvent args) + { + ApcPowerReceiverComponent? powerReceiver = null; + if (!Resolve(ent, ref powerReceiver, false)) + return; + + UpdatePowerState(ent, powerReceiver); + UpdateState((ent, ent.Comp, powerReceiver)); + } + + private void SetSwitchedOn(EntityUid uid, PowerChargeComponent component, bool on, + ApcPowerReceiverComponent? powerReceiver = null, EntityUid? user = null) + { + if (!Resolve(uid, ref powerReceiver)) + return; + + if (user is { } ) + _adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} set ${ToPrettyString(uid):target} to {(on ? "on" : "off")}"); + + component.SwitchedOn = on; + UpdatePowerState(component, powerReceiver); + component.NeedUIUpdate = true; + } + + private static void UpdatePowerState(PowerChargeComponent component, ApcPowerReceiverComponent powerReceiver) + { + powerReceiver.Load = component.SwitchedOn ? component.ActivePowerUse : component.IdlePowerUse; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator<PowerChargeComponent, ApcPowerReceiverComponent>(); + while (query.MoveNext(out var uid, out var chargingMachine, out var powerReceiver)) + { + var ent = (uid, gravGen: chargingMachine, powerReceiver); + if (!chargingMachine.Intact) + continue; + + // Calculate charge rate based on power state and such. + // Negative charge rate means discharging. + float chargeRate; + if (chargingMachine.SwitchedOn) + { + if (powerReceiver.Powered) + { + chargeRate = chargingMachine.ChargeRate; + } + else + { + // Scale discharge rate such that if we're at 25% active power we discharge at 75% rate. + var receiving = powerReceiver.PowerReceived; + var mainSystemPower = Math.Max(0, receiving - chargingMachine.IdlePowerUse); + var ratio = 1 - mainSystemPower / (chargingMachine.ActivePowerUse - chargingMachine.IdlePowerUse); + chargeRate = -(ratio * chargingMachine.ChargeRate); + } + } + else + { + chargeRate = -chargingMachine.ChargeRate; + } + + var active = chargingMachine.Active; + var lastCharge = chargingMachine.Charge; + chargingMachine.Charge = Math.Clamp(chargingMachine.Charge + frameTime * chargeRate, 0, chargingMachine.MaxCharge); + if (chargeRate > 0) + { + // Charging. + if (MathHelper.CloseTo(chargingMachine.Charge, chargingMachine.MaxCharge) && !chargingMachine.Active) + { + chargingMachine.Active = true; + } + } + else + { + // Discharging + if (MathHelper.CloseTo(chargingMachine.Charge, 0) && chargingMachine.Active) + { + chargingMachine.Active = false; + } + } + + var updateUI = chargingMachine.NeedUIUpdate; + if (!MathHelper.CloseTo(lastCharge, chargingMachine.Charge)) + { + UpdateState(ent); + updateUI = true; + } + + if (updateUI) + UpdateUI(ent, chargeRate); + + if (active == chargingMachine.Active) + continue; + + if (chargingMachine.Active) + { + var eventArgs = new ChargedMachineActivatedEvent(); + RaiseLocalEvent(uid, ref eventArgs); + } + else + { + var eventArgs = new ChargedMachineDeactivatedEvent(); + RaiseLocalEvent(uid, ref eventArgs); + } + } + } + + private void UpdateUI(Entity<PowerChargeComponent, ApcPowerReceiverComponent> ent, float chargeRate) + { + var (_, component, powerReceiver) = ent; + if (!_uiSystem.IsUiOpen(ent.Owner, component.UiKey)) + return; + + var chargeTarget = chargeRate < 0 ? 0 : component.MaxCharge; + short chargeEta; + var atTarget = false; + if (MathHelper.CloseTo(component.Charge, chargeTarget)) + { + chargeEta = short.MinValue; // N/A + atTarget = true; + } + else + { + var diff = chargeTarget - component.Charge; + chargeEta = (short) Math.Abs(diff / chargeRate); + } + + var status = chargeRate switch + { + > 0 when atTarget => PowerChargePowerStatus.FullyCharged, + < 0 when atTarget => PowerChargePowerStatus.Off, + > 0 => PowerChargePowerStatus.Charging, + < 0 => PowerChargePowerStatus.Discharging, + _ => throw new ArgumentOutOfRangeException() + }; + + var state = new PowerChargeState( + component.SwitchedOn, + (byte) (component.Charge * 255), + status, + (short) Math.Round(powerReceiver.PowerReceived), + (short) Math.Round(powerReceiver.Load), + chargeEta + ); + + _uiSystem.SetUiState( + ent.Owner, + component.UiKey, + state); + + component.NeedUIUpdate = false; + } + + private void UpdateState(Entity<PowerChargeComponent, ApcPowerReceiverComponent> ent) + { + var (uid, machine, powerReceiver) = ent; + var appearance = EntityManager.GetComponentOrNull<AppearanceComponent>(uid); + _appearance.SetData(uid, PowerChargeVisuals.Charge, machine.Charge, appearance); + _appearance.SetData(uid, PowerChargeVisuals.Active, machine.Active); + + + if (!machine.Intact) + { + MakeBroken((uid, machine), appearance); + } + else if (powerReceiver.PowerReceived < machine.IdlePowerUse) + { + MakeUnpowered((uid, machine), appearance); + } + else if (!machine.SwitchedOn) + { + MakeOff((uid, machine), appearance); + } + else + { + MakeOn((uid, machine), appearance); + } + } + + private void MakeBroken(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance) + { + _ambientSoundSystem.SetAmbience(ent, false); + + _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.Broken, appearance); + } + + private void MakeUnpowered(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance) + { + _ambientSoundSystem.SetAmbience(ent, false); + + _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.Unpowered, appearance); + } + + private void MakeOff(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance) + { + _ambientSoundSystem.SetAmbience(ent, false); + + _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.Off, appearance); + } + + private void MakeOn(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance) + { + _ambientSoundSystem.SetAmbience(ent, true); + + _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.On, appearance); + } +} + +[ByRefEvent] public record struct ChargedMachineActivatedEvent; +[ByRefEvent] public record struct ChargedMachineDeactivatedEvent; diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 2d80c810e2e..8dcb6240a56 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Power.Pow3r; using Content.Shared.CCVar; using Content.Shared.Power; +using Content.Shared.Power.Components; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Configuration; @@ -22,6 +23,7 @@ public sealed class PowerNetSystem : EntitySystem [Dependency] private readonly PowerNetConnectorSystem _powerNetConnector = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IParallelManager _parMan = default!; + [Dependency] private readonly PowerReceiverSystem _powerReceiver = default!; private readonly PowerState _powerState = new(); private readonly HashSet<PowerNet> _powerNetReconnectQueue = new(); @@ -313,19 +315,27 @@ private void UpdateApcPowerReceiver() var enumerator = AllEntityQuery<ApcPowerReceiverComponent>(); while (enumerator.MoveNext(out var uid, out var apcReceiver)) { - var powered = apcReceiver.Powered; - if (powered == apcReceiver.PoweredLastUpdate) + var powered = !apcReceiver.PowerDisabled + && (!apcReceiver.NeedsPower + || MathHelper.CloseToPercent(apcReceiver.NetworkLoad.ReceivingPower, + apcReceiver.Load)); + + // If new value is the same as the old, then exit + if (!apcReceiver.Recalculate && apcReceiver.Powered == powered) continue; - if (metaQuery.GetComponent(uid).EntityPaused) + var metadata = metaQuery.Comp(uid); + if (metadata.EntityPaused) continue; - apcReceiver.PoweredLastUpdate = powered; - var ev = new PowerChangedEvent(apcReceiver.Powered, apcReceiver.NetworkLoad.ReceivingPower); + apcReceiver.Recalculate = false; + apcReceiver.Powered = powered; + Dirty(uid, apcReceiver, metadata); + var ev = new PowerChangedEvent(powered, apcReceiver.NetworkLoad.ReceivingPower); RaiseLocalEvent(uid, ref ev); - if (appearanceQuery.TryGetComponent(uid, out var appearance)) + if (appearanceQuery.TryComp(uid, out var appearance)) _appearance.SetData(uid, PowerDeviceVisuals.Powered, powered, appearance); } } diff --git a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs index 9ba30813dd5..925ba63f916 100644 --- a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Power.Components; @@ -7,10 +8,13 @@ using Content.Shared.Examine; using Content.Shared.Hands.Components; using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Verbs; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Audio; +using Robust.Shared.GameStates; using Robust.Shared.Utility; using Content.Shared.Emp; using Content.Shared.Interaction; @@ -18,7 +22,7 @@ namespace Content.Server.Power.EntitySystems { - public sealed class PowerReceiverSystem : EntitySystem + public sealed class PowerReceiverSystem : SharedPowerReceiverSystem { [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminManager _adminManager = default!; @@ -43,13 +47,17 @@ public override void Initialize() SubscribeLocalEvent<ApcPowerReceiverComponent, GetVerbsEvent<Verb>>(OnGetVerbs); SubscribeLocalEvent<PowerSwitchComponent, GetVerbsEvent<AlternativeVerb>>(AddSwitchPowerVerb); - SubscribeLocalEvent<ApcPowerReceiverComponent, EmpPulseEvent>(OnEmpPulse); - SubscribeLocalEvent<ApcPowerReceiverComponent, EmpDisabledRemoved>(OnEmpEnd); + SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentGetState>(OnGetState); _recQuery = GetEntityQuery<ApcPowerReceiverComponent>(); _provQuery = GetEntityQuery<ApcPowerProviderComponent>(); } + private void OnExamined(Entity<ApcPowerReceiverComponent> ent, ref ExaminedEvent args) + { + args.PushMarkup(GetExamineText(ent.Comp.Powered)); + } + private void OnGetVerbs(EntityUid uid, ApcPowerReceiverComponent component, GetVerbsEvent<Verb> args) { if (!_adminManager.HasAdminFlag(args.User, AdminFlags.Admin)) @@ -65,17 +73,6 @@ private void OnGetVerbs(EntityUid uid, ApcPowerReceiverComponent component, GetV }); } - ///<summary> - ///Adds some markup to the examine text of whatever object is using this component to tell you if it's powered or not, even if it doesn't have an icon state to do this for you. - ///</summary> - private void OnExamined(EntityUid uid, ApcPowerReceiverComponent component, ExaminedEvent args) - { - args.PushMarkup(Loc.GetString("power-receiver-component-on-examine-main", - ("stateText", Loc.GetString( component.Powered - ? "power-receiver-component-on-examine-powered" - : "power-receiver-component-on-examine-unpowered")))); - } - private void OnProviderShutdown(EntityUid uid, ApcPowerProviderComponent component, ComponentShutdown args) { foreach (var receiver in component.LinkedReceivers) @@ -142,7 +139,7 @@ private void AddSwitchPowerVerb(EntityUid uid, PowerSwitchComponent component, G { Act = () => { - TryTogglePower(uid, user: args.User); + TogglePower(uid, user: args.User); }, Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/Spare/poweronoff.svg.192dpi.png")), Text = Loc.GetString("power-switch-component-toggle-verb"), @@ -151,14 +148,18 @@ private void AddSwitchPowerVerb(EntityUid uid, PowerSwitchComponent component, G args.Verbs.Add(verb); } + private void OnGetState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentGetState args) + { + args.State = new ApcPowerReceiverComponentState + { + Powered = component.Powered + }; + } + private void ProviderChanged(Entity<ApcPowerReceiverComponent> receiver) { var comp = receiver.Comp; comp.NetworkLoad.LinkedNetwork = default; - var ev = new PowerChangedEvent(comp.Powered, comp.NetworkLoad.ReceivingPower); - - RaiseLocalEvent(receiver, ref ev); - _appearance.SetData(receiver, PowerDeviceVisuals.Powered, comp.Powered); } /// <summary> @@ -166,12 +167,10 @@ private void ProviderChanged(Entity<ApcPowerReceiverComponent> receiver) /// Otherwise, it returns 'true' because if something doesn't take power /// it's effectively always powered. /// </summary> + /// <returns>True when entity has no ApcPowerReceiverComponent or is Powered. False when not.</returns> public bool IsPowered(EntityUid uid, ApcPowerReceiverComponent? receiver = null) { - if (!_recQuery.Resolve(uid, ref receiver, false)) - return true; - - return receiver.Powered; + return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered; } /// <summary> @@ -204,35 +203,21 @@ public bool TogglePower(EntityUid uid, bool playSwitchSound = true, ApcPowerRece return !receiver.PowerDisabled; // i.e. PowerEnabled } - public bool TryTogglePower(EntityUid uid, bool playSwitchSound = true, ApcPowerReceiverComponent? receiver = null, EntityUid? user = null) - { - if (HasComp<EmpDisabledComponent>(uid)) - return false; - - return TogglePower(uid, playSwitchSound, receiver, user); - } - public void SetLoad(ApcPowerReceiverComponent comp, float load) { comp.Load = load; } - private void OnEmpPulse(EntityUid uid, ApcPowerReceiverComponent component, ref EmpPulseEvent args) + public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component) { - if (!component.PowerDisabled) - { - args.Affected = true; - args.Disabled = true; - TogglePower(uid, false); - } - } + if (component != null) + return true; - private void OnEmpEnd(EntityUid uid, ApcPowerReceiverComponent component, ref EmpDisabledRemoved args) - { - if (component.PowerDisabled) - { - TogglePower(uid, false); - } + if (!TryComp(entity, out ApcPowerReceiverComponent? receiver)) + return false; + + component = receiver; + return true; } } } diff --git a/Content.Server/Power/Generation/Teg/TegSystem.cs b/Content.Server/Power/Generation/Teg/TegSystem.cs index 540bd6c4832..dd09467efe0 100644 --- a/Content.Server/Power/Generation/Teg/TegSystem.cs +++ b/Content.Server/Power/Generation/Teg/TegSystem.cs @@ -10,6 +10,8 @@ using Content.Shared.Atmos; using Content.Shared.DeviceNetwork; using Content.Shared.Examine; +using Content.Shared.Power; +using Content.Shared.Power.Components; using Content.Shared.Power.Generation.Teg; using Content.Shared.Rounding; using Robust.Server.GameObjects; diff --git a/Content.Server/Power/Generator/GasPowerReceiverSystem.cs b/Content.Server/Power/Generator/GasPowerReceiverSystem.cs index 5f79906c995..5a1bd31a15c 100644 --- a/Content.Server/Power/Generator/GasPowerReceiverSystem.cs +++ b/Content.Server/Power/Generator/GasPowerReceiverSystem.cs @@ -5,6 +5,8 @@ using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Shared.Atmos; +using Content.Shared.Power; +using Content.Shared.Power.Components; namespace Content.Server.Power.Generator; diff --git a/Content.Server/Power/Generator/PortableGeneratorSystem.cs b/Content.Server/Power/Generator/PortableGeneratorSystem.cs index e2996a54d71..33033daa054 100644 --- a/Content.Server/Power/Generator/PortableGeneratorSystem.cs +++ b/Content.Server/Power/Generator/PortableGeneratorSystem.cs @@ -73,7 +73,9 @@ private void StartGenerator(EntityUid uid, PortableGeneratorComponent component, _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.StartTime, new GeneratorStartedEvent(), uid, uid) { - BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, RequireCanInteract = true, + BreakOnDamage = true, + BreakOnMove = true, + RequireCanInteract = true, NeedHand = true }); } diff --git a/Content.Server/Power/Systems/BatteryDrinkerSystem.cs b/Content.Server/Power/Systems/BatteryDrinkerSystem.cs index e42783c4d8d..4f2ddf0bbad 100644 --- a/Content.Server/Power/Systems/BatteryDrinkerSystem.cs +++ b/Content.Server/Power/Systems/BatteryDrinkerSystem.cs @@ -72,8 +72,7 @@ private void DrinkBattery(EntityUid target, EntityUid user, BatteryDrinkerCompon var args = new DoAfterArgs(EntityManager, user, doAfterTime, new BatteryDrinkerDoAfterEvent(), user, target) // TODO: Make this doafter loop, once we merge Upstream. { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, Broadcast = false, DistanceThreshold = 1.35f, RequireCanInteract = true, diff --git a/Content.Server/PowerCell/PowerCellSystem.Draw.cs b/Content.Server/PowerCell/PowerCellSystem.Draw.cs index 4155a4f6bec..9ebd677f473 100644 --- a/Content.Server/PowerCell/PowerCellSystem.Draw.cs +++ b/Content.Server/PowerCell/PowerCellSystem.Draw.cs @@ -1,4 +1,5 @@ using Content.Server.Power.Components; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.PowerCell; using Content.Shared.PowerCell.Components; @@ -10,22 +11,20 @@ public sealed partial class PowerCellSystem * Handles PowerCellDraw */ - private static readonly TimeSpan Delay = TimeSpan.FromSeconds(1); - public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator<PowerCellDrawComponent, PowerCellSlotComponent>(); + var query = EntityQueryEnumerator<PowerCellDrawComponent, PowerCellSlotComponent, ItemToggleComponent>(); - while (query.MoveNext(out var uid, out var comp, out var slot)) + while (query.MoveNext(out var uid, out var comp, out var slot, out var toggle)) { - if (!comp.Drawing) + if (!comp.Enabled || !toggle.Activated) continue; if (Timing.CurTime < comp.NextUpdateTime) continue; - comp.NextUpdateTime += Delay; + comp.NextUpdateTime += comp.Delay; if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot)) continue; @@ -33,7 +32,8 @@ public override void Update(float frameTime) if (_battery.TryUseCharge(batteryEnt.Value, comp.DrawRate, battery)) continue; - comp.Drawing = false; + Toggle.TryDeactivate((uid, toggle)); + var ev = new PowerCellSlotEmptyEvent(); RaiseLocalEvent(uid, ref ev); } @@ -42,26 +42,9 @@ public override void Update(float frameTime) private void OnDrawChargeChanged(EntityUid uid, PowerCellDrawComponent component, ref ChargeChangedEvent args) { // Update the bools for client prediction. - bool canDraw; - bool canUse; - - if (component.UseRate > 0f) - { - canUse = args.Charge > component.UseRate; - } - else - { - canUse = true; - } + var canUse = component.UseRate <= 0f || args.Charge > component.UseRate; - if (component.DrawRate > 0f) - { - canDraw = args.Charge > 0f; - } - else - { - canDraw = true; - } + var canDraw = component.DrawRate <= 0f || args.Charge > 0f; if (canUse != component.CanUse || canDraw != component.CanDraw) { @@ -76,6 +59,9 @@ private void OnDrawCellChanged(EntityUid uid, PowerCellDrawComponent component, var canDraw = !args.Ejected && HasCharge(uid, float.MinValue); var canUse = !args.Ejected && HasActivatableCharge(uid, component); + if (!canDraw) + Toggle.TryDeactivate(uid); + if (canUse != component.CanUse || canDraw != component.CanDraw) { component.CanDraw = canDraw; diff --git a/Content.Server/PowerCell/PowerCellSystem.cs b/Content.Server/PowerCell/PowerCellSystem.cs index f45a01b2e1b..f7b4cf02491 100644 --- a/Content.Server/PowerCell/PowerCellSystem.cs +++ b/Content.Server/PowerCell/PowerCellSystem.cs @@ -39,8 +39,8 @@ public override void Initialize() SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged); SubscribeLocalEvent<PowerCellDrawComponent, PowerCellChangedEvent>(OnDrawCellChanged); - // funny SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined); + // funny SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(OnSlotMicrowaved); } @@ -64,11 +64,11 @@ private void OnChargeChanged(EntityUid uid, PowerCellComponent component, ref Ch } var frac = args.Charge / args.MaxCharge; - var level = (byte) ContentHelpers.RoundToNearestLevels(frac, 1, PowerCellComponent.PowerCellVisualsLevels); + var level = (byte)ContentHelpers.RoundToNearestLevels(frac, 1, PowerCellComponent.PowerCellVisualsLevels); _sharedAppearanceSystem.SetData(uid, PowerCellVisuals.ChargeLevel, level); // If this power cell is inside a cell-slot, inform that entity that the power has changed (for updating visuals n such). - if (_containerSystem.TryGetContainingContainer(uid, out var container) + if (_containerSystem.TryGetContainingContainer((uid, null, null), out var container) && TryComp(container.Owner, out PowerCellSlotComponent? slot) && _itemSlotsSystem.TryGetSlot(container.Owner, slot.CellSlotId, out var itemSlot)) { diff --git a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs index cdb53bb4be7..d9b864e9816 100644 --- a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs @@ -12,6 +12,7 @@ public interface IServerPreferencesManager void Init(); Task LoadData(ICommonSession session, CancellationToken cancel); + void SanitizeData(ICommonSession session); void FinishLoad(ICommonSession session); void OnClientDisconnected(ICommonSession session); diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index df7bac72e90..929586d735e 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -9,6 +9,7 @@ using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -27,6 +28,7 @@ public sealed class ServerPreferencesManager : IServerPreferencesManager, IPostI [Dependency] private readonly IDependencyCollection _dependencies = default!; [Dependency] private readonly ILogManager _log = default!; [Dependency] private readonly UserDbDataManager _userDb = default!; + [Dependency] private readonly IPrototypeManager _protos = default!; // Cache player prefs on the server so we don't need as much async hell related to them. private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs = @@ -52,7 +54,7 @@ private async void HandleSelectCharacterMessage(MsgSelectCharacter message) if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded) { - Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded."); + _sawmill.Error($"User {userId} tried to modify preferences before they loaded."); return; } @@ -221,6 +223,17 @@ public void FinishLoad(ICommonSession session) _netManager.ServerSendMessage(msg, session.Channel); } + public void SanitizeData(ICommonSession session) + { + // This is a separate step from the actual database load. + // Sanitizing preferences requires play time info due to loadouts. + // And play time info is loaded concurrently from the DB with preferences. + var data = _cachedPlayerPrefs[session.UserId]; + DebugTools.Assert(data.Prefs != null); + data.Prefs = SanitizePreferences(session, data.Prefs, _dependencies); + _sawmill.Debug("here"); + } + public void OnClientDisconnected(ICommonSession session) { _cachedPlayerPrefs.Remove(session.UserId); diff --git a/Content.Server/Procedural/DungeonJob.PostGen.cs b/Content.Server/Procedural/DungeonJob.PostGen.cs index b326bcc378f..cb9e64f04e2 100644 --- a/Content.Server/Procedural/DungeonJob.PostGen.cs +++ b/Content.Server/Procedural/DungeonJob.PostGen.cs @@ -13,6 +13,7 @@ using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -24,13 +25,15 @@ public sealed partial class DungeonJob * Run after the main dungeon generation */ + private static readonly ProtoId<TagPrototype> WallTag = "Wall"; + private bool HasWall(MapGridComponent grid, Vector2i tile) { var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile); while (anchored.MoveNext(out var uid)) { - if (_tagQuery.TryGetComponent(uid, out var tagComp) && tagComp.Tags.Contains("Wall")) + if (_tag.HasTag(uid.Value, WallTag)) return true; } diff --git a/Content.Server/Procedural/DungeonJob.cs b/Content.Server/Procedural/DungeonJob.cs index 8fecf1c9e8e..bf2822ff423 100644 --- a/Content.Server/Procedural/DungeonJob.cs +++ b/Content.Server/Procedural/DungeonJob.cs @@ -28,10 +28,10 @@ public sealed partial class DungeonJob : Job<Dungeon> private readonly DecalSystem _decals; private readonly DungeonSystem _dungeon; private readonly EntityLookupSystem _lookup; + private readonly TagSystem _tag; private readonly TileSystem _tile; private readonly SharedMapSystem _maps; private readonly SharedTransformSystem _transform; - private EntityQuery<TagComponent> _tagQuery; private readonly DungeonConfigPrototype _gen; private readonly int _seed; @@ -53,6 +53,7 @@ public DungeonJob( DecalSystem decals, DungeonSystem dungeon, EntityLookupSystem lookup, + TagSystem tag, TileSystem tile, SharedTransformSystem transform, DungeonConfigPrototype gen, @@ -72,10 +73,10 @@ public DungeonJob( _decals = decals; _dungeon = dungeon; _lookup = lookup; + _tag = tag; _tile = tile; _maps = _entManager.System<SharedMapSystem>(); _transform = transform; - _tagQuery = _entManager.GetEntityQuery<TagComponent>(); _gen = gen; _grid = grid; diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index 069508bcbbb..36009896a2c 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Procedural; +using Content.Shared.Tag; using Robust.Server.GameObjects; using Robust.Shared.Configuration; using Robust.Shared.Console; @@ -31,6 +32,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem [Dependency] private readonly AnchorableSystem _anchorable = default!; [Dependency] private readonly DecalSystem _decals = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly TileSystem _tile = default!; [Dependency] private readonly MapLoaderSystem _loader = default!; [Dependency] private readonly SharedMapSystem _maps = default!; @@ -199,6 +201,7 @@ public void GenerateDungeon(DungeonConfigPrototype gen, _decals, this, _lookup, + _tag, _tile, _transform, gen, @@ -231,6 +234,7 @@ public async Task<Dungeon> GenerateDungeonAsync( _decals, this, _lookup, + _tag, _tile, _transform, gen, diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs index c5ec2d76ad5..436221103d2 100644 --- a/Content.Server/Projectiles/ProjectileSystem.cs +++ b/Content.Server/Projectiles/ProjectileSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Damage.Events; using Content.Server.Administration.Logs; using Content.Server.Effects; using Content.Server.Weapons.Ranged.Systems; @@ -8,7 +7,6 @@ using Content.Shared.Projectiles; using Robust.Shared.Physics.Events; using Robust.Shared.Player; -using Robust.Shared.Utility; namespace Content.Server.Projectiles; @@ -24,14 +22,13 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent<ProjectileComponent, StartCollideEvent>(OnStartCollide); - SubscribeLocalEvent<EmbeddableProjectileComponent, DamageExamineEvent>(OnDamageExamine); } private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref StartCollideEvent args) { // This is so entities that shouldn't get a collision are ignored. if (args.OurFixtureId != ProjectileFixture || !args.OtherFixture.Hard - || component.DamagedEntity || component is { Weapon: null, OnlyCollideWhenShot: true }) + || component.DamagedEntity || component is { Weapon: null, OnlyCollideWhenShot: true, }) return; var target = args.OtherEntity; @@ -48,18 +45,16 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St RaiseLocalEvent(uid, ref ev); var otherName = ToPrettyString(target); - var direction = args.OurBody.LinearVelocity.Normalized(); var modifiedDamage = _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, origin: component.Shooter); var deleted = Deleted(target); if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter)) { - if (modifiedDamage.Any() && !deleted) - { - _color.RaiseEffect(Color.Red, new List<EntityUid> { target }, Filter.Pvs(target, entityManager: EntityManager)); - } + if (modifiedDamage.AnyPositive() && !deleted) + _color.RaiseEffect(Color.Red, [ target, ], Filter.Pvs(target, entityManager: EntityManager)); - _adminLogger.Add(LogType.BulletHit, + _adminLogger.Add( + LogType.BulletHit, HasComp<ActorComponent>(target) ? LogImpact.Extreme : LogImpact.High, $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {modifiedDamage.GetTotal():damage} damage"); } @@ -67,7 +62,9 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St if (!deleted) { _guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound); - _sharedCameraRecoil.KickCamera(target, direction); + + if (!args.OurBody.LinearVelocity.IsLengthZero()) + _sharedCameraRecoil.KickCamera(target, args.OurBody.LinearVelocity.Normalized()); } component.DamagedEntity = true; @@ -75,26 +72,7 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St if (component.DeleteOnCollide) QueueDel(uid); - if (component.ImpactEffect != null && TryComp<TransformComponent>(uid, out var xform)) - { + if (component.ImpactEffect != null && TryComp(uid, out TransformComponent? xform)) RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager)); - } - } - - private void OnDamageExamine(EntityUid uid, EmbeddableProjectileComponent component, ref DamageExamineEvent args) - { - if (!component.EmbedOnThrow) - return; - - if (!args.Message.IsEmpty) - args.Message.PushNewline(); - - var isHarmful = TryComp<EmbedPassiveDamageComponent>(uid, out var passiveDamage) && passiveDamage.Damage.Any(); - var loc = isHarmful - ? "damage-examine-embeddable-harmful" - : "damage-examine-embeddable"; - - var staminaCostMarkup = FormattedMessage.FromMarkupOrThrow(Loc.GetString(loc)); - args.Message.AddMessage(staminaCostMarkup); } } diff --git a/Content.Server/Psionics/Glimmer/GlimmerReactiveSystem.cs b/Content.Server/Psionics/Glimmer/GlimmerReactiveSystem.cs index f828874aacb..85aea76bd65 100644 --- a/Content.Server/Psionics/Glimmer/GlimmerReactiveSystem.cs +++ b/Content.Server/Psionics/Glimmer/GlimmerReactiveSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Destructible; using Content.Shared.Construction.Components; using Content.Shared.Mind.Components; +using Content.Shared.Power; using Content.Shared.Weapons.Melee.Components; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; diff --git a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs index 8694147dc0e..c0c11b083ce 100644 --- a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs +++ b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Anomaly.Components; +using Content.Shared.Power; using Content.Shared.Psionics.Glimmer; namespace Content.Server.Psionics.Glimmer diff --git a/Content.Server/Psionics/Invisibility/PsionicInvisibilitySystem.cs b/Content.Server/Psionics/Invisibility/PsionicInvisibilitySystem.cs index b9aac6ccee2..d6d20149be6 100644 --- a/Content.Server/Psionics/Invisibility/PsionicInvisibilitySystem.cs +++ b/Content.Server/Psionics/Invisibility/PsionicInvisibilitySystem.cs @@ -5,6 +5,8 @@ using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Server.GameObjects; +using Content.Shared.NPC.Systems; + namespace Content.Server.Psionics { diff --git a/Content.Server/Psionics/PsionicsSystem.cs b/Content.Server/Psionics/PsionicsSystem.cs index 20e55576721..a775b60bc90 100644 --- a/Content.Server/Psionics/PsionicsSystem.cs +++ b/Content.Server/Psionics/PsionicsSystem.cs @@ -23,6 +23,8 @@ using Content.Shared.Interaction.Events; using Timer = Robust.Shared.Timing.Timer; using Content.Shared.Alert; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Rounding; namespace Content.Server.Psionics; @@ -367,7 +369,7 @@ private void SetFamiliarTarget(EntityUid target, PsionicComponent component) if (!TryComp<NPCRetaliationComponent>(familiar, out var retaliationComponent)) continue; - _retaliationSystem.TryRetaliate(familiar, target, retaliationComponent); + _retaliationSystem.TryRetaliate((familiar, retaliationComponent), target); } } } diff --git a/Content.Server/Radiation/Components/RadiationProtectionComponent.cs b/Content.Server/Radiation/Components/RadiationProtectionComponent.cs new file mode 100644 index 00000000000..44b11af8347 --- /dev/null +++ b/Content.Server/Radiation/Components/RadiationProtectionComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Prototypes; +using Content.Shared.Damage.Prototypes; + +namespace Content.Server.Radiation.Components; + +/// <summary> +/// Exists for use as a status effect. +/// Adds the DamageProtectionBuffComponent to the entity and adds the specified DamageModifierSet to its list of modifiers. +/// </summary> +[RegisterComponent] +public sealed partial class RadiationProtectionComponent : Component +{ + /// <summary> + /// The radiation damage modifier for entities with this component. + /// </summary> + [DataField("modifier")] + public ProtoId<DamageModifierSetPrototype> RadiationProtectionModifierSetId = "PotassiumIodide"; +} diff --git a/Content.Server/Radiation/Systems/RadiationProtectionSystem.cs b/Content.Server/Radiation/Systems/RadiationProtectionSystem.cs new file mode 100644 index 00000000000..5222c31bfe6 --- /dev/null +++ b/Content.Server/Radiation/Systems/RadiationProtectionSystem.cs @@ -0,0 +1,38 @@ +using Content.Server.Radiation.Components; +using Content.Shared.Damage.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.Radiation.EntitySystems; + +public sealed class RadiationProtectionSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<RadiationProtectionComponent, ComponentInit>(OnInit); + SubscribeLocalEvent<RadiationProtectionComponent, ComponentShutdown>(OnShutdown); + } + + private void OnInit(EntityUid uid, RadiationProtectionComponent component, ComponentInit args) + { + if (!_prototypeManager.TryIndex(component.RadiationProtectionModifierSetId, out var modifier)) + return; + var buffComp = EnsureComp<DamageProtectionBuffComponent>(uid); + // add the damage modifier if it isn't in the dict yet + if (!buffComp.Modifiers.ContainsKey(component.RadiationProtectionModifierSetId)) + buffComp.Modifiers.Add(component.RadiationProtectionModifierSetId, modifier); + } + + private void OnShutdown(EntityUid uid, RadiationProtectionComponent component, ComponentShutdown args) + { + if (!TryComp<DamageProtectionBuffComponent>(uid, out var buffComp)) + return; + // remove the damage modifier from the dict + buffComp.Modifiers.Remove(component.RadiationProtectionModifierSetId); + // if the dict is empty now, remove the buff component + if (buffComp.Modifiers.Count == 0) + RemComp<DamageProtectionBuffComponent>(uid); + } +} diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index 02e46e6b11d..6fd5e46e10a 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -1,19 +1,18 @@ +using System.Linq; using Content.Server.Chat.Systems; using Content.Server.Interaction; -using Content.Server.Language; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Radio.Components; using Content.Server.Speech; using Content.Server.Speech.Components; -using Content.Shared.UserInterface; -using Content.Shared.Chat; using Content.Shared.Examine; using Content.Shared.Interaction; +using Content.Shared.Power; using Content.Shared.Radio; +using Content.Shared.Chat; using Content.Shared.Radio.Components; -using Robust.Server.GameObjects; using Robust.Shared.Prototypes; namespace Content.Server.Radio.EntitySystems; @@ -29,8 +28,6 @@ public sealed class RadioDeviceSystem : EntitySystem [Dependency] private readonly RadioSystem _radio = default!; [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; - [Dependency] private readonly LanguageSystem _language = default!; // Used to prevent a shitter from using a bunch of radios to spam chat. private HashSet<(string, EntityUid)> _recentlySent = new(); @@ -49,7 +46,7 @@ public override void Initialize() SubscribeLocalEvent<RadioSpeakerComponent, ActivateInWorldEvent>(OnActivateSpeaker); SubscribeLocalEvent<RadioSpeakerComponent, RadioReceiveEvent>(OnReceiveRadio); - SubscribeLocalEvent<IntercomComponent, BeforeActivatableUIOpenEvent>(OnBeforeIntercomUiOpen); + SubscribeLocalEvent<IntercomComponent, EncryptionChannelsChangedEvent>(OnIntercomEncryptionChannelsChanged); SubscribeLocalEvent<IntercomComponent, ToggleIntercomMicMessage>(OnToggleIntercomMic); SubscribeLocalEvent<IntercomComponent, ToggleIntercomSpeakerMessage>(OnToggleIntercomSpeaker); SubscribeLocalEvent<IntercomComponent, SelectIntercomChannelMessage>(OnSelectIntercomChannel); @@ -152,18 +149,18 @@ public void ToggleRadioSpeaker(EntityUid uid, EntityUid user, bool quiet = false SetSpeakerEnabled(uid, user, !component.Enabled, quiet, component); } - public void SetSpeakerEnabled(EntityUid uid, EntityUid user, bool enabled, bool quiet = false, RadioSpeakerComponent? component = null) + public void SetSpeakerEnabled(EntityUid uid, EntityUid? user, bool enabled, bool quiet = false, RadioSpeakerComponent? component = null) { if (!Resolve(uid, ref component)) return; component.Enabled = enabled; - if (!quiet) + if (!quiet && user != null) { var state = Loc.GetString(component.Enabled ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state"); var message = Loc.GetString("handheld-radio-component-on-use", ("radioState", state)); - _popup.PopupEntity(message, user, user); + _popup.PopupEntity(message, user.Value, user.Value); } _appearance.SetData(uid, RadioDeviceVisuals.Speaker, component.Enabled); @@ -212,65 +209,77 @@ private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref if (uid == args.RadioSource) return; - var nameEv = new TransformSpeakerSpeechEvent(args.MessageSource, Name(args.MessageSource)); + var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource)); RaiseLocalEvent(args.MessageSource, nameEv); - var name = Loc.GetString("speech-name-relay", ("speaker", Name(uid)), - ("originalName", nameEv.VoiceName ?? Name(args.MessageSource))); + var name = Loc.GetString("speech-name-relay", + ("speaker", Name(uid)), + ("originalName", nameEv.VoiceName)); // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios - var message = args.OriginalChatMsg.Message; // The chat system will handle the rest and re-obfuscate if needed. - _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); + _chat.TrySendInGameICMessage(uid, args.OriginalChatMsg.Message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false); } - private void OnBeforeIntercomUiOpen(EntityUid uid, IntercomComponent component, BeforeActivatableUIOpenEvent args) + private void OnIntercomEncryptionChannelsChanged(Entity<IntercomComponent> ent, ref EncryptionChannelsChangedEvent args) { - UpdateIntercomUi(uid, component); + ent.Comp.SupportedChannels = args.Component.Channels.Select(p => new ProtoId<RadioChannelPrototype>(p)).ToList(); + + var channel = args.Component.DefaultChannel; + if (ent.Comp.CurrentChannel != null && ent.Comp.SupportedChannels.Contains(ent.Comp.CurrentChannel.Value)) + channel = ent.Comp.CurrentChannel; + + SetIntercomChannel(ent, channel); } - private void OnToggleIntercomMic(EntityUid uid, IntercomComponent component, ToggleIntercomMicMessage args) + private void OnToggleIntercomMic(Entity<IntercomComponent> ent, ref ToggleIntercomMicMessage args) { - if (component.RequiresPower && !this.IsPowered(uid, EntityManager)) + if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager)) return; - SetMicrophoneEnabled(uid, args.Actor, args.Enabled, true); - UpdateIntercomUi(uid, component); + SetMicrophoneEnabled(ent, args.Actor, args.Enabled, true); + ent.Comp.MicrophoneEnabled = args.Enabled; + Dirty(ent); } - private void OnToggleIntercomSpeaker(EntityUid uid, IntercomComponent component, ToggleIntercomSpeakerMessage args) + private void OnToggleIntercomSpeaker(Entity<IntercomComponent> ent, ref ToggleIntercomSpeakerMessage args) { - if (component.RequiresPower && !this.IsPowered(uid, EntityManager)) + if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager)) return; - SetSpeakerEnabled(uid, args.Actor, args.Enabled, true); - UpdateIntercomUi(uid, component); + SetSpeakerEnabled(ent, args.Actor, args.Enabled, true); + ent.Comp.SpeakerEnabled = args.Enabled; + Dirty(ent); } - private void OnSelectIntercomChannel(EntityUid uid, IntercomComponent component, SelectIntercomChannelMessage args) + private void OnSelectIntercomChannel(Entity<IntercomComponent> ent, ref SelectIntercomChannelMessage args) { - if (component.RequiresPower && !this.IsPowered(uid, EntityManager)) + if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager)) return; - if (!_protoMan.TryIndex<RadioChannelPrototype>(args.Channel, out _) || !component.SupportedChannels.Contains(args.Channel)) + if (!_protoMan.HasIndex<RadioChannelPrototype>(args.Channel) || !ent.Comp.SupportedChannels.Contains(args.Channel)) return; - if (TryComp<RadioMicrophoneComponent>(uid, out var mic)) - mic.BroadcastChannel = args.Channel; - if (TryComp<RadioSpeakerComponent>(uid, out var speaker)) - speaker.Channels = new(){ args.Channel }; - UpdateIntercomUi(uid, component); + SetIntercomChannel(ent, args.Channel); } - private void UpdateIntercomUi(EntityUid uid, IntercomComponent component) + private void SetIntercomChannel(Entity<IntercomComponent> ent, ProtoId<RadioChannelPrototype>? channel) { - var micComp = CompOrNull<RadioMicrophoneComponent>(uid); - var speakerComp = CompOrNull<RadioSpeakerComponent>(uid); - - var micEnabled = micComp?.Enabled ?? false; - var speakerEnabled = speakerComp?.Enabled ?? false; - var availableChannels = component.SupportedChannels; - var selectedChannel = micComp?.BroadcastChannel ?? SharedChatSystem.CommonChannel; - var state = new IntercomBoundUIState(micEnabled, speakerEnabled, availableChannels, selectedChannel); - _ui.SetUiState(uid, IntercomUiKey.Key, state); + ent.Comp.CurrentChannel = channel; + + if (channel == null) + { + SetSpeakerEnabled(ent, null, false); + SetMicrophoneEnabled(ent, null, false); + ent.Comp.MicrophoneEnabled = false; + ent.Comp.SpeakerEnabled = false; + Dirty(ent); + return; + } + + if (TryComp<RadioMicrophoneComponent>(ent, out var mic)) + mic.BroadcastChannel = channel; + if (TryComp<RadioSpeakerComponent>(ent, out var speaker)) + speaker.Channels = new(){ channel }; + Dirty(ent); } } diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 66dadc85b5d..4433ac6d545 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -6,9 +6,11 @@ using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Language; +using Content.Shared.Language.Systems; using Content.Shared.Radio; using Content.Shared.Radio.Components; using Content.Shared.Speech; +using Microsoft.CodeAnalysis.Host; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; @@ -35,11 +37,15 @@ public sealed class RadioSystem : EntitySystem // set used to prevent radio feedback loops. private readonly HashSet<string> _messages = new(); + private EntityQuery<TelecomExemptComponent> _exemptQuery; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive); SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntitySpokeEvent>(OnIntrinsicSpeak); + + _exemptQuery = GetEntityQuery<TelecomExemptComponent>(); } private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntitySpokeEvent args) @@ -58,6 +64,7 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c // Einstein-Engines - languages mechanic var listener = component.Owner; var msg = args.OriginalChatMsg; + if (listener != null && !_language.CanUnderstand(listener, args.Language.ID)) msg = args.LanguageObfuscatedChatMsg; @@ -68,16 +75,19 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c /// <summary> /// Send radio message to all active radio listeners /// </summary> - public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) - { + public void SendRadioMessage( + EntityUid messageSource, + string message, + ProtoId<RadioChannelPrototype> channel, + EntityUid radioSource, + LanguagePrototype? language = null, + bool escapeMarkup = true + ) => SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language); - } /// <summary> /// Send radio message to all active radio listeners /// </summary> - /// <param name="messageSource">Entity that spoke the message</param> - /// <param name="radioSource">Entity that picked up the message and will send it, e.g. headset</param> public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) { if (language == null) @@ -90,9 +100,9 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann if (!_messages.Add(message)) return; - var evt = new TransformSpeakerSpeechEvent(messageSource, Name(messageSource)); + var evt = new TransformSpeakerNameEvent(messageSource, Name(messageSource)); RaiseLocalEvent(messageSource, evt); - var name = evt.VoiceName ?? Name(messageSource); + var name = evt.VoiceName; name = FormattedMessage.EscapeText(name); diff --git a/Content.Server/Radio/IntrinsicRadioKeySystem.cs b/Content.Server/Radio/IntrinsicRadioKeySystem.cs index eeea64c2f70..01770e6e59b 100644 --- a/Content.Server/Radio/IntrinsicRadioKeySystem.cs +++ b/Content.Server/Radio/IntrinsicRadioKeySystem.cs @@ -24,9 +24,9 @@ private void OnReceiverChannelsChanged(EntityUid uid, ActiveRadioComponent compo UpdateChannels(uid, args.Component, ref component.Channels); } - private void UpdateChannels(EntityUid _, EncryptionKeyHolderComponent keyHolderComp, ref HashSet<string> channels) + private void UpdateChannels(EntityUid _, EncryptionKeyHolderComponent component, ref HashSet<string> channels) { channels.Clear(); - channels.UnionWith(keyHolderComp.Channels); + channels.UnionWith(component.Channels); } } diff --git a/Content.Server/Research/Oracle/OracleComponent.cs b/Content.Server/Research/Oracle/OracleComponent.cs index 6196ce95060..e9f8b272889 100644 --- a/Content.Server/Research/Oracle/OracleComponent.cs +++ b/Content.Server/Research/Oracle/OracleComponent.cs @@ -12,7 +12,7 @@ public sealed partial class OracleComponent : Component public ProtoId<WeightedRandomPrototype> DemandTypes; [DataField] - public List<ProtoId<EntityPrototype>> BlacklistedDemands = new(); + public List<EntProtoId> BlacklistedDemands = new(); [DataField(required: true)] public List<ProtoId<WeightedRandomEntityPrototype>> RewardEntities; diff --git a/Content.Server/Research/Oracle/OracleSystem.cs b/Content.Server/Research/Oracle/OracleSystem.cs index 15f0a474470..511a18f3795 100644 --- a/Content.Server/Research/Oracle/OracleSystem.cs +++ b/Content.Server/Research/Oracle/OracleSystem.cs @@ -271,6 +271,10 @@ private bool GetRandomTechProto(Entity<OracleComponent> oracle, [NotNullWhen(tru } proto = _random.Pick(techs); + + if (proto == null) + return false; + return true; } @@ -291,7 +295,7 @@ private bool GetRandomPlantProto(Entity<OracleComponent> oracle, [NotNullWhen(tr return true; } - private bool IsDemandValid(Entity<OracleComponent> oracle, ProtoId<EntityPrototype>? id) + private bool IsDemandValid(Entity<OracleComponent> oracle, EntProtoId? id) { if (id == null || oracle.Comp.BlacklistedDemands.Contains(id.Value)) return false; diff --git a/Content.Server/Research/Systems/ResearchSystem.Console.cs b/Content.Server/Research/Systems/ResearchSystem.Console.cs index 5358ddefcdf..127b80a631e 100644 --- a/Content.Server/Research/Systems/ResearchSystem.Console.cs +++ b/Content.Server/Research/Systems/ResearchSystem.Console.cs @@ -2,6 +2,8 @@ using Content.Server.Research.Components; using Content.Shared.UserInterface; using Content.Shared.Access.Components; +using Content.Shared.Emag.Components; +using Content.Shared.IdentityManagement; using Content.Shared.Research.Components; using Content.Shared.Research.Prototypes; @@ -37,10 +39,20 @@ private void OnConsoleUnlock(EntityUid uid, ResearchConsoleComponent component, if (!UnlockTechnology(uid, args.Id, act)) return; - var message = Loc.GetString("research-console-unlock-technology-radio-broadcast", - ("technology", Loc.GetString(technologyPrototype.Name)), - ("amount", technologyPrototype.Cost)); - _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); + if (!HasComp<EmaggedComponent>(uid)) + { + var getIdentityEvent = new TryGetIdentityShortInfoEvent(uid, act); + RaiseLocalEvent(getIdentityEvent); + + var message = Loc.GetString( + "research-console-unlock-technology-radio-broadcast", + ("technology", Loc.GetString(technologyPrototype.Name)), + ("amount", technologyPrototype.Cost), + ("approver", getIdentityEvent.Title ?? string.Empty) + ); + _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); + } + SyncClientWithServer(uid); UpdateConsoleInterface(uid, component); } @@ -87,4 +99,5 @@ private void OnConsoleDatabaseModified(EntityUid uid, ResearchConsoleComponent c { UpdateConsoleInterface(uid, component); } + } diff --git a/Content.Server/Research/Systems/ResearchSystem.Technology.cs b/Content.Server/Research/Systems/ResearchSystem.Technology.cs index 9bd71cf7c6e..7578d316c5e 100644 --- a/Content.Server/Research/Systems/ResearchSystem.Technology.cs +++ b/Content.Server/Research/Systems/ResearchSystem.Technology.cs @@ -131,25 +131,6 @@ public void AddTechnology(EntityUid uid, TechnologyPrototype technology, Technol RaiseLocalEvent(uid, ref ev); } - /// <summary> - /// Adds a lathe recipe to the specified technology database - /// without checking if it can be unlocked. - /// </summary> - public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.UnlockedRecipes.Contains(recipe)) - return; - - component.UnlockedRecipes.Add(recipe); - Dirty(uid, component); - - var ev = new TechnologyDatabaseModifiedEvent(); - RaiseLocalEvent(uid, ref ev); - } - /// <summary> /// Returns whether a technology can be unlocked on this database, /// taking parent technologies into account. diff --git a/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs b/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs deleted file mode 100644 index eb5a0623a08..00000000000 --- a/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Shared.Random; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Research.TechnologyDisk.Components; - -[RegisterComponent] -public sealed partial class TechnologyDiskComponent : Component -{ - /// <summary> - /// The recipe that will be added. If null, one will be randomly generated - /// </summary> - [DataField("recipes")] - public List<string>? Recipes; - - /// <summary> - /// A weighted random prototype for how rare each tier should be. - /// </summary> - [DataField("tierWeightPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))] - public string TierWeightPrototype = "TechDiskTierWeights"; -} diff --git a/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs b/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs deleted file mode 100644 index 176b2b68bc9..00000000000 --- a/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Linq; -using Content.Server.Popups; -using Content.Server.Research.Systems; -using Content.Server.Research.TechnologyDisk.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; -using Content.Shared.Random; -using Content.Shared.Random.Helpers; -using Content.Shared.Research.Components; -using Content.Shared.Research.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Research.TechnologyDisk.Systems; - -public sealed class TechnologyDiskSystem : EntitySystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly ResearchSystem _research = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - /// <inheritdoc/> - public override void Initialize() - { - SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract); - SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine); - SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit); - } - - private void OnAfterInteract(EntityUid uid, TechnologyDiskComponent component, AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target is not { } target) - return; - - if (!HasComp<ResearchServerComponent>(target) || !TryComp<TechnologyDatabaseComponent>(target, out var database)) - return; - - if (component.Recipes != null) - { - foreach (var recipe in component.Recipes) - { - _research.AddLatheRecipe(target, recipe, database); - } - } - _popup.PopupEntity(Loc.GetString("tech-disk-inserted"), target, args.User); - QueueDel(uid); - args.Handled = true; - } - - private void OnExamine(EntityUid uid, TechnologyDiskComponent component, ExaminedEvent args) - { - var message = Loc.GetString("tech-disk-examine-none"); - if (component.Recipes != null && component.Recipes.Any()) - { - var prototype = _prototype.Index<LatheRecipePrototype>(component.Recipes[0]); - var resultPrototype = _prototype.Index<EntityPrototype>(prototype.Result); - message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name)); - - if (component.Recipes.Count > 1) //idk how to do this well. sue me. - message += " " + Loc.GetString("tech-disk-examine-more"); - } - args.PushMarkup(message); - } - - private void OnMapInit(EntityUid uid, TechnologyDiskComponent component, MapInitEvent args) - { - if (component.Recipes != null) - return; - - var weightedRandom = _prototype.Index<WeightedRandomPrototype>(component.TierWeightPrototype); - var tier = int.Parse(weightedRandom.Pick(_random)); - - //get a list of every distinct recipe in all the technologies. - var techs = new List<ProtoId<LatheRecipePrototype>>(); - foreach (var tech in _prototype.EnumeratePrototypes<TechnologyPrototype>()) - { - if (tech.Tier != tier) - continue; - - techs.AddRange(tech.RecipeUnlocks); - } - techs = techs.Distinct().ToList(); - - if (!techs.Any()) - return; - - //pick one - component.Recipes = new(); - component.Recipes.Add(_random.Pick(techs)); - } -} diff --git a/Content.Server/Resist/EscapeInventorySystem.cs b/Content.Server/Resist/EscapeInventorySystem.cs index 2e060b1b42d..63e8173117c 100644 --- a/Content.Server/Resist/EscapeInventorySystem.cs +++ b/Content.Server/Resist/EscapeInventorySystem.cs @@ -56,7 +56,7 @@ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent componen if (!args.HasDirectionalMovement) return; - if (!_containerSystem.TryGetContainingContainer(uid, out var container) || !_actionBlockerSystem.CanInteract(uid, container.Owner)) + if (!_containerSystem.TryGetContainingContainer((uid, null, null), out var container) || !_actionBlockerSystem.CanInteract(uid, container.Owner)) return; // Make sure there's nothing stopped the removal (like being glued) @@ -86,8 +86,7 @@ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent componen var doAfterEventArgs = new DoAfterArgs(EntityManager, user, component.BaseResistTime * multiplier, new EscapeInventoryEvent(), user, target: container) { - BreakOnTargetMove = false, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = false }; diff --git a/Content.Server/Resist/ResistLockerSystem.cs b/Content.Server/Resist/ResistLockerSystem.cs index 7a17a2eba19..2ab277d0f1a 100644 --- a/Content.Server/Resist/ResistLockerSystem.cs +++ b/Content.Server/Resist/ResistLockerSystem.cs @@ -47,8 +47,7 @@ private void AttemptResist(EntityUid user, EntityUid target, EntityStorageCompon var doAfterEventArgs = new DoAfterArgs(EntityManager, user, resistLockerComponent.ResistTime, new ResistLockerDoAfterEvent(), target, target: target) { - BreakOnTargetMove = false, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = false //No hands 'cause we be kickin' }; diff --git a/Content.Server/Respawn/SpecialRespawnSystem.cs b/Content.Server/Respawn/SpecialRespawnSystem.cs index 2463bcd7402..6c7bb5c9234 100644 --- a/Content.Server/Respawn/SpecialRespawnSystem.cs +++ b/Content.Server/Respawn/SpecialRespawnSystem.cs @@ -95,7 +95,7 @@ private void OnTermination(EntityUid uid, SpecialRespawnComponent component, ref { var xform = Transform(entityGridUid.Value); var pos = xform.Coordinates; - var mapPos = xform.MapPosition; + var mapPos = _transform.GetMapCoordinates(entityGridUid.Value, xform: xform); var circle = new Circle(mapPos.Position, 2); var found = false; diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index 0a72e48071e..c889d59f155 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -91,7 +91,7 @@ private void BeginSoulSearchDoAfter(EntityUid uid, EntityUid target, RevenantCom { var searchDoAfter = new DoAfterArgs(EntityManager, uid, revenant.SoulSearchDuration, new SoulEvent(), uid, target: target) { - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, DistanceThreshold = 2 }; @@ -152,7 +152,7 @@ private void BeginHarvestDoAfter(EntityUid uid, EntityUid target, RevenantCompon var doAfter = new DoAfterArgs(EntityManager, uid, revenant.HarvestDebuffs.X, new HarvestEvent(), uid, target: target) { DistanceThreshold = 2, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, RequireCanInteract = false, // stuns itself }; @@ -248,7 +248,7 @@ private void OnDefileAction(EntityUid uid, RevenantComponent component, Revenant foreach (var ent in lookup) { //break windows - if (tags.HasComponent(ent) && _tag.HasAnyTag(ent, "Window")) + if (tags.HasComponent(ent) && _tag.HasTag(ent, "Window")) { //hardcoded damage specifiers til i die. var dspec = new DamageSpecifier(); diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index 595de100e86..fa4f6db240c 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -17,6 +17,7 @@ using Content.Shared.Revenant; using Content.Shared.Revenant.Components; using Content.Shared.StatusEffect; +using Content.Shared.Store.Components; using Content.Shared.Stunnable; using Content.Shared.Tag; using Robust.Server.GameObjects; diff --git a/Content.Server/Revolutionary/RevolutionarySystem.cs b/Content.Server/Revolutionary/RevolutionarySystem.cs new file mode 100644 index 00000000000..597c4896b90 --- /dev/null +++ b/Content.Server/Revolutionary/RevolutionarySystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Revolutionary; + +namespace Content.Server.Revolutionary; + +public sealed class RevolutionarySystem : SharedRevolutionarySystem; diff --git a/Content.Server/Roles/RoleCodeword/RoleCodewordSystem.cs b/Content.Server/Roles/RoleCodeword/RoleCodewordSystem.cs new file mode 100644 index 00000000000..a93d0f773b9 --- /dev/null +++ b/Content.Server/Roles/RoleCodeword/RoleCodewordSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Roles.RoleCodeword; + +namespace Content.Server.Roles.RoleCodeword; + +public sealed class RoleCodewordSystem : SharedRoleCodewordSystem +{ + +} diff --git a/Content.Server/Shadowkin/EtherealSystem.cs b/Content.Server/Shadowkin/EtherealSystem.cs index 4ff0654dfb2..46ad9b8b3e4 100644 --- a/Content.Server/Shadowkin/EtherealSystem.cs +++ b/Content.Server/Shadowkin/EtherealSystem.cs @@ -7,12 +7,13 @@ using Content.Shared.Stealth; using Content.Shared.Stealth.Components; using Content.Server.Body.Components; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; using System.Linq; using Content.Shared.Abilities.Psionics; using Robust.Shared.Random; using Content.Server.Light.Components; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; + namespace Content.Server.Shadowkin; diff --git a/Content.Server/Shuttles/Components/StationAnchorComponent.cs b/Content.Server/Shuttles/Components/StationAnchorComponent.cs new file mode 100644 index 00000000000..c971ed921cb --- /dev/null +++ b/Content.Server/Shuttles/Components/StationAnchorComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Shuttles.Systems; + +namespace Content.Server.Shuttles.Components; + +[RegisterComponent] +[Access(typeof(StationAnchorSystem))] +public sealed partial class StationAnchorComponent : Component +{ + [DataField("switchedOn")] + public bool SwitchedOn { get; set; } = true; +} diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs index c214bb015c9..e2bb5887aa8 100644 --- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs +++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs @@ -124,6 +124,9 @@ private void ConsoleFTL(Entity<ShuttleConsoleComponent> ent, EntityCoordinates t if (!TryComp(shuttleUid, out ShuttleComponent? shuttleComp)) return; + if (shuttleComp.Enabled == false) + return; + // Check shuttle can even FTL if (!_shuttle.CanFTL(shuttleUid.Value, out var reason)) { diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs index 5550201202f..264323dabeb 100644 --- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; using Content.Server.Station.Systems; +using Content.Shared._NF.Shuttles.Events; // Frontier using Content.Shared.ActionBlocker; using Content.Shared.Alert; using Content.Shared.Popups; @@ -12,6 +13,7 @@ using Content.Shared.Shuttles.Systems; using Content.Shared.Tag; using Content.Shared.Movement.Systems; +using Content.Shared.Power; using Content.Shared.Shuttles.UI.MapObjects; using Content.Shared.Timing; using Robust.Server.GameObjects; @@ -256,7 +258,7 @@ private void UpdateState(EntityUid consoleUid, ref DockingInterfaceState? dockSt } else { - navState = new NavInterfaceState(0f, null, null, new Dictionary<NetEntity, List<DockingPortState>>()); + navState = new NavInterfaceState(0f, null, null, new Dictionary<NetEntity, List<DockingPortState>>(), InertiaDampeningMode.Dampen); // Frontier: inertia dampening); mapState = new ShuttleMapInterfaceState( FTLState.Invalid, default, @@ -371,7 +373,7 @@ public void ClearPilots(ShuttleConsoleComponent component) public NavInterfaceState GetNavState(Entity<RadarConsoleComponent?, TransformComponent?> entity, Dictionary<NetEntity, List<DockingPortState>> docks) { if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2)) - return new NavInterfaceState(SharedRadarConsoleSystem.DefaultMaxRange, null, null, docks); + return new NavInterfaceState(SharedRadarConsoleSystem.DefaultMaxRange, null, null, docks, Shared._NF.Shuttles.Events.InertiaDampeningMode.Dampen); // Frontier: add inertia dampening return GetNavState( entity, @@ -387,13 +389,14 @@ public NavInterfaceState GetNavState( Angle angle) { if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2)) - return new NavInterfaceState(SharedRadarConsoleSystem.DefaultMaxRange, GetNetCoordinates(coordinates), angle, docks); + return new NavInterfaceState(SharedRadarConsoleSystem.DefaultMaxRange, GetNetCoordinates(coordinates), angle, docks, InertiaDampeningMode.Dampen); // Frontier: add inertial dampening return new NavInterfaceState( entity.Comp1.MaxRange, GetNetCoordinates(coordinates), angle, - docks); + docks, + _shuttle.NfGetInertiaDampeningMode(entity)); // Frontier: inertia dampening } /// <summary> diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index fe8ec7baeed..74fabc06bb3 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -218,18 +218,22 @@ public void RemoveFTLDestination(EntityUid uid) /// </summary> public bool CanFTL(EntityUid shuttleUid, [NotNullWhen(false)] out string? reason) { + // Currently in FTL already if (HasComp<FTLComponent>(shuttleUid)) { reason = Loc.GetString("shuttle-console-in-ftl"); return false; } - if (FTLMassLimit > 0 && - TryComp(shuttleUid, out PhysicsComponent? shuttlePhysics) && - shuttlePhysics.Mass > FTLMassLimit) + if (TryComp<PhysicsComponent>(shuttleUid, out var shuttlePhysics)) { - reason = Loc.GetString("shuttle-console-mass"); - return false; + + // Too large to FTL + if (FTLMassLimit > 0 && shuttlePhysics.Mass > FTLMassLimit) + { + reason = Loc.GetString("shuttle-console-mass"); + return false; + } } if (HasComp<PreventPilotComponent>(shuttleUid)) diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs index a10d0d56144..95f0bf93d43 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs @@ -69,6 +69,9 @@ public override void Initialize() SubscribeLocalEvent<GridInitializeEvent>(OnGridInit); SubscribeLocalEvent<FixturesComponent, GridFixtureChangeEvent>(OnGridFixtureChange); + + NfInitialize(); // Frontier Initialization for the ShuttleSystem + } public override void Update(float frameTime) diff --git a/Content.Server/Shuttles/Systems/StationAnchorSystem.cs b/Content.Server/Shuttles/Systems/StationAnchorSystem.cs new file mode 100644 index 00000000000..9ac1ed4d171 --- /dev/null +++ b/Content.Server/Shuttles/Systems/StationAnchorSystem.cs @@ -0,0 +1,86 @@ +using Content.Server.Popups; +using Content.Server.Power.EntitySystems; +using Content.Server.Shuttles.Components; +using Content.Shared.Construction.Components; +using Content.Shared.Popups; + +namespace Content.Server.Shuttles.Systems; + +public sealed class StationAnchorSystem : EntitySystem +{ + [Dependency] private readonly ShuttleSystem _shuttleSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<StationAnchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt); + SubscribeLocalEvent<StationAnchorComponent, AnchorStateChangedEvent>(OnAnchorStationChange); + + SubscribeLocalEvent<StationAnchorComponent, ChargedMachineActivatedEvent>(OnActivated); + SubscribeLocalEvent<StationAnchorComponent, ChargedMachineDeactivatedEvent>(OnDeactivated); + + SubscribeLocalEvent<StationAnchorComponent, MapInitEvent>(OnMapInit); + } + + private void OnMapInit(Entity<StationAnchorComponent> ent, ref MapInitEvent args) + { + if (!ent.Comp.SwitchedOn) + return; + + SetStatus(ent, true); + } + + private void OnActivated(Entity<StationAnchorComponent> ent, ref ChargedMachineActivatedEvent args) + { + SetStatus(ent, true); + } + + private void OnDeactivated(Entity<StationAnchorComponent> ent, ref ChargedMachineDeactivatedEvent args) + { + SetStatus(ent, false); + } + + /// <summary> + /// Prevent unanchoring when anchor is active + /// </summary> + private void OnUnanchorAttempt(Entity<StationAnchorComponent> ent, ref UnanchorAttemptEvent args) + { + if (!ent.Comp.SwitchedOn) + return; + + _popupSystem.PopupEntity( + Loc.GetString("station-anchor-unanchoring-failed"), + ent, + args.User, + PopupType.Medium); + + args.Cancel(); + } + + private void OnAnchorStationChange(Entity<StationAnchorComponent> ent, ref AnchorStateChangedEvent args) + { + if (!args.Anchored) + SetStatus(ent, false); + } + + private void SetStatus(Entity<StationAnchorComponent> ent, bool enabled, ShuttleComponent? shuttleComponent = default) + { + var transform = Transform(ent); + var grid = transform.GridUid; + if (!grid.HasValue || !transform.Anchored && enabled || !Resolve(grid.Value, ref shuttleComponent)) + return; + + if (enabled) + { + _shuttleSystem.Disable(grid.Value); + } + else + { + _shuttleSystem.Enable(grid.Value); + } + + shuttleComponent.Enabled = !enabled; + ent.Comp.SwitchedOn = enabled; + } +} diff --git a/Content.Server/Shuttles/Systems/ThrusterSystem.cs b/Content.Server/Shuttles/Systems/ThrusterSystem.cs index 2454856a701..b1426802135 100644 --- a/Content.Server/Shuttles/Systems/ThrusterSystem.cs +++ b/Content.Server/Shuttles/Systems/ThrusterSystem.cs @@ -19,6 +19,8 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Content.Shared.Localizations; +using Content.Shared.Power; namespace Content.Server.Shuttles.Systems; diff --git a/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs b/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs index b9d26b59f7c..e43a690881b 100644 --- a/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs +++ b/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs @@ -57,7 +57,7 @@ private bool TryHealBlindness(EntityUid uid, EntityUid user, EntityUid target, f new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), uid, target: target, used: uid) { NeedHand = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnWeightlessMove = false, }; diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs index 623b6ba9787..0feecad4d08 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs @@ -30,7 +30,7 @@ private void OnModuleGotInserted(EntityUid uid, BorgModuleComponent component, E if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp) || args.Container != chassisComp.ModuleContainer || - !chassisComp.Activated) + !Toggle.IsActivated(chassis)) return; if (!_powerCell.HasDrawCharge(uid)) @@ -144,6 +144,7 @@ public void SelectModule(EntityUid chassis, var ev = new BorgModuleSelectedEvent(chassis); RaiseLocalEvent(moduleUid, ref ev); chassisComp.SelectedModule = moduleUid; + Dirty(chassis, chassisComp); } /// <summary> @@ -163,6 +164,7 @@ public void UnselectModule(EntityUid chassis, BorgChassisComponent? chassisComp var ev = new BorgModuleUnselectedEvent(chassis); RaiseLocalEvent(chassisComp.SelectedModule.Value, ref ev); chassisComp.SelectedModule = null; + Dirty(chassis, chassisComp); } private void OnItemModuleSelected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleSelectedEvent args) diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs index 1c10cbe667e..781f847be35 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs @@ -1,5 +1,6 @@ using Content.Shared.DeviceNetwork; using Content.Shared.Emag.Components; +using Content.Shared.Movement.Components; using Content.Shared.Popups; using Content.Shared.Robotics; using Content.Shared.Silicons.Borgs.Components; @@ -26,6 +27,9 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>(); while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta)) { + if (comp.NextDisable is {} nextDisable && now >= nextDisable) + DoDisable((uid, comp, chassis, meta)); + if (now < comp.NextBroadcast) continue; @@ -33,13 +37,16 @@ public override void Update(float frameTime) if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) charge = battery.CurrentCharge / battery.MaxCharge; + var hasBrain = chassis.BrainEntity != null && !comp.FakeDisabled; + var canDisable = comp.NextDisable == null && !comp.FakeDisabling; var data = new CyborgControlData( comp.Sprite, comp.Name, meta.EntityName, charge, chassis.ModuleCount, - chassis.BrainEntity != null); + hasBrain, + canDisable); var payload = new NetworkPayload() { @@ -52,6 +59,24 @@ public override void Update(float frameTime) } } + private void DoDisable(Entity<BorgTransponderComponent, BorgChassisComponent, MetaDataComponent> ent) + { + ent.Comp1.NextDisable = null; + if (ent.Comp1.FakeDisabling) + { + ent.Comp1.FakeDisabled = true; + ent.Comp1.FakeDisabling = false; + return; + } + + if (ent.Comp2.BrainEntity is not {} brain) + return; + + var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent, ent.Comp3))); + Popup.PopupEntity(message, ent); + _container.Remove(brain, ent.Comp2.BrainContainer); + } + private void OnPacketReceived(Entity<BorgTransponderComponent> ent, ref DeviceNetworkPacketEvent args) { var payload = args.Data; @@ -61,28 +86,28 @@ private void OnPacketReceived(Entity<BorgTransponderComponent> ent, ref DeviceNe if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND) Disable(ent); else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND) - Destroy(ent.Owner); + Destroy(ent); } private void Disable(Entity<BorgTransponderComponent, BorgChassisComponent?> ent) { - if (!Resolve(ent, ref ent.Comp2) || ent.Comp2.BrainEntity is not {} brain) + if (!Resolve(ent, ref ent.Comp2) || ent.Comp2.BrainEntity == null || ent.Comp1.NextDisable != null) return; - // this won't exactly be stealthy but if you are malf its better than actually disabling you + // update ui immediately + ent.Comp1.NextBroadcast = _timing.CurTime; + + // pretend the borg is being disabled forever now if (CheckEmagged(ent, "disabled")) - return; + ent.Comp1.FakeDisabling = true; + else + Popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent); - var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent))); - Popup.PopupEntity(message, ent); - _container.Remove(brain, ent.Comp2.BrainContainer); + ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay; } - private void Destroy(Entity<ExplosiveComponent?> ent) + private void Destroy(Entity<BorgTransponderComponent> ent) { - if (!Resolve(ent, ref ent.Comp)) - return; - // this is stealthy until someone realises you havent exploded if (CheckEmagged(ent, "destroyed")) { @@ -91,7 +116,12 @@ private void Destroy(Entity<ExplosiveComponent?> ent) return; } - _explosion.TriggerExplosive(ent, ent.Comp, delete: false); + var message = Loc.GetString(ent.Comp.DestroyingPopup, ("name", Name(ent))); + Popup.PopupEntity(message, ent); + _trigger.StartTimer(ent.Owner, user: null); + + // prevent a shitter borg running into people + RemComp<InputMoverComponent>(ent); } private bool CheckEmagged(EntityUid uid, string name) diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index b6e5adbe2d4..3ffd1baf5de 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -11,6 +11,7 @@ using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs; @@ -44,8 +45,8 @@ public sealed partial class BorgSystem : SharedBorgSystem [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; - [Dependency] private readonly ExplosionSystem _explosion = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly TriggerSystem _trigger = default!; [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedMindSystem _mind = default!; @@ -73,8 +74,8 @@ public override void Initialize() SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged); SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); - SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt); SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC); + SubscribeLocalEvent<BorgChassisComponent, ItemToggledEvent>(OnToggled); SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded); SubscribeLocalEvent<BorgBrainComponent, PointAttemptEvent>(OnBrainPointAttempt); @@ -175,11 +176,11 @@ private void OnMobStateChanged(EntityUid uid, BorgChassisComponent component, Mo if (args.NewMobState == MobState.Alive) { if (_mind.TryGetMind(uid, out _, out _)) - _powerCell.SetPowerCellDrawEnabled(uid, true); + _powerCell.SetDrawEnabled(uid, true); } else { - _powerCell.SetPowerCellDrawEnabled(uid, false); + _powerCell.SetDrawEnabled(uid, false); } } @@ -187,24 +188,10 @@ private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, P { UpdateBatteryAlert((uid, component)); - if (!TryComp<PowerCellDrawComponent>(uid, out var draw)) - return; - - // if we eject the battery or run out of charge, then disable - if (args.Ejected || !_powerCell.HasDrawCharge(uid)) - { - DisableBorgAbilities(uid, component); - return; - } - // if we aren't drawing and suddenly get enough power to draw again, reeanble. - if (_powerCell.HasDrawCharge(uid, draw)) + if (_powerCell.HasDrawCharge(uid)) { - // only reenable the powerdraw if a player has the role. - if (!draw.Drawing && _mind.TryGetMind(uid, out _, out _) && _mobState.IsAlive(uid)) - _powerCell.SetPowerCellDrawEnabled(uid, true); - - EnableBorgAbilities(uid, component); + Toggle.TryActivate(uid); } UpdateUI(uid, component); @@ -212,20 +199,29 @@ private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, P private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args) { - DisableBorgAbilities(uid, component); + Toggle.TryDeactivate(uid); UpdateUI(uid, component); } - - private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args) + private void OnGetDeadIC(EntityUid uid, BorgChassisComponent component, ref GetCharactedDeadIcEvent args) { - // borgs can't view their own ui - if (args.User == uid) - args.Cancel(); + args.Dead = true; } - private void OnGetDeadIC(EntityUid uid, BorgChassisComponent component, ref GetCharactedDeadIcEvent args) + private void OnToggled(Entity<BorgChassisComponent> ent, ref ItemToggledEvent args) { - args.Dead = true; + var (uid, comp) = ent; + if (args.Activated) + InstallAllModules(uid, comp); + else + DisableAllModules(uid, comp); + + // only enable the powerdraw if there is a player in the chassis + var drawing = _mind.TryGetMind(uid, out _, out _) && _mobState.IsAlive(ent); + _powerCell.SetDrawEnabled(uid, drawing); + + UpdateUI(uid, comp); + + _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); } private void OnBrainMindAdded(EntityUid uid, BorgBrainComponent component, MindAddedMessage args) @@ -280,44 +276,14 @@ private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotC _alerts.ShowAlert(ent, ent.Comp.BatteryAlert, chargePercent); } - /// <summary> - /// Activates the borg, enabling all of its modules. - /// </summary> - public void EnableBorgAbilities(EntityUid uid, BorgChassisComponent component, PowerCellDrawComponent? powerCell = null) - { - if (component.Activated) - return; - - component.Activated = true; - InstallAllModules(uid, component); - Dirty(uid, component); - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - - /// <summary> - /// Deactivates the borg, disabling all of its modules and decreasing its speed. - /// </summary> - public void DisableBorgAbilities(EntityUid uid, BorgChassisComponent component) - { - if (!component.Activated) - return; - - component.Activated = false; - DisableAllModules(uid, component); - Dirty(uid, component); - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - /// <summary> /// Activates a borg when a player occupies it /// </summary> public void BorgActivate(EntityUid uid, BorgChassisComponent component) { Popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(uid, EntityManager))), uid); - _powerCell.SetPowerCellDrawEnabled(uid, true); - _access.SetAccessEnabled(uid, true); + Toggle.TryActivate(uid); _appearance.SetData(uid, BorgVisuals.HasPlayer, true); - Dirty(uid, component); } /// <summary> @@ -326,10 +292,8 @@ public void BorgActivate(EntityUid uid, BorgChassisComponent component) public void BorgDeactivate(EntityUid uid, BorgChassisComponent component) { Popup.PopupEntity(Loc.GetString("borg-mind-removed", ("name", Identity.Name(uid, EntityManager))), uid); - _powerCell.SetPowerCellDrawEnabled(uid, false); - _access.SetAccessEnabled(uid, false); + Toggle.TryDeactivate(uid); _appearance.SetData(uid, BorgVisuals.HasPlayer, false); - Dirty(uid, component); } /// <summary> diff --git a/Content.Server/Silicons/Laws/SiliconLawEui.cs b/Content.Server/Silicons/Laws/SiliconLawEui.cs new file mode 100644 index 00000000000..1d3c57b7cb7 --- /dev/null +++ b/Content.Server/Silicons/Laws/SiliconLawEui.cs @@ -0,0 +1,70 @@ +using Content.Server.Administration.Managers; +using Content.Server.EUI; +using Content.Shared.Administration; +using Content.Shared.Eui; +using Content.Shared.Silicons.Laws; +using Content.Shared.Silicons.Laws.Components; + +namespace Content.Server.Silicons.Laws; + +public sealed class SiliconLawEui : BaseEui +{ + private readonly SiliconLawSystem _siliconLawSystem; + private readonly EntityManager _entityManager; + private readonly IAdminManager _adminManager; + + private List<SiliconLaw> _laws = new(); + private ISawmill _sawmill = default!; + private EntityUid _target; + + public SiliconLawEui(SiliconLawSystem siliconLawSystem, EntityManager entityManager, IAdminManager manager) + { + _siliconLawSystem = siliconLawSystem; + _adminManager = manager; + _entityManager = entityManager; + _sawmill = Logger.GetSawmill("silicon-law-eui"); + } + + public override EuiStateBase GetNewState() + { + return new SiliconLawsEuiState(_laws, _entityManager.GetNetEntity(_target)); + } + + public void UpdateLaws(SiliconLawBoundComponent? lawBoundComponent, EntityUid player) + { + if (!IsAllowed()) + return; + + var laws = _siliconLawSystem.GetLaws(player, lawBoundComponent); + _laws = laws.Laws; + _target = player; + StateDirty(); + } + + public override void HandleMessage(EuiMessageBase msg) + { + if (msg is not SiliconLawsSaveMessage message) + { + return; + } + + if (!IsAllowed()) + return; + + var player = _entityManager.GetEntity(message.Target); + + _siliconLawSystem.SetLaws(message.Laws, player); + } + + private bool IsAllowed() + { + var adminData = _adminManager.GetAdminData(Player); + if (adminData == null || !adminData.HasFlag(AdminFlags.Admin)) + { + _sawmill.Warning("Player {0} tried to open / use silicon law UI without permission.", Player.UserId); + return false; + } + + return true; + } +} diff --git a/Content.Server/Silicons/Laws/SiliconLawSystem.cs b/Content.Server/Silicons/Laws/SiliconLawSystem.cs index bc7a7b35c27..0c0f68c23f3 100644 --- a/Content.Server/Silicons/Laws/SiliconLawSystem.cs +++ b/Content.Server/Silicons/Laws/SiliconLawSystem.cs @@ -22,6 +22,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; +using Robust.Shared.Utility; namespace Content.Server.Silicons.Laws; @@ -278,6 +279,21 @@ public SiliconLawset GetLawset(string lawset) return laws; } + + /// <summary> + /// Set the laws of a silicon entity while notifying the player. + /// </summary> + public void SetLaws(List<SiliconLaw> newLaws, EntityUid target) + { + if (!TryComp<SiliconLawProviderComponent>(target, out var component)) + return; + + if (component.Lawset == null) + component.Lawset = new SiliconLawset(); + + component.Lawset.Laws = newLaws; + NotifyLawsChanged(target); + } } [ToolshedCommand, AdminCommand(AdminFlags.Admin)] diff --git a/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs b/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs index 561db76a470..9488cc24e4d 100644 --- a/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs +++ b/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs @@ -37,7 +37,7 @@ private void HandleFieldCollide(EntityUid uid, ContainmentFieldComponent compone var fieldDir = Transform(uid).WorldPosition; var playerDir = Transform(otherBody).WorldPosition; - _throwing.TryThrow(otherBody, playerDir-fieldDir, strength: component.ThrowForce); + _throwing.TryThrow(otherBody, playerDir-fieldDir, baseThrowSpeed: component.ThrowForce); } } diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index 652ca236e44..e728f4986bb 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Interaction; using Content.Shared.Lock; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Projectiles; using Content.Shared.Singularity.Components; using Content.Shared.Singularity.EntitySystems; diff --git a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs index c74a3c49d63..e57a59475f2 100644 --- a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs @@ -97,7 +97,7 @@ public void Update(EntityUid uid, EventHorizonComponent? eventHorizon = null, Tr return; // Handle singularities some admin smited into a locker. - if (_containerSystem.TryGetContainingContainer(uid, out var container, transform: xform) + if (_containerSystem.TryGetContainingContainer((uid, xform, null), out var container) && !AttemptConsumeEntity(uid, container.Owner, eventHorizon)) { // Locker is indestructible. Consume everything else in the locker instead of magically teleporting out. @@ -167,7 +167,7 @@ public void ConsumeEntitiesInRange(EntityUid uid, float range, TransformComponen var range2 = range * range; var xformQuery = EntityManager.GetEntityQuery<TransformComponent>(); var epicenter = _xformSystem.GetWorldPosition(xform, xformQuery); - foreach (var entity in _lookup.GetEntitiesInRange(xform.MapPosition, range, flags: LookupFlags.Uncontained)) + foreach (var entity in _lookup.GetEntitiesInRange(_xformSystem.GetMapCoordinates(uid, xform), range, flags: LookupFlags.Uncontained)) { if (entity == uid) continue; @@ -214,7 +214,7 @@ public void ConsumeEntitiesInContainer(EntityUid hungry, BaseContainer container if (_containerSystem.Insert(entity, target_container)) break; - _containerSystem.TryGetContainingContainer(target_container.Owner, out target_container); + _containerSystem.TryGetContainingContainer((target_container.Owner, null, null), out target_container); } // If we couldn't or there was no container to insert into just dump them to the map/grid. @@ -297,7 +297,7 @@ public void ConsumeTilesInRange(EntityUid uid, float range, TransformComponent? if (!Resolve(uid, ref xform) || !Resolve(uid, ref eventHorizon)) return; - var mapPos = xform.MapPosition; + var mapPos = _xformSystem.GetMapCoordinates(uid, xform: xform); var box = Box2.CenteredAround(mapPos.Position, new Vector2(range, range)); var circle = new Circle(mapPos.Position, range); var grids = new List<Entity<MapGridComponent>>(); @@ -470,7 +470,7 @@ private void OnContainerConsumed(EntityUid uid, ContainerManagerComponent comp, { var drop_container = args.Container; if (drop_container is null) - _containerSystem.TryGetContainingContainer(uid, out drop_container); + _containerSystem.TryGetContainingContainer((uid, null, null), out drop_container); foreach (var container in comp.GetAllContainers()) { diff --git a/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs index 779b2f59719..f53d658ebd7 100644 --- a/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs +++ b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs @@ -1,6 +1,6 @@ using System.Numerics; -using Content.Server.Atmos.Components; using Content.Server.Singularity.Components; +using Content.Shared.Atmos.Components; using Content.Shared.Ghost; using Content.Shared.Singularity.EntitySystems; using Robust.Shared.Map; diff --git a/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs b/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs index 9cc85060c6e..d2c2a8a1ca7 100644 --- a/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs +++ b/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs @@ -1,5 +1,7 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; +using Content.Shared.Power; +using Content.Shared.Power.Components; using Content.Shared.Sound; using Content.Shared.Sound.Components; diff --git a/Content.Server/Speech/Components/VoiceOverrideComponent.cs b/Content.Server/Speech/Components/VoiceOverrideComponent.cs index 70deba5ba55..349babc948c 100644 --- a/Content.Server/Speech/Components/VoiceOverrideComponent.cs +++ b/Content.Server/Speech/Components/VoiceOverrideComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Speech; +using Content.Shared.Speech; using Robust.Shared.Prototypes; namespace Content.Server.Speech.Components; diff --git a/Content.Server/Speech/EntitySystems/VoiceOverrideSystem.cs b/Content.Server/Speech/EntitySystems/VoiceOverrideSystem.cs index 97510966f55..daaad099d65 100644 --- a/Content.Server/Speech/EntitySystems/VoiceOverrideSystem.cs +++ b/Content.Server/Speech/EntitySystems/VoiceOverrideSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chat; +using Content.Shared.Chat; using Content.Server.Speech.Components; namespace Content.Server.Speech.EntitySystems; @@ -8,10 +8,10 @@ public sealed partial class VoiceOverrideSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<VoiceOverrideComponent, TransformSpeakerSpeechEvent>(OnTransformSpeakerName); + SubscribeLocalEvent<VoiceOverrideComponent, TransformSpeakerNameEvent>(OnTransformSpeakerName); } - private void OnTransformSpeakerName(Entity<VoiceOverrideComponent> entity, ref TransformSpeakerSpeechEvent args) + private void OnTransformSpeakerName(Entity<VoiceOverrideComponent> entity, ref TransformSpeakerNameEvent args) { if (!entity.Comp.Enabled) return; diff --git a/Content.Server/SprayPainter/SprayPainterSystem.cs b/Content.Server/SprayPainter/SprayPainterSystem.cs index e49c49c1da0..a8024e2d77b 100644 --- a/Content.Server/SprayPainter/SprayPainterSystem.cs +++ b/Content.Server/SprayPainter/SprayPainterSystem.cs @@ -55,8 +55,7 @@ private void OnPipeInteract(Entity<AtmosPipeColorComponent> ent, ref InteractUsi var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.PipeSprayTime, new SprayPainterPipeDoAfterEvent(color), args.Used, target: ent, used: args.Used) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, CancelDuplicate = true, // multiple pipes can be sprayed at once just not the same one diff --git a/Content.Server/Spreader/SpreaderSystem.cs b/Content.Server/Spreader/SpreaderSystem.cs index fe14d86aa1d..7de8a43d354 100644 --- a/Content.Server/Spreader/SpreaderSystem.cs +++ b/Content.Server/Spreader/SpreaderSystem.cs @@ -21,6 +21,7 @@ public sealed class SpreaderSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly TagSystem _tag = default!; /// <summary> /// Cached maximum number of updates per spreader prototype. This is applied per-grid. @@ -37,8 +38,7 @@ public sealed class SpreaderSystem : EntitySystem public const float SpreadCooldownSeconds = 1; - [ValidatePrototypeId<TagPrototype>] - private const string IgnoredTag = "SpreaderIgnore"; + private static readonly ProtoId<TagPrototype> IgnoredTag = "SpreaderIgnore"; /// <inheritdoc/> public override void Initialize() @@ -189,7 +189,6 @@ public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId<EdgeSpr var airtightQuery = GetEntityQuery<AirtightComponent>(); var dockQuery = GetEntityQuery<DockingComponent>(); var xformQuery = GetEntityQuery<TransformComponent>(); - var tagQuery = GetEntityQuery<TagComponent>(); var blockedAtmosDirs = AtmosDirection.Invalid; // Due to docking ports they may not necessarily be opposite directions. @@ -212,7 +211,7 @@ public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId<EdgeSpr // If we're on a blocked tile work out which directions we can go. if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked || - tagQuery.TryGetComponent(ent, out var tags) && tags.Tags.Contains(IgnoredTag)) + _tag.HasTag(ent.Value, IgnoredTag)) { continue; } @@ -250,8 +249,7 @@ public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId<EdgeSpr while (directionEnumerator.MoveNext(out var ent)) { - if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked || - tagQuery.TryGetComponent(ent, out var tags) && tags.Tags.Contains(IgnoredTag)) + if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked || _tag.HasTag(ent.Value, IgnoredTag)) { continue; } diff --git a/Content.Server/Station/Commands/JobsCommand.cs b/Content.Server/Station/Commands/JobsCommand.cs index 1d023c4a844..1e39140e840 100644 --- a/Content.Server/Station/Commands/JobsCommand.cs +++ b/Content.Server/Station/Commands/JobsCommand.cs @@ -30,7 +30,7 @@ public IEnumerable<JobSlotRef> Jobs([PipedArgument] IEnumerable<EntityUid> stati => stations.SelectMany(Jobs); [CommandImplementation("job")] - public JobSlotRef Job([PipedArgument] EntityUid station, [CommandArgument] Prototype<JobPrototype> job) + public JobSlotRef Job([PipedArgument] EntityUid station, Prototype<JobPrototype> job) { _jobs ??= GetSys<StationJobsSystem>(); @@ -38,7 +38,7 @@ public JobSlotRef Job([PipedArgument] EntityUid station, [CommandArgument] Proto } [CommandImplementation("job")] - public IEnumerable<JobSlotRef> Job([PipedArgument] IEnumerable<EntityUid> stations, [CommandArgument] Prototype<JobPrototype> job) + public IEnumerable<JobSlotRef> Job([PipedArgument] IEnumerable<EntityUid> stations, Prototype<JobPrototype> job) => stations.Select(x => Job(x, job)); [CommandImplementation("isinfinite")] @@ -50,63 +50,41 @@ public IEnumerable<bool> IsInfinite([PipedArgument] IEnumerable<JobSlotRef> jobs => jobs.Select(x => IsInfinite(x, inverted)); [CommandImplementation("adjust")] - public JobSlotRef Adjust( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] JobSlotRef @ref, - [CommandArgument] ValueRef<int> by - ) + public JobSlotRef Adjust([PipedArgument] JobSlotRef @ref, int by) { _jobs ??= GetSys<StationJobsSystem>(); - _jobs.TryAdjustJobSlot(@ref.Station, @ref.Job, by.Evaluate(ctx), true, true); + _jobs.TryAdjustJobSlot(@ref.Station, @ref.Job, by, true, true); return @ref; } [CommandImplementation("adjust")] - public IEnumerable<JobSlotRef> Adjust( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<JobSlotRef> @ref, - [CommandArgument] ValueRef<int> by - ) - => @ref.Select(x => Adjust(ctx, x, by)); + public IEnumerable<JobSlotRef> Adjust([PipedArgument] IEnumerable<JobSlotRef> @ref, int by) + => @ref.Select(x => Adjust(x, by)); [CommandImplementation("set")] - public JobSlotRef Set( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] JobSlotRef @ref, - [CommandArgument] ValueRef<int> by - ) + public JobSlotRef Set([PipedArgument] JobSlotRef @ref, int by) { _jobs ??= GetSys<StationJobsSystem>(); - _jobs.TrySetJobSlot(@ref.Station, @ref.Job, by.Evaluate(ctx), true); + _jobs.TrySetJobSlot(@ref.Station, @ref.Job, by, true); return @ref; } [CommandImplementation("set")] - public IEnumerable<JobSlotRef> Set( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<JobSlotRef> @ref, - [CommandArgument] ValueRef<int> by - ) - => @ref.Select(x => Set(ctx, x, by)); + public IEnumerable<JobSlotRef> Set([PipedArgument] IEnumerable<JobSlotRef> @ref, int by) + => @ref.Select(x => Set(x, by)); [CommandImplementation("amount")] - public int Amount( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] JobSlotRef @ref - ) + public uint Amount([PipedArgument] JobSlotRef @ref) { _jobs ??= GetSys<StationJobsSystem>(); _jobs.TryGetJobSlot(@ref.Station, @ref.Job, out var slots); - return (int)(slots ?? 0); + return slots.HasValue ? slots.Value : 0; } [CommandImplementation("amount")] - public IEnumerable<int> Amount( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable<JobSlotRef> @ref - ) - => @ref.Select(x => Amount(ctx, x)); + public IEnumerable<uint> Amount([PipedArgument] IEnumerable<JobSlotRef> @ref) + => @ref.Select(Amount); } // Used for Toolshed queries. diff --git a/Content.Server/Station/Commands/StationCommand.cs b/Content.Server/Station/Commands/StationCommand.cs index 96e47d63363..cfda730eca9 100644 --- a/Content.Server/Station/Commands/StationCommand.cs +++ b/Content.Server/Station/Commands/StationCommand.cs @@ -27,7 +27,7 @@ public IEnumerable<EntityUid> List() } [CommandImplementation("get")] - public EntityUid Get([CommandInvocationContext] IInvocationContext ctx) + public EntityUid Get(IInvocationContext ctx) { _station ??= GetSys<StationSystem>(); @@ -54,7 +54,6 @@ public EntityUid Get([CommandInvocationContext] IInvocationContext ctx) public EntityUid? LargestGrid([PipedArgument] EntityUid input) { _station ??= GetSys<StationSystem>(); - return _station.GetLargestGrid(Comp<StationDataComponent>(input)); } @@ -80,46 +79,30 @@ public IEnumerable<EntityUid> Grids([PipedArgument] IEnumerable<EntityUid> input => input.Select(Config); [CommandImplementation("addgrid")] - public void AddGrid( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<EntityUid> grid - ) + public void AddGrid([PipedArgument] EntityUid input, EntityUid grid) { _station ??= GetSys<StationSystem>(); - - _station.AddGridToStation(input, grid.Evaluate(ctx)); + _station.AddGridToStation(input, grid); } [CommandImplementation("rmgrid")] - public void RmGrid( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<EntityUid> grid - ) + public void RmGrid([PipedArgument] EntityUid input, EntityUid grid) { _station ??= GetSys<StationSystem>(); - - _station.RemoveGridFromStation(input, grid.Evaluate(ctx)); + _station.RemoveGridFromStation(input, grid); } [CommandImplementation("rename")] - public void Rename([CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input, - [CommandArgument] ValueRef<string> name - ) + public void Rename([PipedArgument] EntityUid input, string name) { _station ??= GetSys<StationSystem>(); - - _station.RenameStation(input, name.Evaluate(ctx)!); + _station.RenameStation(input, name); } [CommandImplementation("rerollBounties")] - public void RerollBounties([CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid input) + public void RerollBounties([PipedArgument] EntityUid input) { _cargo ??= GetSys<CargoSystem>(); - _cargo.RerollBountyDatabase(input); } } diff --git a/Content.Server/Station/Systems/StationJobsSystem.cs b/Content.Server/Station/Systems/StationJobsSystem.cs index 3bfa815af1e..f708e8526d9 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.cs +++ b/Content.Server/Station/Systems/StationJobsSystem.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Server.DeltaV.Station.Events; // DeltaV using Content.Server.GameTicking; using Content.Server.Station.Components; using Content.Shared.CCVar; @@ -111,7 +112,8 @@ public bool TryAssignJob(EntityUid station, string jobPrototypeId, NetUserId net if (!TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs)) return false; - + var playerJobAdded = new PlayerJobAddedEvent(netUserId, jobPrototypeId); + RaiseLocalEvent(station, ref playerJobAdded, false); // DeltaV added AddedPlayerJobsEvent for CaptainStateSystem stationJobs.PlayerJobs.TryAdd(netUserId, new()); stationJobs.PlayerJobs[netUserId].Add(jobPrototypeId); return true; @@ -212,8 +214,15 @@ public bool TryRemovePlayerJobs(EntityUid station, { if (!Resolve(station, ref jobsComponent, false)) return false; - - return jobsComponent.PlayerJobs.Remove(userId); + // DeltaV added RemovedPlayerJobsEvent for CaptainStateSystem + if (jobsComponent.PlayerJobs.Remove(userId, out var playerJobsEntry)) + { + var playerJobRemovedEvent = new PlayerJobsRemovedEvent(userId, playerJobsEntry); + RaiseLocalEvent(station, ref playerJobRemovedEvent, false); + return true; + } + return false; + // DeltaV end added RemovedPlayerJobsEvent for CaptainStateSystem } /// <inheritdoc cref="TrySetJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,Content.Server.Station.Components.StationJobsComponent?)"/> diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs index 85be5f740d5..79192d49b40 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -181,16 +181,14 @@ public EntityUid SpawnPlayerMob( } var gearEquippedEv = new StartingGearEquippedEvent(entity.Value); - RaiseLocalEvent(entity.Value, ref gearEquippedEv, true); + RaiseLocalEvent(entity.Value, ref gearEquippedEv); if (profile != null) { _humanoidSystem.LoadProfile(entity.Value, profile); _metaSystem.SetEntityName(entity.Value, profile.Name); if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText)) - { AddComp<DetailExaminableComponent>(entity.Value).Content = profile.FlavorText; - } } DoJobSpecials(job, entity.Value); @@ -204,9 +202,7 @@ private void DoJobSpecials(JobComponent? job, EntityUid entity) return; foreach (var jobSpecial in prototype.Special) - { jobSpecial.AfterEquip(entity); - } } /// <summary> @@ -231,10 +227,8 @@ public void EquipIdCard(EntityUid entity, string characterName, JobPrototype job _cardSystem.TryChangeFullName(cardId, characterName, card); _cardSystem.TryChangeJobTitle(cardId, jobPrototype.LocalizedName, card); - if (_prototypeManager.TryIndex<StatusIconPrototype>(jobPrototype.Icon, out var jobIcon)) - { + if (_prototypeManager.TryIndex(jobPrototype.Icon, out var jobIcon)) _cardSystem.TryChangeJobIcon(cardId, jobIcon, card); - } var extendedAccess = false; if (station != null) diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index f2704d53f4c..95c2b0085bd 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.GameTicking.Components; using JetBrains.Annotations; using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Toolshed; using Robust.Shared.Utility; @@ -25,6 +26,9 @@ public sealed class BasicStationEventSchedulerSystem : GameRuleSystem<BasicStati [Dependency] private readonly EventManagerSystem _event = default!; [Dependency] private readonly IConfigurationManager _config = default!; + public const float MinEventTime = 60 * 3; + public const float MaxEventTime = 60 * 10; + protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) { @@ -70,9 +74,62 @@ private void ResetTimer(BasicStationEventSchedulerComponent component) public sealed class StationEventCommand : ToolshedCommand { private EventManagerSystem? _stationEvent; + private BasicStationEventSchedulerSystem? _basicScheduler; + private IRobustRandom? _random; + + /// <summary> + /// Estimates the expected number of times an event will run over the course of X rounds, taking into account weights and + /// how many events are expected to run over a given timeframe for a given playercount by repeatedly simulating rounds. + /// Effectively /100 (if you put 100 rounds) = probability an event will run per round. + /// </summary> + /// <remarks> + /// This isn't perfect. Code path eventually goes into <see cref="EventManagerSystem.CanRun"/>, which requires + /// state from <see cref="GameTicker"/>. As a result, you should probably just run this locally and not doing + /// a real round (it won't pollute the state, but it will get contaminated by previously ran events in the actual round) + /// and things like `MaxOccurrences` and `ReoccurrenceDelay` won't be respected. + /// + /// I consider these to not be that relevant to the analysis here though (and I don't want most uses of them + /// to even exist) so I think it's fine. + /// </remarks> + [CommandImplementation("simulate")] + public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev) + { + _stationEvent ??= GetSys<EventManagerSystem>(); + _basicScheduler ??= GetSys<BasicStationEventSchedulerSystem>(); + _random ??= IoCManager.Resolve<IRobustRandom>(); + + var occurrences = new Dictionary<string, int>(); + + foreach (var ev in _stationEvent.AllEvents()) + { + occurrences.Add(ev.Key.ID, 0); + } + + for (var i = 0; i < rounds; i++) + { + var curTime = TimeSpan.Zero; + var randomEndTime = _random.NextGaussian(roundEndMean, roundEndStdDev) * 60; // *60 = minutes to seconds + if (randomEndTime <= 0) + continue; + + while (curTime.TotalSeconds < randomEndTime) + { + // sim an event + curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime)); + var available = _stationEvent.AvailableEvents(false, playerCount, curTime); + var ev = _stationEvent.FindEvent(available); + if (ev == null) + continue; + + occurrences[ev] += 1; + } + } + + return occurrences.Select(p => (p.Key, (float) p.Value)).OrderByDescending(p => p.Item2); + } [CommandImplementation("lsprob")] - public IEnumerable<(string, float)> LsProb() + public IEnumerable<(string, float)> LsProb(EntityPrototype eventScheduler) { _stationEvent ??= GetSys<EventManagerSystem>(); var events = _stationEvent.AllEvents(); @@ -86,7 +143,7 @@ public sealed class StationEventCommand : ToolshedCommand } [CommandImplementation("lsprobtime")] - public IEnumerable<(string, float)> LsProbTime([CommandArgument] float time) + public IEnumerable<(string, float)> LsProbTime(EntityPrototype eventScheduler, float time) { _stationEvent ??= GetSys<EventManagerSystem>(); var events = _stationEvent.AllEvents().Where(pair => pair.Value.EarliestStart <= time).ToList(); @@ -100,7 +157,7 @@ public sealed class StationEventCommand : ToolshedCommand } [CommandImplementation("prob")] - public float Prob([CommandArgument] string eventId) + public float Prob(EntityPrototype eventScheduler, string eventId) { _stationEvent ??= GetSys<EventManagerSystem>(); var events = _stationEvent.AllEvents(); diff --git a/Content.Server/StationEvents/Components/NinjaSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/NinjaSpawnRuleComponent.cs deleted file mode 100644 index d758247eca2..00000000000 --- a/Content.Server/StationEvents/Components/NinjaSpawnRuleComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -/// <summary> -/// Configuration component for the Space Ninja antag. -/// </summary> -[RegisterComponent, Access(typeof(NinjaSpawnRule))] -public sealed partial class NinjaSpawnRuleComponent : Component -{ - /// <summary> - /// Distance that the ninja spawns from the station's half AABB radius - /// </summary> - [DataField("spawnDistance")] - public float SpawnDistance = 20f; -} diff --git a/Content.Server/StationEvents/Components/SpaceSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/SpaceSpawnRuleComponent.cs new file mode 100644 index 00000000000..a0168077fd6 --- /dev/null +++ b/Content.Server/StationEvents/Components/SpaceSpawnRuleComponent.cs @@ -0,0 +1,25 @@ +using Content.Server.StationEvents.Events; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.StationEvents.Components; + +/// <summary> +/// Component for spawning antags in space around a station. +/// Requires <c>AntagSelectionComponent</c>. +/// </summary> +[RegisterComponent, Access(typeof(SpaceSpawnRule))] +public sealed partial class SpaceSpawnRuleComponent : Component +{ + /// <summary> + /// Distance that the entity spawns from the station's half AABB radius + /// </summary> + [DataField] + public float SpawnDistance = 20f; + + /// <summary> + /// Location that was picked. + /// </summary> + [DataField] + public MapCoordinates? Coords; +} diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 9c0005d06db..eec4d792178 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -66,7 +66,7 @@ public string RunRandomEvent() /// Pick a random event from the available events at this time, also considering their weightings. /// </summary> /// <returns></returns> - private string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents) + public string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents) { if (availableEvents.Count == 0) { @@ -100,16 +100,20 @@ public string RunRandomEvent() /// <summary> /// Gets the events that have met their player count, time-until start, etc. /// </summary> - /// <param name="ignoreEarliestStart"></param> + /// <param name="playerCountOverride">Override for player count, if using this to simulate events rather than in an actual round.</param> + /// <param name="currentTimeOverride">Override for round time, if using this to simulate events rather than in an actual round.</param> /// <returns></returns> - private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool ignoreEarliestStart = false) + public Dictionary<EntityPrototype, StationEventComponent> AvailableEvents( + bool ignoreEarliestStart = false, + int? playerCountOverride = null, + TimeSpan? currentTimeOverride = null) { - var playerCount = _playerManager.PlayerCount; + var playerCount = playerCountOverride ?? _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - var currentTime = !ignoreEarliestStart + var currentTime = currentTimeOverride ?? (!ignoreEarliestStart ? GameTicker.RoundDuration() - : TimeSpan.Zero; + : TimeSpan.Zero); var result = new Dictionary<EntityPrototype, StationEventComponent>(); @@ -117,7 +121,6 @@ private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool { if (CanRun(proto, stationEvent, playerCount, currentTime)) { - Log.Debug($"Adding event {proto.ID} to possibilities"); result.Add(proto, stationEvent); } } diff --git a/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs b/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs index f80bc83a1e6..4df099b0897 100644 --- a/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs +++ b/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs @@ -8,6 +8,7 @@ using Content.Server.StationEvents.Components; using Content.Shared.Psionics.Glimmer; using Content.Shared.Abilities.Psionics; +using Content.Shared.NPC.Components; using Robust.Shared.Map; namespace Content.Server.StationEvents.Events; diff --git a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs similarity index 53% rename from Content.Server/StationEvents/Events/NinjaSpawnRule.cs rename to Content.Server/StationEvents/Events/SpaceSpawnRule.cs index 9cbc193ce61..6fccaaa5cfc 100644 --- a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs +++ b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs @@ -1,5 +1,5 @@ +using Content.Server.Antag; using Content.Server.GameTicking.Rules.Components; -using Content.Server.Ninja.Systems; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; @@ -9,18 +9,28 @@ namespace Content.Server.StationEvents.Events; /// <summary> -/// Event for spawning a Space Ninja mid-game. +/// Station event component for spawning this rules antags in space around a station. /// </summary> -public sealed class NinjaSpawnRule : StationEventSystem<NinjaSpawnRuleComponent> +public sealed class SpaceSpawnRule : StationEventSystem<SpaceSpawnRuleComponent> { [Dependency] private readonly SharedTransformSystem _transform = default!; - protected override void Started(EntityUid uid, NinjaSpawnRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Initialize() { - base.Started(uid, comp, gameRule, args); + base.Initialize(); + + SubscribeLocalEvent<SpaceSpawnRuleComponent, AntagSelectLocationEvent>(OnSelectLocation); + } + + protected override void Added(EntityUid uid, SpaceSpawnRuleComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + base.Added(uid, comp, gameRule, args); if (!TryGetRandomStation(out var station)) + { + ForceEndSelf(uid, gameRule); return; + } var stationData = Comp<StationDataComponent>(station.Value); @@ -28,22 +38,28 @@ protected override void Started(EntityUid uid, NinjaSpawnRuleComponent comp, Gam var gridUid = StationSystem.GetLargestGrid(stationData); if (gridUid == null || !TryComp<MapGridComponent>(gridUid, out var grid)) { - Sawmill.Warning("Chosen station has no grids, cannot spawn space ninja!"); + Sawmill.Warning("Chosen station has no grids, cannot pick location for {ToPrettyString(uid):rule}"); + ForceEndSelf(uid, gameRule); return; } - // figure out its AABB size and use that as a guide to how far ninja should be + // figure out its AABB size and use that as a guide to how far the spawner should be var size = grid.LocalAABB.Size.Length() / 2; var distance = size + comp.SpawnDistance; var angle = RobustRandom.NextAngle(); // position relative to station center var location = angle.ToVec() * distance; - // create the spawner, the ninja will appear when a ghost has picked the role + // create the spawner! var xform = Transform(gridUid.Value); var position = _transform.GetWorldPosition(xform) + location; - var coords = new MapCoordinates(position, xform.MapID); - Sawmill.Info($"Creating ninja spawnpoint at {coords}"); - Spawn("SpawnPointGhostSpaceNinja", coords); + comp.Coords = new MapCoordinates(position, xform.MapID); + Sawmill.Info($"Picked location {comp.Coords} for {ToPrettyString(uid):rule}"); + } + + private void OnSelectLocation(Entity<SpaceSpawnRuleComponent> ent, ref AntagSelectLocationEvent args) + { + if (ent.Comp.Coords is {} coords) + args.Coordinates.Add(coords); } } diff --git a/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs b/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs index 9076bee436f..a6356f0baa3 100644 --- a/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs +++ b/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs @@ -18,4 +18,10 @@ public sealed partial class GeneralStationRecordConsoleComponent : Component /// </summary> [DataField] public StationRecordsFilter? Filter; + + /// <summary> + /// Whether this Records Console is able to delete entries. + /// </summary> + [DataField] + public bool CanDeleteEntries; } diff --git a/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs b/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs index a5202285d99..87246ab6757 100644 --- a/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs +++ b/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs @@ -23,9 +23,22 @@ public override void Initialize() subs.Event<BoundUIOpenedEvent>(UpdateUserInterface); subs.Event<SelectStationRecord>(OnKeySelected); subs.Event<SetStationRecordFilter>(OnFiltersChanged); + subs.Event<DeleteStationRecord>(OnRecordDelete); }); } + private void OnRecordDelete(Entity<GeneralStationRecordConsoleComponent> ent, ref DeleteStationRecord args) + { + if (!ent.Comp.CanDeleteEntries) + return; + + var owning = _station.GetOwningStation(ent.Owner); + + if (owning != null) + _stationRecords.RemoveRecord(new StationRecordKey(args.Id, owning.Value)); + UpdateUserInterface(ent); // Apparently an event does not get raised for this. + } + private void UpdateUserInterface<T>(Entity<GeneralStationRecordConsoleComponent> ent, ref T args) { UpdateUserInterface(ent); @@ -68,8 +81,9 @@ private void UpdateUserInterface(Entity<GeneralStationRecordConsoleComponent> en case 0: _ui.SetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState()); return; - case 1: - console.ActiveKey = listing.Keys.First(); + default: + if (console.ActiveKey == null) + console.ActiveKey = listing.Keys.First(); break; } @@ -79,7 +93,7 @@ private void UpdateUserInterface(Entity<GeneralStationRecordConsoleComponent> en var key = new StationRecordKey(id, owningStation.Value); _stationRecords.TryGetRecord<GeneralStationRecord>(key, out var record, stationRecords); - GeneralStationRecordConsoleState newState = new(id, record, listing, console.Filter); + GeneralStationRecordConsoleState newState = new(id, record, listing, console.Filter, ent.Comp.CanDeleteEntries); _ui.SetUiState(uid, GeneralStationRecordConsoleKey.Key, newState); } } diff --git a/Content.Server/Sticky/Systems/StickySystem.cs b/Content.Server/Sticky/Systems/StickySystem.cs index 2d1104e2ceb..21a7f4d0395 100644 --- a/Content.Server/Sticky/Systems/StickySystem.cs +++ b/Content.Server/Sticky/Systems/StickySystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Sticky; using Content.Shared.Sticky.Components; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Utility; @@ -20,6 +21,7 @@ public sealed class StickySystem : EntitySystem [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; private const string StickerSlotId = "stickers_container"; @@ -67,9 +69,8 @@ private bool StartSticking(EntityUid uid, EntityUid user, EntityUid target, Stic return false; // check whitelist and blacklist - if (component.Whitelist != null && !component.Whitelist.IsValid(target)) - return false; - if (component.Blacklist != null && component.Blacklist.IsValid(target)) + if (_whitelistSystem.IsWhitelistFail(component.Whitelist, target) || + _whitelistSystem.IsBlacklistPass(component.Blacklist, target)) return false; var attemptEv = new AttemptEntityStickEvent(target, user); @@ -93,8 +94,7 @@ private bool StartSticking(EntityUid uid, EntityUid user, EntityUid target, Stic // start sticking object to target _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new StickyDoAfterEvent(), uid, target: target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }); } @@ -148,8 +148,7 @@ private void StartUnsticking(EntityUid uid, EntityUid user, StickyComponent? com // start unsticking object _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new StickyDoAfterEvent(), uid, target: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }); } diff --git a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs index 838311c1aac..9da7606bcc5 100644 --- a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs +++ b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs @@ -109,7 +109,7 @@ private void PreOpen(EntityUid uid, BluespaceLockerComponent component, ref Stor // Move contained air if (component.BehaviorProperties.TransportGas) { - entityStorageComponent.Air.CopyFromMutable(target.Value.storageComponent.Air); + entityStorageComponent.Air.CopyFrom(target.Value.storageComponent.Air); target.Value.storageComponent.Air.Clear(); } @@ -326,7 +326,7 @@ private void PostClose(EntityUid uid, BluespaceLockerComponent component, bool d // Move contained air if (component.BehaviorProperties.TransportGas) { - target.Value.storageComponent.Air.CopyFromMutable(entityStorageComponent.Air); + target.Value.storageComponent.Air.CopyFrom(entityStorageComponent.Air); entityStorageComponent.Air.Clear(); } diff --git a/Content.Server/Storage/EntitySystems/ItemCounterSystem.cs b/Content.Server/Storage/EntitySystems/ItemCounterSystem.cs index 415e8d92467..43fcb32d1f0 100644 --- a/Content.Server/Storage/EntitySystems/ItemCounterSystem.cs +++ b/Content.Server/Storage/EntitySystems/ItemCounterSystem.cs @@ -1,6 +1,7 @@ -using Content.Shared.Storage; +using Content.Shared.Storage; using Content.Shared.Storage.Components; using Content.Shared.Storage.EntitySystems; +using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Containers; @@ -9,6 +10,7 @@ namespace Content.Server.Storage.EntitySystems [UsedImplicitly] public sealed class ItemCounterSystem : SharedItemCounterSystem { + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; protected override int? GetCount(ContainerModifiedMessage msg, ItemCounterComponent itemCounter) { if (!EntityManager.TryGetComponent(msg.Container.Owner, out StorageComponent? component)) @@ -19,7 +21,7 @@ public sealed class ItemCounterSystem : SharedItemCounterSystem var count = 0; foreach (var entity in component.Container.ContainedEntities) { - if (itemCounter.Count.IsValid(entity)) + if (_whitelistSystem.IsWhitelistPass(itemCounter.Count, entity)) count++; } diff --git a/Content.Server/Store/Conditions/BuyBeforeCondition.cs b/Content.Server/Store/Conditions/BuyBeforeCondition.cs index 132f3534391..fcfb5f92c9d 100644 --- a/Content.Server/Store/Conditions/BuyBeforeCondition.cs +++ b/Content.Server/Store/Conditions/BuyBeforeCondition.cs @@ -1,6 +1,5 @@ -using Content.Server.Store.Components; -using Content.Server.Store.Systems; using Content.Shared.Store; +using Content.Shared.Store.Components; using Robust.Shared.Prototypes; namespace Content.Server.Store.Conditions; @@ -23,7 +22,7 @@ public override bool Condition(ListingConditionArgs args) if (!args.EntityManager.TryGetComponent<StoreComponent>(args.StoreEntity, out var storeComp)) return false; - var allListings = storeComp.Listings; + var allListings = storeComp.FullListingsCatalog; var purchasesFound = false; diff --git a/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs b/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs index 859703a72a7..ff4a9a19cdd 100644 --- a/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs +++ b/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs @@ -23,18 +23,11 @@ public sealed partial class BuyerWhitelistCondition : ListingCondition public override bool Condition(ListingConditionArgs args) { var ent = args.EntityManager; + var whitelistSystem = ent.System<EntityWhitelistSystem>(); - if (Whitelist != null) - { - if (!Whitelist.IsValid(args.Buyer, ent)) - return false; - } - - if (Blacklist != null) - { - if (Blacklist.IsValid(args.Buyer, ent)) - return false; - } + if (whitelistSystem.IsWhitelistFail(Whitelist, args.Buyer) || + whitelistSystem.IsBlacklistPass(Blacklist, args.Buyer)) + return false; return true; } diff --git a/Content.Server/Store/Conditions/StoreWhitelistCondition.cs b/Content.Server/Store/Conditions/StoreWhitelistCondition.cs index 20ec5cecce8..ced4dfa9c0b 100644 --- a/Content.Server/Store/Conditions/StoreWhitelistCondition.cs +++ b/Content.Server/Store/Conditions/StoreWhitelistCondition.cs @@ -26,18 +26,11 @@ public override bool Condition(ListingConditionArgs args) return false; var ent = args.EntityManager; + var whitelistSystem = ent.System<EntityWhitelistSystem>(); - if (Whitelist != null) - { - if (!Whitelist.IsValid(args.StoreEntity.Value, ent)) - return false; - } - - if (Blacklist != null) - { - if (Blacklist.IsValid(args.StoreEntity.Value, ent)) - return false; - } + if (whitelistSystem.IsWhitelistFail(Whitelist, args.StoreEntity.Value) || + whitelistSystem.IsBlacklistPass(Blacklist, args.StoreEntity.Value)) + return false; return true; } diff --git a/Content.Server/Store/Systems/StoreSystem.Command.cs b/Content.Server/Store/Systems/StoreSystem.Command.cs index d259da2c95e..5ad361eb427 100644 --- a/Content.Server/Store/Systems/StoreSystem.Command.cs +++ b/Content.Server/Store/Systems/StoreSystem.Command.cs @@ -1,7 +1,9 @@ +using System.Linq; using Content.Server.Store.Components; using Content.Shared.FixedPoint; using Content.Server.Administration; using Content.Shared.Administration; +using Content.Shared.Store.Components; using Robust.Shared.Console; namespace Content.Server.Store.Systems; @@ -58,7 +60,7 @@ private CompletionResult AddCurrencyCommandCompletions(IConsoleShell shell, stri if (args.Length == 2 && NetEntity.TryParse(args[0], out var uidNet) && TryGetEntity(uidNet, out var uid)) { if (TryComp<StoreComponent>(uid, out var store)) - return CompletionResult.FromHintOptions(store.CurrencyWhitelist, "<currency prototype>"); + return CompletionResult.FromHintOptions(store.CurrencyWhitelist.Select(p => p.ToString()), "<currency prototype>"); } return CompletionResult.Empty; diff --git a/Content.Server/Store/Systems/StoreSystem.Listings.cs b/Content.Server/Store/Systems/StoreSystem.Listings.cs index a56d9640d37..b32eb505709 100644 --- a/Content.Server/Store/Systems/StoreSystem.Listings.cs +++ b/Content.Server/Store/Systems/StoreSystem.Listings.cs @@ -1,5 +1,7 @@ -using Content.Server.Store.Components; +using System.Diagnostics.CodeAnalysis; using Content.Shared.Store; +using Content.Shared.Store.Components; +using Robust.Shared.Prototypes; namespace Content.Server.Store.Systems; @@ -12,25 +14,43 @@ public sealed partial class StoreSystem /// <param name="component">The store to refresh</param> public void RefreshAllListings(StoreComponent component) { - component.Listings = GetAllListings(); + var previousState = component.FullListingsCatalog; + var newState = GetAllListings(); + // if we refresh list with existing cost modifiers - they will be removed, + // need to restore them + if (previousState.Count != 0) + { + foreach (var previousStateListingItem in previousState) + { + if (!previousStateListingItem.IsCostModified + || !TryGetListing(newState, previousStateListingItem.ID, out var found)) + { + continue; + } + + foreach (var (modifierSourceId, costModifier) in previousStateListingItem.CostModifiersBySourceId) + { + found.AddCostModifier(modifierSourceId, costModifier); + } + } + } + + component.FullListingsCatalog = newState; } /// <summary> /// Gets all listings from a prototype. /// </summary> /// <returns>All the listings</returns> - public HashSet<ListingData> GetAllListings() + public HashSet<ListingDataWithCostModifiers> GetAllListings() { - var allListings = _proto.EnumeratePrototypes<ListingPrototype>(); - - var allData = new HashSet<ListingData>(); - - foreach (var listing in allListings) + var clones = new HashSet<ListingDataWithCostModifiers>(); + foreach (var prototype in _proto.EnumeratePrototypes<ListingPrototype>()) { - allData.Add((ListingData) listing.Clone()); + clones.Add(new(prototype)); } - return allData; + return clones; } /// <summary> @@ -38,7 +58,7 @@ public HashSet<ListingData> GetAllListings() /// </summary> /// <param name="component">The store to add the listing to</param> /// <param name="listingId">The id of the listing</param> - /// <returns>Whetehr or not the listing was added successfully</returns> + /// <returns>Whether or not the listing was added successfully</returns> public bool TryAddListing(StoreComponent component, string listingId) { if (!_proto.TryIndex<ListingPrototype>(listingId, out var proto)) @@ -46,6 +66,7 @@ public bool TryAddListing(StoreComponent component, string listingId) Log.Error("Attempted to add invalid listing."); return false; } + return TryAddListing(component, proto); } @@ -55,9 +76,9 @@ public bool TryAddListing(StoreComponent component, string listingId) /// <param name="component">The store to add the listing to</param> /// <param name="listing">The listing</param> /// <returns>Whether or not the listing was add successfully</returns> - public bool TryAddListing(StoreComponent component, ListingData listing) + public bool TryAddListing(StoreComponent component, ListingPrototype listing) { - return component.Listings.Add(listing); + return component.FullListingsCatalog.Add(new ListingDataWithCostModifiers(listing)); } /// <summary> @@ -67,9 +88,9 @@ public bool TryAddListing(StoreComponent component, ListingData listing) /// <param name="store"></param> /// <param name="component">The store the listings are coming from.</param> /// <returns>The available listings.</returns> - public IEnumerable<ListingData> GetAvailableListings(EntityUid buyer, EntityUid store, StoreComponent component) + public IEnumerable<ListingDataWithCostModifiers> GetAvailableListings(EntityUid buyer, EntityUid store, StoreComponent component) { - return GetAvailableListings(buyer, component.Listings, component.Categories, store); + return GetAvailableListings(buyer, component.FullListingsCatalog, component.Categories, store); } /// <summary> @@ -80,7 +101,12 @@ public IEnumerable<ListingData> GetAvailableListings(EntityUid buyer, EntityUid /// <param name="categories">What categories to filter by.</param> /// <param name="storeEntity">The physial entity of the store. Can be null.</param> /// <returns>The available listings.</returns> - public IEnumerable<ListingData> GetAvailableListings(EntityUid buyer, HashSet<ListingData>? listings, HashSet<string> categories, EntityUid? storeEntity = null) + public IEnumerable<ListingDataWithCostModifiers> GetAvailableListings( + EntityUid buyer, + IReadOnlyCollection<ListingDataWithCostModifiers>? listings, + HashSet<ProtoId<StoreCategoryPrototype>> categories, + EntityUid? storeEntity = null + ) { listings ??= GetAllListings(); @@ -117,7 +143,7 @@ public IEnumerable<ListingData> GetAvailableListings(EntityUid buyer, HashSet<Li /// <param name="listing">The listing itself.</param> /// <param name="categories">The categories to check through.</param> /// <returns>If the listing was present in one of the categories.</returns> - public bool ListingHasCategory(ListingData listing, HashSet<string> categories) + public bool ListingHasCategory(ListingData listing, HashSet<ProtoId<StoreCategoryPrototype>> categories) { foreach (var cat in categories) { @@ -126,4 +152,19 @@ public bool ListingHasCategory(ListingData listing, HashSet<string> categories) } return false; } + + private bool TryGetListing(IReadOnlyCollection<ListingDataWithCostModifiers> collection, string listingId, [MaybeNullWhen(false)] out ListingDataWithCostModifiers found) + { + foreach(var current in collection) + { + if (current.ID == listingId) + { + found = current; + return true; + } + } + + found = null!; + return false; + } } diff --git a/Content.Server/Store/Systems/StoreSystem.Refund.cs b/Content.Server/Store/Systems/StoreSystem.Refund.cs index 5a8be4be2bb..04bd585ffcf 100644 --- a/Content.Server/Store/Systems/StoreSystem.Refund.cs +++ b/Content.Server/Store/Systems/StoreSystem.Refund.cs @@ -1,4 +1,5 @@ using Content.Server.Store.Components; +using Content.Shared.Store.Components; using Robust.Shared.Containers; namespace Content.Server.Store.Systems; @@ -15,7 +16,7 @@ private void InitializeRefund() private void OnEntityRemoved(EntityUid uid, StoreRefundComponent component, EntRemovedFromContainerMessage args) { - if (component.StoreEntity == null || _actions.TryGetActionData(uid, out _) || !TryComp<StoreComponent>(component.StoreEntity.Value, out var storeComp)) + if (component.StoreEntity == null || _actions.TryGetActionData(uid, out _, false) || !TryComp<StoreComponent>(component.StoreEntity.Value, out var storeComp)) return; DisableRefund(component.StoreEntity.Value, storeComp); diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 0d2f3af91b3..d186ce1e3b2 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -10,6 +10,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.Mind; using Content.Shared.Store; +using Content.Shared.Store.Components; using Content.Shared.UserInterface; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; @@ -82,20 +83,16 @@ public void CloseUi(EntityUid uid, StoreComponent? component = null) /// <param name="user">The person who if opening the store ui. Listings are filtered based on this.</param> /// <param name="store">The store entity itself</param> /// <param name="component">The store component being refreshed.</param> - /// <param name="ui"></param> public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent? component = null) { if (!Resolve(store, ref component)) return; - // TODO: Why is the state not being set unless this? - if (!_ui.HasUi(store, StoreUiKey.Key)) - return; - //this is the person who will be passed into logic for all listing filtering. if (user != null) //if we have no "buyer" for this update, then don't update the listings { - component.LastAvailableListings = GetAvailableListings(component.AccountOwner ?? user.Value, store, component).ToHashSet(); + component.LastAvailableListings = GetAvailableListings(component.AccountOwner ?? user.Value, store, component) + .ToHashSet(); } //dictionary for all currencies, including 0 values for currencies on the whitelist @@ -113,6 +110,7 @@ public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent // only tell operatives to lock their uplink if it can be locked var showFooter = HasComp<RingerUplinkComponent>(store); + var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed); _ui.SetUiState(store, StoreUiKey.Key, state); } @@ -132,7 +130,7 @@ private void BeforeActivatableUiOpen(EntityUid uid, StoreComponent component, Be /// </summary> private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListingMessage msg) { - var listing = component.Listings.FirstOrDefault(x => x.Equals(msg.Listing)); + var listing = component.FullListingsCatalog.FirstOrDefault(x => x.ID.Equals(msg.Listing.Id)); if (listing == null) //make sure this listing actually exists { @@ -157,9 +155,10 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi } //check that we have enough money - foreach (var currency in listing.Cost) + var cost = listing.Cost; + foreach (var (currency, amount) in cost) { - if (!component.Balance.TryGetValue(currency.Key, out var balance) || balance < currency.Value) + if (!component.Balance.TryGetValue(currency, out var balance) || balance < amount) { return; } @@ -169,13 +168,13 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi component.RefundAllowed = false; //subtract the cash - foreach (var (currency, value) in listing.Cost) + foreach (var (currency, amount) in cost) { - component.Balance[currency] -= value; + component.Balance[currency] -= amount; component.BalanceSpent.TryAdd(currency, FixedPoint2.Zero); - component.BalanceSpent[currency] += value; + component.BalanceSpent[currency] += amount; } //spawn entity @@ -217,7 +216,7 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi if (listing.ProductUpgradeId != null) { - foreach (var upgradeListing in component.Listings) + foreach (var upgradeListing in component.FullListingsCatalog) { if (upgradeListing.ID == listing.ProductUpgradeId) { @@ -259,18 +258,22 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi } //log dat shit. - _admin.Add(LogType.StorePurchase, LogImpact.Low, + _admin.Add(LogType.StorePurchase, + LogImpact.Low, $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager)}\" from {ToPrettyString(uid)}"); listing.PurchaseAmount++; //track how many times something has been purchased _audio.PlayEntity(component.BuySuccessSound, msg.Actor, uid); //cha-ching! - if (listing.SaleLimit != 0 && listing.DiscountValue > 0 && listing.PurchaseAmount >= listing.SaleLimit) + // Nyano code needs to know when a buy finished. Probably a better way? + var buyFinished = new StoreBuyFinishedEvent { - listing.DiscountValue = 0; - listing.Cost = listing.OldCost; - } + Buyer = buyer, + PurchasedItem = listing, + StoreUid = uid + }; + RaiseLocalEvent(ref buyFinished); UpdateUserInterface(buyer, uid, component); } @@ -355,6 +358,7 @@ private void OnRequestRefund(EntityUid uid, StoreComponent component, StoreReque { component.Balance[currency] += value; } + // Reset store back to its original state RefreshAllListings(component); component.BalanceSpent = new(); diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 978d98ac0cf..7bdf550301e 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -5,13 +5,11 @@ using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Stacks; -using Content.Shared.Store; +using Content.Shared.Store.Components; using JetBrains.Annotations; -using Robust.Server.GameObjects; using Robust.Shared.Prototypes; -using System.Linq; -using Content.Server.StoreDiscount; using Robust.Shared.Utility; +using System.Linq; namespace Content.Server.Store.Systems; @@ -23,7 +21,6 @@ public sealed partial class StoreSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly StoreDiscountSystem _storeDiscount = default!; public override void Initialize() { @@ -46,7 +43,6 @@ public override void Initialize() private void OnMapInit(EntityUid uid, StoreComponent component, MapInitEvent args) { RefreshAllListings(component); - InitializeFromPreset(component.Preset, uid, component); component.StartingMap = Transform(uid).MapUid; } @@ -56,7 +52,6 @@ private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup if (MetaData(uid).EntityLifeStage == EntityLifeStage.MapInitialized) { RefreshAllListings(component); - InitializeFromPreset(component.Preset, uid, component); } var ev = new StoreAddedEvent(); @@ -97,14 +92,12 @@ private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterIn if (ev.Cancelled) return; - args.Handled = TryAddCurrency(GetCurrencyValue(uid, component), args.Target.Value, store); + if (!TryAddCurrency((uid, component), (args.Target.Value, store))) + return; - if (args.Handled) - { - var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target)); - _popup.PopupEntity(msg, args.Target.Value, args.User); - QueueDel(args.Used); - } + args.Handled = true; + var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target)); + _popup.PopupEntity(msg, args.Target.Value, args.User); } private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args) @@ -116,6 +109,10 @@ private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUpli /// Gets the value from an entity's currency component. /// Scales with stacks. /// </summary> + /// <remarks> + /// If this result is intended to be used with <see cref="TryAddCurrency(Robust.Shared.GameObjects.Entity{Content.Server.Store.Components.CurrencyComponent?},Robust.Shared.GameObjects.Entity{Content.Shared.Store.Components.StoreComponent?})"/>, + /// consider using <see cref="TryAddCurrency(Robust.Shared.GameObjects.Entity{Content.Server.Store.Components.CurrencyComponent?},Robust.Shared.GameObjects.Entity{Content.Shared.Store.Components.StoreComponent?})"/> instead to ensure that the currency is consumed in the process. + /// </remarks> /// <param name="uid"></param> /// <param name="component"></param> /// <returns>The value of the currency</returns> @@ -126,19 +123,34 @@ public Dictionary<string, FixedPoint2> GetCurrencyValue(EntityUid uid, CurrencyC } /// <summary> - /// Tries to add a currency to a store's balance. + /// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process. /// </summary> - /// <param name="currencyEnt"></param> - /// <param name="storeEnt"></param> - /// <param name="currency">The currency to add</param> - /// <param name="store">The store to add it to</param> - /// <returns>Whether or not the currency was succesfully added</returns> - [PublicAPI] - public bool TryAddCurrency(EntityUid currencyEnt, EntityUid storeEnt, StoreComponent? store = null, CurrencyComponent? currency = null) + public bool TryAddCurrency(Entity<CurrencyComponent?> currency, Entity<StoreComponent?> store) { - if (!Resolve(currencyEnt, ref currency) || !Resolve(storeEnt, ref store)) + if (!Resolve(currency.Owner, ref currency.Comp)) + return false; + + if (!Resolve(store.Owner, ref store.Comp)) + return false; + + var value = currency.Comp.Price; + if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1) + { + value = currency.Comp.Price + .ToDictionary(v => v.Key, p => p.Value * stack.Count); + } + + if (!TryAddCurrency(value, store, store.Comp)) return false; - return TryAddCurrency(GetCurrencyValue(currencyEnt, currency), storeEnt, store); + + // Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the + // same tick + currency.Comp.Price.Clear(); + if (stack != null) + _stack.SetCount(currency.Owner, 0, stack); + + QueueDel(currency); + return true; } /// <summary> @@ -169,43 +181,6 @@ public bool TryAddCurrency(Dictionary<string, FixedPoint2> currency, EntityUid u UpdateUserInterface(null, uid, store); return true; } - - /// <summary> - /// Initializes a store based on a preset ID - /// </summary> - /// <param name="preset">The ID of a store preset prototype</param> - /// <param name="uid"></param> - /// <param name="component">The store being initialized</param> - public void InitializeFromPreset(string? preset, EntityUid uid, StoreComponent component) - { - if (preset == null) - return; - - if (!_proto.TryIndex<StorePresetPrototype>(preset, out var proto)) - return; - - InitializeFromPreset(proto, uid, component); - } - - /// <summary> - /// Initializes a store based on a given preset - /// </summary> - /// <param name="preset">The StorePresetPrototype</param> - /// <param name="uid"></param> - /// <param name="component">The store being initialized</param> - public void InitializeFromPreset(StorePresetPrototype preset, EntityUid uid, StoreComponent component) - { - component.Preset = preset.ID; - component.CurrencyWhitelist.UnionWith(preset.CurrencyWhitelist); - component.Categories.UnionWith(preset.Categories); - if (component.Balance == new Dictionary<string, FixedPoint2>() && preset.InitialBalance != null) //if we don't have a value stored, use the preset - TryAddCurrency(preset.InitialBalance, uid, component); - - _storeDiscount.ApplyDiscounts(component.Listings, preset); - - if (_ui.HasUi(uid, StoreUiKey.Key)) - _ui.SetUiState(uid, StoreUiKey.Key, new StoreInitializeState(preset.StoreName)); - } } public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs @@ -223,14 +198,3 @@ public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid us Store = store; } } - - -/// <summary> -/// Nyano/DeltaV Code. For penguin bombs and what not. -/// Raised on an item when it is purchased. -/// An item may need to set it upself up for its purchaser. -/// For example, to make sure it isn't hostile to them or -/// to make sure it fits their apperance. -/// </summary> -[ByRefEvent] -public readonly record struct ItemPurchasedEvent(EntityUid Purchaser); diff --git a/Content.Server/StoreDiscount/StoreDiscountSystem.cs b/Content.Server/StoreDiscount/StoreDiscountSystem.cs deleted file mode 100644 index 8f77eba7801..00000000000 --- a/Content.Server/StoreDiscount/StoreDiscountSystem.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Linq; -using Content.Shared.FixedPoint; -using Content.Shared.Store; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.StoreDiscount; - -public sealed class StoreDiscountSystem : EntitySystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - - public void ApplyDiscounts(IEnumerable<ListingData> listings, StorePresetPrototype store) - { - if (!store.Sales.Enabled) - return; - - var count = _random.Next(store.Sales.MinItems, store.Sales.MaxItems + 1); - - listings = listings - .Where(l => - !l.SaleBlacklist - && l.Cost.Any(x => x.Value > 1) - && store.Categories.Overlaps(ChangedFormatCategories(l.Categories))) - .OrderBy(_ => _random.Next()).Take(count).ToList(); - - foreach (var listing in listings) - { - var sale = GetDiscount(store.Sales.MinMultiplier, store.Sales.MaxMultiplier); - var newCost = listing.Cost.ToDictionary(x => x.Key, - x => FixedPoint2.New(Math.Max(1, (int) MathF.Round(x.Value.Float() * sale)))); - - if (listing.Cost.All(x => x.Value.Int() == newCost[x.Key].Int())) - continue; - - var key = listing.Cost.First(x => x.Value > 0).Key; - listing.OldCost = listing.Cost; - listing.DiscountValue = 100 - (newCost[key] / listing.Cost[key] * 100).Int(); - listing.Cost = newCost; - listing.Categories = new() {store.Sales.SalesCategory}; - } - } - - private IEnumerable<string> ChangedFormatCategories(List<ProtoId<StoreCategoryPrototype>> categories) - { - var modified = from p in categories select p.Id; - - return modified; - } - - private float GetDiscount(float minMultiplier, float maxMultiplier) - { - return _random.NextFloat() * (maxMultiplier - minMultiplier) + minMultiplier; - } -} diff --git a/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs b/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs new file mode 100644 index 00000000000..c1352baf113 --- /dev/null +++ b/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs @@ -0,0 +1,397 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.Store.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Store; +using Content.Shared.StoreDiscount.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.StoreDiscount.Systems; + +/// <summary> +/// Discount system that is part of <see cref="StoreSystem"/>. +/// </summary> +public sealed class StoreDiscountSystem : EntitySystem +{ + [ValidatePrototypeId<StoreCategoryPrototype>] + private const string DiscountedStoreCategoryPrototypeKey = "DiscountedItems"; + + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + /// <inheritdoc /> + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<StoreInitializedEvent>(OnStoreInitialized); + SubscribeLocalEvent<StoreBuyFinishedEvent>(OnBuyFinished); + } + + /// <summary> Decrements discounted item count, removes discount modifier and category, if counter reaches zero. </summary> + private void OnBuyFinished(ref StoreBuyFinishedEvent ev) + { + var (_, storeId, purchasedItem) = ev; + if (!TryComp<StoreDiscountComponent>(storeId, out var discountsComponent)) + { + return; + } + + // find and decrement discount count for item, if there is one. + if (!TryGetDiscountData(discountsComponent.Discounts, purchasedItem, out var discountData) || discountData.Count == 0) + { + return; + } + + discountData.Count--; + if (discountData.Count > 0) + { + return; + } + + // if there were discounts, but they are all bought up now - restore state: remove modifier and remove store category + purchasedItem.RemoveCostModifier(discountData.DiscountCategory); + purchasedItem.Categories.Remove(DiscountedStoreCategoryPrototypeKey); + } + + /// <summary> Initialized discounts if required. </summary> + private void OnStoreInitialized(ref StoreInitializedEvent ev) + { + if (!ev.UseDiscounts) + { + return; + } + + var discountComponent = EnsureComp<StoreDiscountComponent>(ev.Store); + var discounts = InitializeDiscounts(ev.Listings); + ApplyDiscounts(ev.Listings, discounts); + discountComponent.Discounts = discounts; + } + + private IReadOnlyList<StoreDiscountData> InitializeDiscounts( + IReadOnlyCollection<ListingDataWithCostModifiers> listings, + int totalAvailableDiscounts = 3 + ) + { + // Get list of categories with cumulative weights. + // for example if we have categories with weights 2, 18 and 80 + // list of cumulative ones will be 2,20,100 (with 100 being total). + // Then roll amount of unique listing items to be discounted under + // each category, and after that - roll exact items in categories + // and their cost + + var prototypes = _prototypeManager.EnumeratePrototypes<DiscountCategoryPrototype>(); + var categoriesWithCumulativeWeight = new CategoriesWithCumulativeWeightMap(prototypes); + var uniqueListingItemCountByCategory = PickCategoriesToRoll(totalAvailableDiscounts, categoriesWithCumulativeWeight); + + return RollItems(listings, uniqueListingItemCountByCategory); + } + + /// <summary> + /// Roll <b>how many</b> unique listing items which discount categories going to have. This will be used later to then pick listing items + /// to actually set discounts. + /// </summary> + /// <remarks> + /// Not every discount category have equal chance to be rolled, and not every discount category even can be rolled. + /// This step is important to distribute discounts properly (weighted) and with respect of + /// category maxItems, and more importantly - to not roll same item multiple times on next step. + /// </remarks> + /// <param name="totalAvailableDiscounts"> + /// Total amount of different listing items to be discounted. Depending on <see cref="DiscountCategoryPrototype.MaxItems"/> + /// there might be less discounts then <see cref="totalAvailableDiscounts"/>, but never more. + /// </param> + /// <param name="categoriesWithCumulativeWeightMap"> + /// Map of discount category cumulative weights by respective protoId of discount category. + /// </param> + /// <returns>Map: <b>count</b> of different listing items to be discounted, by discount category.</returns> + private Dictionary<ProtoId<DiscountCategoryPrototype>, int> PickCategoriesToRoll( + int totalAvailableDiscounts, + CategoriesWithCumulativeWeightMap categoriesWithCumulativeWeightMap + ) + { + var chosenDiscounts = new Dictionary<ProtoId<DiscountCategoryPrototype>, int>(); + for (var i = 0; i < totalAvailableDiscounts; i++) + { + var discountCategory = categoriesWithCumulativeWeightMap.RollCategory(_random); + if (discountCategory == null) + { + break; + } + + // * if category was not previously picked - we mark it as picked 1 time + // * if category was previously picked - we increment its 'picked' marker + // * if category 'picked' marker going to exceed limit on category - we need to remove IT from further rolls + int newDiscountCount; + if (!chosenDiscounts.TryGetValue(discountCategory.ID, out var alreadySelectedCount)) + { + newDiscountCount = 1; + } + else + { + newDiscountCount = alreadySelectedCount + 1; + } + chosenDiscounts[discountCategory.ID] = newDiscountCount; + + if (newDiscountCount >= discountCategory.MaxItems) + { + categoriesWithCumulativeWeightMap.Remove(discountCategory); + } + } + + return chosenDiscounts; + } + + /// <summary> + /// Rolls list of exact <see cref="ListingData"/> items to be discounted, and amount of currency to be discounted. + /// </summary> + /// <param name="listings">List of all available listing items from which discounted ones could be selected.</param> + /// <param name="chosenDiscounts"></param> + /// <returns>Collection of containers with rolled discount data.</returns> + private IReadOnlyList<StoreDiscountData> RollItems(IEnumerable<ListingDataWithCostModifiers> listings, Dictionary<ProtoId<DiscountCategoryPrototype>, int> chosenDiscounts) + { + // To roll for discounts on items we: pick listing items that have values inside 'DiscountDownTo'. + // then we iterate over discount categories that were chosen on previous step and pick unique set + // of items from that exact category. Then we roll for their cost: + // cost could be anything between DiscountDownTo value and actual item cost. + + var listingsByDiscountCategory = GroupDiscountableListingsByDiscountCategory(listings); + + var list = new List<StoreDiscountData>(); + foreach (var (discountCategory, itemsCount) in chosenDiscounts) + { + if (!listingsByDiscountCategory.TryGetValue(discountCategory, out var itemsForDiscount)) + { + continue; + } + + var chosen = _random.GetItems(itemsForDiscount, itemsCount, allowDuplicates: false); + foreach (var listingData in chosen) + { + var cost = listingData.OriginalCost; + var discountAmountByCurrencyId = RollItemCost(cost, listingData); + + var discountData = new StoreDiscountData + { + ListingId = listingData.ID, + Count = 1, + DiscountCategory = listingData.DiscountCategory!.Value, + DiscountAmountByCurrency = discountAmountByCurrencyId + }; + list.Add(discountData); + } + } + + return list; + } + + /// <summary> Roll amount of each currency by which item cost should be reduced. </summary> + /// <remarks> + /// No point in confusing user with a fractional number, so we remove numbers after dot that were rolled. + /// </remarks> + private Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> RollItemCost( + IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> originalCost, + ListingDataWithCostModifiers listingData + ) + { + var discountAmountByCurrencyId = new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(originalCost.Count); + foreach (var (currency, amount) in originalCost) + { + if (!listingData.DiscountDownTo.TryGetValue(currency, out var discountUntilValue)) + { + continue; + } + + var discountUntilRolledValue = _random.NextDouble(discountUntilValue.Double(), amount.Double()); + var discountedCost = amount - Math.Floor(discountUntilRolledValue); + + // discount is negative modifier for cost + discountAmountByCurrencyId.Add(currency.Id, -discountedCost); + } + + return discountAmountByCurrencyId; + } + + private void ApplyDiscounts(IReadOnlyList<ListingDataWithCostModifiers> listings, IReadOnlyCollection<StoreDiscountData> discounts) + { + foreach (var discountData in discounts) + { + if (discountData.Count <= 0) + { + continue; + } + + ListingDataWithCostModifiers? found = null; + for (var i = 0; i < listings.Count; i++) + { + var current = listings[i]; + if (current.ID == discountData.ListingId) + { + found = current; + break; + } + } + + if (found == null) + { + Log.Warning($"Attempted to apply discount to listing item with {discountData.ListingId}, but found no such listing item."); + return; + } + + found.AddCostModifier(discountData.DiscountCategory, discountData.DiscountAmountByCurrency); + found.Categories.Add(DiscountedStoreCategoryPrototypeKey); + } + } + + private static Dictionary<ProtoId<DiscountCategoryPrototype>, List<ListingDataWithCostModifiers>> GroupDiscountableListingsByDiscountCategory( + IEnumerable<ListingDataWithCostModifiers> listings + ) + { + var listingsByDiscountCategory = new Dictionary<ProtoId<DiscountCategoryPrototype>, List<ListingDataWithCostModifiers>>(); + foreach (var listing in listings) + { + var category = listing.DiscountCategory; + if (category == null || listing.DiscountDownTo.Count == 0) + { + continue; + } + + if (!listingsByDiscountCategory.TryGetValue(category.Value, out var list)) + { + list = new List<ListingDataWithCostModifiers>(); + listingsByDiscountCategory[category.Value] = list; + } + + list.Add(listing); + } + + return listingsByDiscountCategory; + } + + private static bool TryGetDiscountData( + IReadOnlyList<StoreDiscountData> discounts, + ListingDataWithCostModifiers purchasedItem, + [MaybeNullWhen(false)] out StoreDiscountData discountData + ) + { + for (var i = 0; i < discounts.Count; i++) + { + var current = discounts[i]; + if (current.ListingId == purchasedItem.ID) + { + discountData = current; + return true; + } + } + + discountData = null!; + return false; + } + + /// <summary> Map for holding discount categories with their calculated cumulative weight. </summary> + private sealed record CategoriesWithCumulativeWeightMap + { + private readonly List<DiscountCategoryPrototype> _categories; + private readonly List<int> _weights; + private int _totalWeight; + + /// <summary> + /// Creates map, filtering out categories that could not be picked (no weight, no max items). + /// Calculates cumulative weights by summing each next category weight with sum of all previous ones. + /// </summary> + public CategoriesWithCumulativeWeightMap(IEnumerable<DiscountCategoryPrototype> prototypes) + { + var asArray = prototypes.ToArray(); + _weights = new (asArray.Length); + _categories = new(asArray.Length); + + var currentIndex = 0; + _totalWeight = 0; + for (var i = 0; i < asArray.Length; i++) + { + var category = asArray[i]; + if (category.MaxItems <= 0 || category.Weight <= 0) + { + continue; + } + + _categories.Add(category); + + if (currentIndex == 0) + { + _totalWeight = category.Weight; + } + else + { + // cumulative weight of last discount category is total weight of all categories + _totalWeight += category.Weight; + } + _weights.Add(_totalWeight); + + currentIndex++; + } + } + + /// <summary> + /// Removes category and all of its effects on other items in map: + /// decreases cumulativeWeight of every category that is following current one, and then + /// reduces total cumulative count by that category weight, so it won't affect next rolls in any way. + /// </summary> + public void Remove(DiscountCategoryPrototype discountCategory) + { + var indexToRemove = _categories.IndexOf(discountCategory); + if (indexToRemove == -1) + { + return; + } + + for (var i = indexToRemove + 1; i < _categories.Count; i++) + { + _weights[i]-= discountCategory.Weight; + } + + _totalWeight -= discountCategory.Weight; + _categories.RemoveAt(indexToRemove); + _weights.RemoveAt(indexToRemove); + } + + /// <summary> + /// Roll category respecting categories weight. + /// </summary> + /// <remarks> + /// We rolled random point inside range of 0 and 'total weight' to pick category respecting category weights + /// now we find index of category we rolled. If category cumulative weight is less than roll - + /// we rolled other category, skip and try next. + /// </remarks> + /// <param name="random">Random number generator.</param> + /// <returns>Rolled category, or null if no category could be picked based on current map state.</returns> + public DiscountCategoryPrototype? RollCategory(IRobustRandom random) + { + var roll = random.Next(_totalWeight); + for (int i = 0; i < _weights.Count; i++) + { + if (roll < _weights[i]) + { + return _categories[i]; + } + } + + return null; + } + } +} + +/// <summary> +/// Event of store being initialized. +/// </summary> +/// <param name="TargetUser">EntityUid of store entity owner.</param> +/// <param name="Store">EntityUid of store entity.</param> +/// <param name="UseDiscounts">Marker, if store should have discounts.</param> +/// <param name="Listings">List of available listings items.</param> +[ByRefEvent] +public record struct StoreInitializedEvent( + EntityUid TargetUser, + EntityUid Store, + bool UseDiscounts, + IReadOnlyList<ListingDataWithCostModifiers> Listings +); diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 73f2ce17a66..64e96e1467e 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -263,8 +263,7 @@ private void StartStripInsertInventory( Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true, DuplicateCondition = DuplicateConditions.SameTool }; @@ -359,8 +358,7 @@ private void StartStripRemoveInventory( Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true, BreakOnHandChange = false, // Allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool @@ -459,8 +457,7 @@ private void StartStripInsertHand( Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true, DuplicateCondition = DuplicateConditions.SameTool }; @@ -561,8 +558,7 @@ private void StartStripRemoveHand( Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true, BreakOnHandChange = false, // Allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool diff --git a/Content.Server/Stunnable/Systems/StunbatonSystem.cs b/Content.Server/Stunnable/Systems/StunbatonSystem.cs index c1782efabaf..97dd2c7e735 100644 --- a/Content.Server/Stunnable/Systems/StunbatonSystem.cs +++ b/Content.Server/Stunnable/Systems/StunbatonSystem.cs @@ -19,7 +19,7 @@ public sealed class StunbatonSystem : SharedStunbatonSystem [Dependency] private readonly RiggableSystem _riggableSystem = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly BatterySystem _battery = default!; - [Dependency] private readonly SharedItemToggleSystem _itemToggle = default!; + [Dependency] private readonly ItemToggleSystem _itemToggle = default!; public override void Initialize() { diff --git a/Content.Server/Supermatter/Systems/SupermatterSystem.cs b/Content.Server/Supermatter/Systems/SupermatterSystem.cs index 3d86f57fb84..24039a50c2b 100644 --- a/Content.Server/Supermatter/Systems/SupermatterSystem.cs +++ b/Content.Server/Supermatter/Systems/SupermatterSystem.cs @@ -176,8 +176,7 @@ private void OnItemInteract(EntityUid uid, SupermatterComponent sm, ref Interact { BreakOnDamage = true, BreakOnHandChange = false, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnWeightlessMove = false, NeedHand = true, RequireCanInteract = true, diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs index f411001bd3a..a01f19c9090 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Server.Speech; using Content.Server.Speech.Components; +using Content.Shared.Whitelist; using Robust.Shared.Player; using static Content.Server.Chat.Systems.ChatSystem; @@ -9,7 +10,7 @@ namespace Content.Server.SurveillanceCamera; public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem { [Dependency] private readonly SharedTransformSystem _xforms = default!; - + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { base.Initialize(); @@ -60,7 +61,7 @@ private void OnInit(EntityUid uid, SurveillanceCameraMicrophoneComponent compone public void CanListen(EntityUid uid, SurveillanceCameraMicrophoneComponent microphone, ListenAttemptEvent args) { // TODO maybe just make this a part of ActiveListenerComponent? - if (microphone.Blacklist.IsValid(args.Source)) + if (_whitelistSystem.IsBlacklistPass(microphone.Blacklist, args.Source)) args.Cancel(); } diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs index ca0f59cd14d..5cc3c52be45 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs @@ -3,6 +3,7 @@ using Content.Server.DeviceNetwork.Systems; using Content.Server.Power.Components; using Content.Shared.DeviceNetwork; +using Content.Shared.Power; using Content.Shared.UserInterface; using Content.Shared.SurveillanceCamera; using Robust.Server.GameObjects; diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraRouterSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraRouterSystem.cs index d0c2cd78d32..315273a0cc4 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraRouterSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraRouterSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Power.Components; using Content.Shared.ActionBlocker; using Content.Shared.DeviceNetwork; +using Content.Shared.Power; using Content.Shared.SurveillanceCamera; using Content.Shared.Verbs; using Robust.Server.GameObjects; diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs index 22af959fbd2..581ac197197 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Chat.Systems; using Content.Server.Speech; -using Content.Shared.Chat; using Content.Shared.Speech; +using Content.Shared.Chat; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; @@ -45,11 +45,11 @@ private void OnSpeechSent(EntityUid uid, SurveillanceCameraSpeakerComponent comp component.LastSoundPlayed = time; } - var nameEv = new TransformSpeakerSpeechEvent(args.Speaker, Name(args.Speaker)); + var nameEv = new TransformSpeakerNameEvent(args.Speaker, Name(args.Speaker)); RaiseLocalEvent(args.Speaker, nameEv); var name = Loc.GetString("speech-name-relay", ("speaker", Name(uid)), - ("originalName", nameEv.VoiceName ?? Name(uid))); + ("originalName", nameEv.VoiceName)); // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios _chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, ChatTransmitRange.GhostRangeLimit, nameOverride: name); diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs index accaa75d1c3..f84039276f4 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Power.Components; using Content.Shared.ActionBlocker; using Content.Shared.DeviceNetwork; +using Content.Shared.Power; using Content.Shared.SurveillanceCamera; using Content.Shared.Verbs; using Robust.Server.GameObjects; diff --git a/Content.Server/Teleportation/HandTeleporterSystem.cs b/Content.Server/Teleportation/HandTeleporterSystem.cs index 29cde5d741d..3d988b09161 100644 --- a/Content.Server/Teleportation/HandTeleporterSystem.cs +++ b/Content.Server/Teleportation/HandTeleporterSystem.cs @@ -58,7 +58,7 @@ private void OnUseInHand(EntityUid uid, HandTeleporterComponent component, UseIn var doafterArgs = new DoAfterArgs(EntityManager, args.User, component.PortalCreationDelay, new TeleporterDoAfterEvent(), uid, used: uid) { BreakOnDamage = true, - BreakOnUserMove = true, + BreakOnMove = true, MovementThreshold = 0.5f, }; diff --git a/Content.Server/Temperature/Systems/EntityHeaterSystem.cs b/Content.Server/Temperature/Systems/EntityHeaterSystem.cs index 6da774ba076..cf86400b0ea 100644 --- a/Content.Server/Temperature/Systems/EntityHeaterSystem.cs +++ b/Content.Server/Temperature/Systems/EntityHeaterSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Examine; using Content.Shared.Placeable; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Temperature; using Content.Shared.Verbs; diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs index 2f4497bdbbc..ee13cb96a5e 100644 --- a/Content.Server/Temperature/Systems/TemperatureSystem.cs +++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs @@ -126,7 +126,7 @@ public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureCompone public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, TemperatureComponent? temperature = null) { - if (!Resolve(uid, ref temperature)) + if (!Resolve(uid, ref temperature, false)) return; if (!ignoreHeatResistance) diff --git a/Content.Server/Tiles/LavaComponent.cs b/Content.Server/Tiles/LavaComponent.cs deleted file mode 100644 index 1f36e8ffb05..00000000000 --- a/Content.Server/Tiles/LavaComponent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.GameStates; - -namespace Content.Server.Tiles; - -/// <summary> -/// Applies flammable and damage while vaulting. -/// </summary> -[RegisterComponent, Access(typeof(LavaSystem))] -public sealed partial class LavaComponent : Component -{ - /// <summary> - /// Sound played if something disintegrates in lava. - /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("soundDisintegration")] - public SoundSpecifier DisintegrationSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg"); - - /// <summary> - /// How many fire stacks are applied per second. - /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("fireStacks")] - public float FireStacks = 1.25f; -} diff --git a/Content.Server/Tiles/LavaSystem.cs b/Content.Server/Tiles/LavaSystem.cs deleted file mode 100644 index 51bd6f8475f..00000000000 --- a/Content.Server/Tiles/LavaSystem.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Shared.StepTrigger.Systems; - -namespace Content.Server.Tiles; - -public sealed class LavaSystem : EntitySystem -{ - [Dependency] private readonly FlammableSystem _flammable = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent<LavaComponent, StepTriggeredOffEvent>(OnLavaStepTriggered); - SubscribeLocalEvent<LavaComponent, StepTriggerAttemptEvent>(OnLavaStepTriggerAttempt); - } - - private void OnLavaStepTriggerAttempt(EntityUid uid, LavaComponent component, ref StepTriggerAttemptEvent args) - { - if (!HasComp<FlammableComponent>(args.Tripper)) - return; - - args.Continue = true; - } - - private void OnLavaStepTriggered(EntityUid uid, LavaComponent component, ref StepTriggeredOffEvent args) - { - var otherUid = args.Tripper; - - if (TryComp<FlammableComponent>(otherUid, out var flammable)) - { - // Apply the fury of a thousand suns - var multiplier = flammable.FireStacks == 0f ? 5f : 1f; - _flammable.AdjustFireStacks(otherUid, component.FireStacks * multiplier, flammable); - _flammable.Ignite(otherUid, uid, flammable); - } - } -} diff --git a/Content.Server/Tiles/TileEntityEffectComponent.cs b/Content.Server/Tiles/TileEntityEffectComponent.cs new file mode 100644 index 00000000000..4201af47f9e --- /dev/null +++ b/Content.Server/Tiles/TileEntityEffectComponent.cs @@ -0,0 +1,19 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Server.Tiles; + +/// <summary> +/// Applies effects upon stepping onto a tile. +/// </summary> +[RegisterComponent, Access(typeof(TileEntityEffectSystem))] +public sealed partial class TileEntityEffectComponent : Component +{ + /// <summary> + /// List of effects that should be applied. + /// </summary> + [ViewVariables(VVAccess.ReadWrite), DataField] + public List<EntityEffect> Effects = default!; +} diff --git a/Content.Server/Tiles/TileEntityEffectSystem.cs b/Content.Server/Tiles/TileEntityEffectSystem.cs new file mode 100644 index 00000000000..7149f16e1ad --- /dev/null +++ b/Content.Server/Tiles/TileEntityEffectSystem.cs @@ -0,0 +1,32 @@ +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.StepTrigger.Systems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; + +namespace Content.Server.Tiles; + +public sealed class TileEntityEffectSystem : EntitySystem +{ + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<TileEntityEffectComponent, StepTriggeredOffEvent>(OnTileStepTriggered); + SubscribeLocalEvent<TileEntityEffectComponent, StepTriggerAttemptEvent>(OnTileStepTriggerAttempt); + } + private void OnTileStepTriggerAttempt(Entity<TileEntityEffectComponent> ent, ref StepTriggerAttemptEvent args) + { + args.Continue = true; + } + + private void OnTileStepTriggered(Entity<TileEntityEffectComponent> ent, ref StepTriggeredOffEvent args) + { + var otherUid = args.Tripper; + + foreach (var effect in ent.Comp.Effects) + { + effect.Effect(new EntityEffectBaseArgs(otherUid, EntityManager)); + } + } +} diff --git a/Content.Server/Toolshed/Commands/AdminDebug/ACmdCommand.cs b/Content.Server/Toolshed/Commands/AdminDebug/ACmdCommand.cs index f113e496555..ff110a9d9ed 100644 --- a/Content.Server/Toolshed/Commands/AdminDebug/ACmdCommand.cs +++ b/Content.Server/Toolshed/Commands/AdminDebug/ACmdCommand.cs @@ -22,13 +22,9 @@ public sealed class ACmdCommand : ToolshedCommand } [CommandImplementation("caninvoke")] - public bool CanInvoke( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] CommandSpec command, - [CommandArgument] ValueRef<ICommonSession> player - ) + public bool CanInvoke(IInvocationContext ctx, [PipedArgument] CommandSpec command, ICommonSession player) { // Deliberately discard the error. - return ((IPermissionController) _adminManager).CheckInvokable(command, player.Evaluate(ctx), out var err); + return ((IPermissionController) _adminManager).CheckInvokable(command, player, out _); } } diff --git a/Content.Server/Toolshed/Commands/Verbs/RunVerbAsCommand.cs b/Content.Server/Toolshed/Commands/Verbs/RunVerbAsCommand.cs index 1b11dffeea7..e74bdf3b6b6 100644 --- a/Content.Server/Toolshed/Commands/Verbs/RunVerbAsCommand.cs +++ b/Content.Server/Toolshed/Commands/Verbs/RunVerbAsCommand.cs @@ -15,10 +15,10 @@ public sealed class RunVerbAsCommand : ToolshedCommand [CommandImplementation] public IEnumerable<NetEntity> RunVerbAs( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable<NetEntity> input, - [CommandArgument] ValueRef<NetEntity> runner, - [CommandArgument] string verb + EntityUid runner, + string verb ) { _verb ??= GetSys<SharedVerbSystem>(); @@ -26,17 +26,14 @@ [CommandArgument] string verb foreach (var i in input) { - var runnerNet = runner.Evaluate(ctx); - var runnerEid = EntityManager.GetEntity(runnerNet); - - if (EntityManager.Deleted(runnerEid) && runnerEid.IsValid()) - ctx.ReportError(new DeadEntity(runnerEid)); + if (EntityManager.Deleted(runner) && runner.IsValid()) + ctx.ReportError(new DeadEntity(runner)); if (ctx.GetErrors().Any()) yield break; var eId = EntityManager.GetEntity(i); - var verbs = _verb.GetLocalVerbs(eId, runnerEid, Verb.VerbTypes, true); + var verbs = _verb.GetLocalVerbs(eId, runner, Verb.VerbTypes, true); // if the "verb name" is actually a verb-type, try run any verb of that type. var verbType = Verb.VerbTypes.FirstOrDefault(x => x.Name == verb); @@ -45,7 +42,7 @@ [CommandArgument] string verb var verbTy = verbs.FirstOrDefault(v => v.GetType() == verbType); if (verbTy != null) { - _verb.ExecuteVerb(verbTy, runnerEid, eId, forced: true); + _verb.ExecuteVerb(verbTy, runner, eId, forced: true); yield return i; } } @@ -54,7 +51,7 @@ [CommandArgument] string verb { if (verbTy.Text.ToLowerInvariant() == verb) { - _verb.ExecuteVerb(verbTy, runnerEid, eId, forced: true); + _verb.ExecuteVerb(verbTy, runner, eId, forced: true); yield return i; } } diff --git a/Content.Server/Toolshed/Commands/VisualizeCommand.cs b/Content.Server/Toolshed/Commands/VisualizeCommand.cs index 74fd4cf844a..73bef536d18 100644 --- a/Content.Server/Toolshed/Commands/VisualizeCommand.cs +++ b/Content.Server/Toolshed/Commands/VisualizeCommand.cs @@ -16,7 +16,7 @@ public sealed class VisualizeCommand : ToolshedCommand [CommandImplementation] public void VisualizeEntities( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable<EntityUid> input ) { diff --git a/Content.Server/Traitor/Components/AutoTraitorComponent.cs b/Content.Server/Traitor/Components/AutoTraitorComponent.cs index 473441ccec2..a4710afd8eb 100644 --- a/Content.Server/Traitor/Components/AutoTraitorComponent.cs +++ b/Content.Server/Traitor/Components/AutoTraitorComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Traitor.Systems; +using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Components; @@ -9,14 +10,8 @@ namespace Content.Server.Traitor.Components; public sealed partial class AutoTraitorComponent : Component { /// <summary> - /// Whether to give the traitor an uplink or not. + /// The traitor profile to use /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] - public bool GiveUplink = true; - - /// <summary> - /// Whether to give the traitor objectives or not. - /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] - public bool GiveObjectives = true; + [DataField] + public EntProtoId Profile = "Traitor"; } diff --git a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs index e9307effbc6..d5a4db591a7 100644 --- a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs +++ b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs @@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem { [Dependency] private readonly AntagSelectionSystem _antag = default!; - [ValidatePrototypeId<EntityPrototype>] - private const string DefaultTraitorRule = "Traitor"; - public override void Initialize() { base.Initialize(); @@ -24,6 +21,6 @@ public override void Initialize() private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args) { - _antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule); + _antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, comp.Profile); } } diff --git a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs index 63be36b360a..f5fde87c11f 100644 --- a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs +++ b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs @@ -28,13 +28,14 @@ public CompletionResult GetCompletion(IConsoleShell shell, string[] args) { 1 => CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("add-uplink-command-completion-1")), 2 => CompletionResult.FromHint(Loc.GetString("add-uplink-command-completion-2")), + 3 => CompletionResult.FromHint(Loc.GetString("add-uplink-command-completion-3")), _ => CompletionResult.Empty }; } public void Execute(IConsoleShell shell, string argStr, string[] args) { - if (args.Length > 2) + if (args.Length > 3) { shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); return; @@ -82,9 +83,19 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) uplinkEntity = eUid; } + bool isDiscounted = false; + if (args.Length >= 3) + { + if (!bool.TryParse(args[2], out isDiscounted)) + { + shell.WriteLine(Loc.GetString("shell-invalid-bool")); + return; + } + } + // Finally add uplink var uplinkSys = _entManager.System<UplinkSystem>(); - if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity)) + if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted)) { shell.WriteLine(Loc.GetString("add-uplink-command-error-2")); } diff --git a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs index 47ce68625a1..120581cf82d 100644 --- a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs +++ b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs @@ -1,6 +1,3 @@ -using Content.Shared.Store; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - namespace Content.Server.Traitor.Uplink.SurplusBundle; /// <summary> @@ -12,14 +9,6 @@ public sealed partial class SurplusBundleComponent : Component /// <summary> /// Total price of all content inside bundle. /// </summary> - [ViewVariables(VVAccess.ReadOnly)] - [DataField("totalPrice")] + [DataField] public int TotalPrice = 20; - - /// <summary> - /// The preset that will be used to get all the listings. - /// Currently just defaults to the basic uplink. - /// </summary> - [DataField("storePreset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))] - public string StorePreset = "StorePresetUplink"; } diff --git a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs index 5c0a56d346c..759cad5deda 100644 --- a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs +++ b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs @@ -3,76 +3,67 @@ using Content.Server.Store.Systems; using Content.Shared.FixedPoint; using Content.Shared.Store; -using Robust.Shared.Prototypes; +using Content.Shared.Store.Components; using Robust.Shared.Random; namespace Content.Server.Traitor.Uplink.SurplusBundle; public sealed class SurplusBundleSystem : EntitySystem { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EntityStorageSystem _entityStorage = default!; [Dependency] private readonly StoreSystem _store = default!; - private ListingData[] _listings = default!; - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<SurplusBundleComponent, MapInitEvent>(OnMapInit); - - SubscribeLocalEvent<SurplusBundleComponent, ComponentInit>(OnInit); - } - private void OnInit(EntityUid uid, SurplusBundleComponent component, ComponentInit args) - { - var storePreset = _prototypeManager.Index<StorePresetPrototype>(component.StorePreset); - - _listings = _store.GetAvailableListings(uid, null, storePreset.Categories).ToArray(); - - Array.Sort(_listings, (a, b) => (int) (b.Cost.Values.Sum() - a.Cost.Values.Sum())); //this might get weird with multicurrency but don't think about it + SubscribeLocalEvent<SurplusBundleComponent, MapInitEvent>(OnMapInit); } private void OnMapInit(EntityUid uid, SurplusBundleComponent component, MapInitEvent args) { - FillStorage(uid, component); - } - - private void FillStorage(EntityUid uid, SurplusBundleComponent? component = null) - { - if (!Resolve(uid, ref component)) + if (!TryComp<StoreComponent>(uid, out var store)) return; - var cords = Transform(uid).Coordinates; + FillStorage((uid, component, store)); + } - var content = GetRandomContent(component.TotalPrice); + private void FillStorage(Entity<SurplusBundleComponent, StoreComponent> ent) + { + var cords = Transform(ent).Coordinates; + var content = GetRandomContent(ent); foreach (var item in content) { - var ent = EntityManager.SpawnEntity(item.ProductEntity, cords); - _entityStorage.Insert(ent, uid); + var dode = Spawn(item.ProductEntity, cords); + _entityStorage.Insert(dode, ent); } } // wow, is this leetcode reference? - private List<ListingData> GetRandomContent(FixedPoint2 targetCost) + private List<ListingData> GetRandomContent(Entity<SurplusBundleComponent, StoreComponent> ent) { var ret = new List<ListingData>(); - if (_listings.Length == 0) + + var listings = _store.GetAvailableListings(ent, null, ent.Comp2.Categories) + .OrderBy(p => p.Cost.Values.Sum()) + .ToList(); + + if (listings.Count == 0) return ret; var totalCost = FixedPoint2.Zero; var index = 0; - while (totalCost < targetCost) + while (totalCost < ent.Comp1.TotalPrice) { // All data is sorted in price descending order // Find new item with the lowest acceptable price // All expansive items will be before index, all acceptable after - var remainingBudget = targetCost - totalCost; - while (_listings[index].Cost.Values.Sum() > remainingBudget) + var remainingBudget = ent.Comp1.TotalPrice - totalCost; + while (listings[index].Cost.Values.Sum() > remainingBudget) { index++; - if (index >= _listings.Length) + if (index >= listings.Count) { // Looks like no cheap items left // It shouldn't be case for ss14 content @@ -82,8 +73,8 @@ private List<ListingData> GetRandomContent(FixedPoint2 targetCost) } // Select random listing and add into crate - var randomIndex = _random.Next(index, _listings.Length); - var randomItem = _listings[randomIndex]; + var randomIndex = _random.Next(index, listings.Count); + var randomItem = listings[randomIndex]; ret.Add(randomItem); totalCost += randomItem.Cost.Values.Sum(); } diff --git a/Content.Server/Traitor/Uplink/UplinkComponent.cs b/Content.Server/Traitor/Uplink/UplinkComponent.cs new file mode 100644 index 00000000000..35f11ce9ef7 --- /dev/null +++ b/Content.Server/Traitor/Uplink/UplinkComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Traitor.Uplink; + +/// <summary> +/// This is used for identifying something as a hidden uplink and showing the UI. +/// </summary> +[RegisterComponent] +public sealed partial class UplinkComponent : Component; diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index 5670e28ec99..4cdaaa2a8d6 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -1,94 +1,134 @@ +using System.Linq; using Content.Server.Store.Systems; +using Content.Server.StoreDiscount.Systems; +using Content.Shared.FixedPoint; using Content.Shared.Hands.EntitySystems; +using Content.Shared.Implants; using Content.Shared.Inventory; using Content.Shared.PDA; -using Content.Server.Store.Components; -using Content.Shared.FixedPoint; using Content.Shared.Store; +using Content.Shared.Store.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.Traitor.Uplink; -namespace Content.Server.Traitor.Uplink +public sealed class UplinkSystem : EntitySystem { - public sealed class UplinkSystem : EntitySystem + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly StoreSystem _store = default!; + [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!; + + [ValidatePrototypeId<CurrencyPrototype>] + public const string TelecrystalCurrencyPrototype = "Telecrystal"; + private const string FallbackUplinkImplant = "UplinkImplant"; + private const string FallbackUplinkCatalog = "UplinkUplinkImplanter"; + + /// <summary> + /// Adds an uplink to the target + /// </summary> + /// <param name="user">The person who is getting the uplink</param> + /// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param> + /// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param> + /// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param> + /// <returns>Whether or not the uplink was added successfully</returns> + public bool AddUplink( + EntityUid user, + FixedPoint2 balance, + EntityUid? uplinkEntity = null, + bool giveDiscounts = false) { - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly StoreSystem _store = default!; - - [ValidatePrototypeId<CurrencyPrototype>] - public const string TelecrystalCurrencyPrototype = "Telecrystal"; - - /// <summary> - /// Gets the amount of TC on an "uplink" - /// Mostly just here for legacy systems based on uplink. - /// </summary> - /// <param name="component"></param> - /// <returns>the amount of TC</returns> - public int GetTCBalance(StoreComponent component) - { - FixedPoint2? tcBalance = component.Balance.GetValueOrDefault(TelecrystalCurrencyPrototype); - return tcBalance?.Int() ?? 0; - } + // Try to find target item if none passed - /// <summary> - /// Adds an uplink to the target - /// </summary> - /// <param name="user">The person who is getting the uplink</param> - /// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param> - /// <param name="uplinkPresetId">The id of the storepreset</param> - /// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param> - /// <returns>Whether or not the uplink was added successfully</returns> - public bool AddUplink(EntityUid user, FixedPoint2? balance, string uplinkPresetId = "StorePresetUplink", EntityUid? uplinkEntity = null) - { - // Try to find target item - if (uplinkEntity == null) - { - uplinkEntity = FindUplinkTarget(user); - if (uplinkEntity == null) - return false; - } + uplinkEntity ??= FindUplinkTarget(user); - var store = EnsureComp<StoreComponent>(uplinkEntity.Value); - _store.InitializeFromPreset(uplinkPresetId, uplinkEntity.Value, store); - store.AccountOwner = user; - store.Balance.Clear(); + if (uplinkEntity == null) + return ImplantUplink(user, balance, giveDiscounts); - if (balance != null) - { - store.Balance.Clear(); - _store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store); - } + EnsureComp<UplinkComponent>(uplinkEntity.Value); - // TODO add BUI. Currently can't be done outside of yaml -_- + SetUplink(user, uplinkEntity.Value, balance, giveDiscounts); - return true; - } + // TODO add BUI. Currently can't be done outside of yaml -_- + // ^ What does this even mean? + + return true; + } + + /// <summary> + /// Configure TC for the uplink + /// </summary> + private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts) + { + var store = EnsureComp<StoreComponent>(uplink); + store.AccountOwner = user; - /// <summary> - /// Finds the entity that can hold an uplink for a user. - /// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.) - /// </summary> - public EntityUid? FindUplinkTarget(EntityUid user) + store.Balance.Clear(); + _store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } }, + uplink, + store); + + var uplinkInitializedEvent = new StoreInitializedEvent( + TargetUser: user, + Store: uplink, + UseDiscounts: giveDiscounts, + Listings: _store.GetAvailableListings(user, uplink, store) + .ToArray()); + RaiseLocalEvent(ref uplinkInitializedEvent); + } + + /// <summary> + /// Implant an uplink as a fallback measure if the traitor had no PDA + /// </summary> + private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts) + { + var implantProto = new string(FallbackUplinkImplant); + + if (!_proto.TryIndex<ListingPrototype>(FallbackUplinkCatalog, out var catalog)) + return false; + + if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost)) + return false; + + if (balance < cost) // Can't use Math functions on FixedPoint2 + balance = 0; + else + balance = balance - cost; + + var implant = _subdermalImplant.AddImplant(user, implantProto); + + if (!HasComp<StoreComponent>(implant)) + return false; + + SetUplink(user, implant.Value, balance, giveDiscounts); + return true; + } + + /// <summary> + /// Finds the entity that can hold an uplink for a user. + /// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.) + /// </summary> + public EntityUid? FindUplinkTarget(EntityUid user) + { + // Try to find PDA in inventory + if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) { - // Try to find PDA in inventory - if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) + while (containerSlotEnumerator.MoveNext(out var pdaUid)) { - while (containerSlotEnumerator.MoveNext(out var pdaUid)) - { - if (!pdaUid.ContainedEntity.HasValue) continue; + if (!pdaUid.ContainedEntity.HasValue) + continue; - if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value)) - return pdaUid.ContainedEntity.Value; - } + if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value)) + return pdaUid.ContainedEntity.Value; } + } - // Also check hands - foreach (var item in _handsSystem.EnumerateHeld(user)) - { - if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item)) - return item; - } + // Also check hands + foreach (var item in _handsSystem.EnumerateHeld(user)) + if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item)) + return item; - return null; - } + return null; } } diff --git a/Content.Server/Traits/TraitSystem.Functions.cs b/Content.Server/Traits/TraitSystem.Functions.cs index 57b508018a6..7ec8b8c54f6 100644 --- a/Content.Server/Traits/TraitSystem.Functions.cs +++ b/Content.Server/Traits/TraitSystem.Functions.cs @@ -9,7 +9,6 @@ using Content.Shared.Psionics; using Content.Server.Language; using Content.Shared.Mood; -using Content.Server.NPC.Systems; using Content.Shared.Traits.Assorted.Components; using Content.Shared.Damage; using Content.Shared.Chemistry.Components; @@ -18,6 +17,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Mobs; using Content.Shared.Damage.Components; +using Content.Shared.NPC.Systems; namespace Content.Server.Traits; diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs index 7a028b381ad..3e0866a2bab 100644 --- a/Content.Server/Traits/TraitSystem.cs +++ b/Content.Server/Traits/TraitSystem.cs @@ -13,6 +13,7 @@ using Content.Shared.Traits; using Robust.Server.Player; using Robust.Shared.Configuration; +using Content.Shared.Whitelist; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; diff --git a/Content.Server/UserInterface/StatValuesCommand.cs b/Content.Server/UserInterface/StatValuesCommand.cs index f0c4f531d01..bbfbb3909ca 100644 --- a/Content.Server/UserInterface/StatValuesCommand.cs +++ b/Content.Server/UserInterface/StatValuesCommand.cs @@ -6,7 +6,6 @@ using Content.Server.Item; using Content.Shared.Administration; using Content.Shared.Item; -using Content.Shared.Materials; using Content.Shared.Research.Prototypes; using Content.Shared.UserInterface; using Content.Shared.Weapons.Melee; @@ -220,13 +219,13 @@ private StatValuesEuiMessage GetLatheMessage() { var cost = 0.0; - foreach (var (material, count) in proto.RequiredMaterials) + foreach (var (material, count) in proto.Materials) { - var materialPrice = _proto.Index<MaterialPrototype>(material).Price; + var materialPrice = _proto.Index(material).Price; cost += materialPrice * count; } - var sell = priceSystem.GetEstimatedPrice(_proto.Index<EntityPrototype>(proto.Result)); + var sell = priceSystem.GetLatheRecipePrice(proto); values.Add(new[] { diff --git a/Content.Server/Vampire/BloodSuckerSystem.cs b/Content.Server/Vampire/BloodSuckerSystem.cs index 41afe0d666b..77fae73bd44 100644 --- a/Content.Server/Vampire/BloodSuckerSystem.cs +++ b/Content.Server/Vampire/BloodSuckerSystem.cs @@ -134,8 +134,7 @@ public void StartSuccDoAfter(EntityUid bloodsucker, EntityUid victim, BloodSucke var args = new DoAfterArgs(EntityManager, bloodsucker, bloodSuckerComponent.Delay, new BloodSuckDoAfterEvent(), bloodsucker, target: victim) { - BreakOnTargetMove = true, - BreakOnUserMove = false, + BreakOnMove = false, DistanceThreshold = 2f, NeedHand = false }; diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs index 60be1ad6f1b..6b233cd43f1 100644 --- a/Content.Server/VendingMachines/VendingMachineSystem.cs +++ b/Content.Server/VendingMachines/VendingMachineSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.Emag.Systems; using Content.Shared.Emp; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Throwing; using Content.Shared.UserInterface; using Content.Shared.VendingMachines; diff --git a/Content.Server/VoiceMask/VoiceMaskSystem.cs b/Content.Server/VoiceMask/VoiceMaskSystem.cs index 98f6b18f537..47ea98d2ccf 100644 --- a/Content.Server/VoiceMask/VoiceMaskSystem.cs +++ b/Content.Server/VoiceMask/VoiceMaskSystem.cs @@ -3,7 +3,6 @@ using Content.Shared.Chat; using Content.Shared.Clothing; using Content.Shared.Database; -using Content.Shared.Implants; using Content.Shared.Inventory; using Content.Shared.Popups; using Content.Shared.Preferences; @@ -24,26 +23,19 @@ public sealed partial class VoiceMaskSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerSpeechEvent>>(OnTransformSpeakerName); - SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerSpeechEvent>>(OnTransformSpeakerNameImplant); + SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName); SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName); SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb); SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip); SubscribeLocalEvent<VoiceMaskSetNameEvent>(OpenUI); } - private void OnTransformSpeakerName(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerSpeechEvent> args) + private void OnTransformSpeakerName(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args) { args.Args.VoiceName = GetCurrentVoiceName(entity); args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb; } - private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerSpeechEvent> args) - { - args.Event.VoiceName = GetCurrentVoiceName(entity); - args.Event.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Event.SpeechVerb; - } - #region User inputs from UI private void OnChangeVerb(Entity<VoiceMaskComponent> entity, ref VoiceMaskChangeVerbMessage msg) { @@ -83,11 +75,7 @@ private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEqu private void OpenUI(VoiceMaskSetNameEvent ev) { - // If this ever becomes an entity remove this! - if (!TryComp<InstantActionComponent>(ev.Action, out var actionComp)) - return; - - var maskEntity = actionComp.Container; + var maskEntity = ev.Action.Comp.Container; if (!TryComp<VoiceMaskComponent>(maskEntity, out var voiceMaskComp)) return; diff --git a/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs index e8897781f5e..5970e163196 100644 --- a/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs +++ b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs @@ -2,8 +2,7 @@ using Content.Shared.Light; using Content.Shared.Light.Components; using Content.Shared.Toggleable; -using Content.Shared.Tools.Components; -using Content.Shared.Item; +using Content.Shared.Tools.Systems; using Robust.Shared.Random; namespace Content.Server.Weapons.Melee.EnergySword; @@ -13,6 +12,7 @@ public sealed class EnergySwordSystem : EntitySystem [Dependency] private readonly SharedRgbLightControllerSystem _rgbSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; public override void Initialize() { @@ -38,7 +38,7 @@ private void OnInteractUsing(EntityUid uid, EnergySwordComponent comp, InteractU if (args.Handled) return; - if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.ContainsAny("Pulsing")) + if (!_toolSystem.HasQuality(args.Used, "Pulsing")) return; args.Handled = true; diff --git a/Content.Server/Weapons/Misc/TetherGunSystem.cs b/Content.Server/Weapons/Misc/TetherGunSystem.cs index f6aafe376d6..2bf53d46f4b 100644 --- a/Content.Server/Weapons/Misc/TetherGunSystem.cs +++ b/Content.Server/Weapons/Misc/TetherGunSystem.cs @@ -1,4 +1,5 @@ using Content.Server.PowerCell; +using Content.Shared.Item.ItemToggle; using Content.Shared.PowerCell; using Content.Shared.Weapons.Misc; using Robust.Shared.Physics.Components; @@ -8,6 +9,7 @@ namespace Content.Server.Weapons.Misc; public sealed class TetherGunSystem : SharedTetherGunSystem { [Dependency] private readonly PowerCellSystem _cell = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; public override void Initialize() { @@ -36,12 +38,12 @@ protected override void StartTether(EntityUid gunUid, BaseForceGunComponent comp PhysicsComponent? targetPhysics = null, TransformComponent? targetXform = null) { base.StartTether(gunUid, component, target, user, targetPhysics, targetXform); - _cell.SetPowerCellDrawEnabled(gunUid, true); + _toggle.TryActivate(gunUid); } protected override void StopTether(EntityUid gunUid, BaseForceGunComponent component, bool land = true, bool transfer = false) { base.StopTether(gunUid, component, land, transfer); - _cell.SetPowerCellDrawEnabled(gunUid, false); + _toggle.TryDeactivate(gunUid); } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs index 39cd2486ed7..e5439cdb064 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs @@ -1,4 +1,6 @@ +using Content.Shared.Damage; using Content.Shared.Weapons.Ranged.Components; +using Robust.Shared.Map; namespace Content.Server.Weapons.Ranged.Systems; @@ -13,17 +15,28 @@ public override void Update(float frameTime) */ // Automatic firing without stopping if the AutoShootGunComponent component is exist and enabled - var query = EntityQueryEnumerator<AutoShootGunComponent, GunComponent>(); + var query = EntityQueryEnumerator<GunComponent>(); - while (query.MoveNext(out var uid, out var autoShoot, out var gun)) + while (query.MoveNext(out var uid, out var gun)) { - if (!autoShoot.Enabled) - continue; - if (gun.NextFire > Timing.CurTime) continue; - AttemptShoot(uid, gun); + if (TryComp(uid, out AutoShootGunComponent? autoShoot)) + { + if (!autoShoot.Enabled) + continue; + + AttemptShoot(uid, gun); + } + else if (gun.BurstActivated) + { + var parent = _transform.GetParentUid(uid); + if (HasComp<DamageableComponent>(parent)) + AttemptShoot(parent, uid, gun, gun.ShootCoordinates ?? new EntityCoordinates(uid, gun.DefaultDirection)); + else + AttemptShoot(uid, gun); + } } } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index cbbfc289cf5..d22b5ec2af7 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -1,16 +1,12 @@ using System.Linq; using System.Numerics; using Content.Server.Cargo.Systems; -using Content.Server.Interaction; using Content.Server.Power.EntitySystems; -using Content.Server.Stunnable; using Content.Server.Weapons.Ranged.Components; -using Content.Shared.Contests; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Effects; -using Content.Shared.Interaction.Components; using Content.Shared.Projectiles; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged; @@ -18,12 +14,14 @@ using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Reflect; +using Content.Shared.Damage.Components; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; +using Robust.Shared.Containers; namespace Content.Server.Weapons.Ranged.Systems; @@ -32,16 +30,13 @@ public sealed partial class GunSystem : SharedGunSystem [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!; - [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StaminaSystem _stamina = default!; - [Dependency] private readonly StunSystem _stun = default!; - [Dependency] private readonly ContestsSystem _contests = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; private const float DamagePitchVariation = 0.05f; - public const float GunClumsyChance = 0.5f; public override void Initialize() { @@ -70,26 +65,14 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? { userImpulse = true; - // Try a clumsy roll - // TODO: Who put this here - if (TryComp<ClumsyComponent>(user, out var clumsy) && gun.ClumsyProof == false) + if (user != null) { - for (var i = 0; i < ammo.Count; i++) + var selfEvent = new SelfBeforeGunShotEvent(user.Value, (gunUid, gun), ammo); + RaiseLocalEvent(user.Value, selfEvent); + if (selfEvent.Cancelled) { - if (_interaction.TryRollClumsy(user.Value, GunClumsyChance, clumsy)) - { - // Wound them - Damageable.TryChangeDamage(user, clumsy.ClumsyDamage, origin: user); - _stun.TryParalyze(user.Value, TimeSpan.FromSeconds(3f), true); - - // Apply salt to the wound ("Honk!") - Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"), gunUid); - Audio.PlayPvs(clumsy.ClumsySound, gunUid); - - PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value); - userImpulse = false; - return; - } + userImpulse = false; + return; } } @@ -97,7 +80,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); var mapDirection = toMap - fromMap.Position; var mapAngle = mapDirection.ToAngle(); - var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle(), user); + var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle()); // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out var grid) @@ -128,27 +111,8 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? case CartridgeAmmoComponent cartridge: if (!cartridge.Spent) { - if (cartridge.Count > 1) - { - var ev = new GunGetAmmoSpreadEvent(cartridge.Spread); - RaiseLocalEvent(gunUid, ref ev); - - var angles = LinearSpread(mapAngle - ev.Spread / 2, - mapAngle + ev.Spread / 2, cartridge.Count); - - for (var i = 0; i < cartridge.Count; i++) - { - var uid = Spawn(cartridge.Prototype, fromEnt); - ShootOrThrow(uid, angles[i].ToVec(), gunVelocity, gun, gunUid, user); - shotProjectiles.Add(uid); - } - } - else - { - var uid = Spawn(cartridge.Prototype, fromEnt); - ShootOrThrow(uid, mapDirection, gunVelocity, gun, gunUid, user); - shotProjectiles.Add(uid); - } + var uid = Spawn(cartridge.Prototype, fromEnt); + CreateAndFireProjectiles(uid, cartridge); RaiseLocalEvent(ent!.Value, new AmmoShotEvent() { @@ -156,8 +120,6 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? }); SetCartridgeSpent(ent.Value, cartridge, true); - MuzzleFlash(gunUid, cartridge, mapDirection.ToAngle(), user); - Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); if (cartridge.DeleteOnSpawn) Del(ent.Value); @@ -176,10 +138,10 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? break; // Ammo shoots itself case AmmoComponent newAmmo: - shotProjectiles.Add(ent!.Value); - MuzzleFlash(gunUid, newAmmo, mapDirection.ToAngle(), user); - Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); - ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user); + if (ent == null) + break; + CreateAndFireProjectiles(ent.Value, newAmmo); + break; case HitscanPrototype hitscan: @@ -204,6 +166,24 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? break; var result = rayCastResults[0]; + + // Check if laser is shot from in a container + if (!_container.IsEntityOrParentInContainer(lastUser)) + { + // Checks if the laser should pass over unless targeted by its user + foreach (var collide in rayCastResults) + { + if (collide.HitEntity != gun.Target && + CompOrNull<RequireProjectileTargetComponent>(collide.HitEntity)?.Active == true) + { + continue; + } + + result = collide; + break; + } + } + var hit = result.HitEntity; lastHit = hit; @@ -239,7 +219,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? { if (!Deleted(hitEntity)) { - if (dmg.Any()) + if (dmg.AnyPositive()) { _color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); } @@ -276,6 +256,36 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? { FiredProjectiles = shotProjectiles, }); + + void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp) + { + if (TryComp<ProjectileSpreadComponent>(ammoEnt, out var ammoSpreadComp)) + { + var spreadEvent = new GunGetAmmoSpreadEvent(ammoSpreadComp.Spread); + RaiseLocalEvent(gunUid, ref spreadEvent); + + var angles = LinearSpread(mapAngle - spreadEvent.Spread / 2, + mapAngle + spreadEvent.Spread / 2, ammoSpreadComp.Count); + + ShootOrThrow(ammoEnt, angles[0].ToVec(), gunVelocity, gun, gunUid, user); + shotProjectiles.Add(ammoEnt); + + for (var i = 1; i < ammoSpreadComp.Count; i++) + { + var newuid = Spawn(ammoSpreadComp.Proto, fromEnt); + ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, gunUid, user); + shotProjectiles.Add(newuid); + } + } + else + { + ShootOrThrow(ammoEnt, mapDirection, gunVelocity, gun, gunUid, user); + shotProjectiles.Add(ammoEnt); + } + + MuzzleFlash(gunUid, ammoComp, mapDirection.ToAngle(), user); + Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); + } } private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user) @@ -318,7 +328,7 @@ private Angle[] LinearSpread(Angle start, Angle end, int intervals) return angles; } - private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction, EntityUid? shooter) + private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction) { var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds; var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncreaseModified.Theta - component.AngleDecayModified.Theta * timeSinceLastFire, component.MinAngleModified.Theta, component.MaxAngleModified.Theta); @@ -326,8 +336,7 @@ private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle dir component.LastFire = component.NextFire; // Convert it so angle can go either side. - - var random = Random.NextFloat(-0.5f, 0.5f) / _contests.MassContest(shooter); + var random = Random.NextFloat(-0.5f, 0.5f); var spread = component.CurrentAngle.Theta * random; var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random); DebugTools.Assert(spread <= component.MaxAngleModified.Theta); diff --git a/Content.Server/WhiteDream/BloodCult/BloodRites/BloodRitesSystem.cs b/Content.Server/WhiteDream/BloodCult/BloodRites/BloodRitesSystem.cs index 63b1c08aaca..d22b2ced2e0 100644 --- a/Content.Server/WhiteDream/BloodCult/BloodRites/BloodRitesSystem.cs +++ b/Content.Server/WhiteDream/BloodCult/BloodRites/BloodRitesSystem.cs @@ -77,8 +77,7 @@ private void OnAfterInteract(Entity<BloodRitesAuraComponent> rites, ref AfterInt var time = rites.Comp.BloodExtractionTime; var doAfterArgs = new DoAfterArgs(EntityManager, args.User, time, ev, rites, args.Target) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, BreakOnDamage = true }; if (_doAfter.TryStartDoAfter(doAfterArgs, out var doAfterId)) diff --git a/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleComponent.cs b/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleComponent.cs index d0a1991a9fd..c89a50f4606 100644 --- a/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleComponent.cs +++ b/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleComponent.cs @@ -1,5 +1,4 @@ -using Content.Server.NPC.Components; -using Content.Server.WhiteDream.BloodCult.RendingRunePlacement; +using Content.Shared.NPC.Prototypes; using Content.Shared.WhiteDream.BloodCult.BloodCultist; using Content.Shared.WhiteDream.BloodCult.Constructs; using Robust.Shared.Prototypes; diff --git a/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleSystem.cs b/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleSystem.cs index bfb06c85ccf..a3ec2a9b07a 100644 --- a/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleSystem.cs +++ b/Content.Server/WhiteDream/BloodCult/Gamerule/BloodCultRuleSystem.cs @@ -36,6 +36,8 @@ using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; +using Content.Shared.NPC.Systems; + namespace Content.Server.WhiteDream.BloodCult.Gamerule; diff --git a/Content.Server/WhiteDream/BloodCult/Runes/Apocalypse/CultRuneApocalypseSystem.cs b/Content.Server/WhiteDream/BloodCult/Runes/Apocalypse/CultRuneApocalypseSystem.cs index 745ea042caf..ae72ebf1504 100644 --- a/Content.Server/WhiteDream/BloodCult/Runes/Apocalypse/CultRuneApocalypseSystem.cs +++ b/Content.Server/WhiteDream/BloodCult/Runes/Apocalypse/CultRuneApocalypseSystem.cs @@ -40,7 +40,7 @@ private void OnApocalypseRuneInvoked(Entity<CultRuneApocalypseComponent> ent, re var doAfter = new DoAfterArgs(EntityManager, args.User, ent.Comp.InvokeTime, new ApocalypseRuneDoAfter(), ent) { - BreakOnUserMove = true + BreakOnMove = true }; _doAfter.TryStartDoAfter(doAfter); diff --git a/Content.Server/WhiteDream/BloodCult/Runes/CultRuneBaseSystem.cs b/Content.Server/WhiteDream/BloodCult/Runes/CultRuneBaseSystem.cs index 013b8df6cdd..cedf021d5f1 100644 --- a/Content.Server/WhiteDream/BloodCult/Runes/CultRuneBaseSystem.cs +++ b/Content.Server/WhiteDream/BloodCult/Runes/CultRuneBaseSystem.cs @@ -101,7 +101,7 @@ private void OnRuneSelected(Entity<RuneDrawerComponent> ent, ref RuneDrawerSelec var argsDoAfterEvent = new DoAfterArgs(EntityManager, args.Actor, timeToDraw, ev, args.Actor) { - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }; @@ -147,7 +147,7 @@ private void EraseOnInteractUsing(Entity<CultRuneBaseComponent> rune, ref Intera var argsDoAfterEvent = new DoAfterArgs(EntityManager, args.User, runeDrawer.EraseTime, new RuneEraseDoAfterEvent(), rune) { - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = true }; diff --git a/Content.Server/WhiteDream/BloodCult/Runes/Rending/CultRuneRendingSystem.cs b/Content.Server/WhiteDream/BloodCult/Runes/Rending/CultRuneRendingSystem.cs index 91347bcdd95..6bc69aba58e 100644 --- a/Content.Server/WhiteDream/BloodCult/Runes/Rending/CultRuneRendingSystem.cs +++ b/Content.Server/WhiteDream/BloodCult/Runes/Rending/CultRuneRendingSystem.cs @@ -66,7 +66,7 @@ private void OnRendingRuneInvoked(Entity<CultRuneRendingComponent> rune, ref Try var ev = new RendingRuneDoAfter(); var argsDoAfterEvent = new DoAfterArgs(EntityManager, args.User, rune.Comp.SummonTime, ev, rune) { - BreakOnUserMove = true + BreakOnMove = true }; if (!_doAfter.TryStartDoAfter(argsDoAfterEvent, out rune.Comp.CurrentDoAfter)) diff --git a/Content.Server/WhiteDream/BloodCult/Spells/BloodCultSpellsSystem.cs b/Content.Server/WhiteDream/BloodCult/Spells/BloodCultSpellsSystem.cs index c69bf6abd4e..18a04392e09 100644 --- a/Content.Server/WhiteDream/BloodCult/Spells/BloodCultSpellsSystem.cs +++ b/Content.Server/WhiteDream/BloodCult/Spells/BloodCultSpellsSystem.cs @@ -148,7 +148,7 @@ private void OnSpellSelected(Entity<BloodCultSpellsHolderComponent> cultist, ref createSpellEvent, cultist.Owner) { - BreakOnUserMove = true + BreakOnMove = true }; if (_doAfter.TryStartDoAfter(doAfter, out var doAfterId)) diff --git a/Content.Server/Whitelist/WhitelistCommands.cs b/Content.Server/Whitelist/WhitelistCommands.cs index 6a9050f57b2..6912c061629 100644 --- a/Content.Server/Whitelist/WhitelistCommands.cs +++ b/Content.Server/Whitelist/WhitelistCommands.cs @@ -31,7 +31,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] var playtime = IoCManager.Resolve<PlayTimeTrackingManager>(); var name = string.Join(' ', args).Trim(); - var data = await loc.LookupIdByNameAsync(name); + var data = await loc.LookupIdByNameOrIdAsync(name); if (data != null) { @@ -91,7 +91,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] var playtime = IoCManager.Resolve<PlayTimeTrackingManager>(); var name = string.Join(' ', args).Trim(); - var data = await loc.LookupIdByNameAsync(name); + var data = await loc.LookupIdByNameOrIdAsync(name); if (data != null) { diff --git a/Content.Server/Wires/WiresSystem.cs b/Content.Server/Wires/WiresSystem.cs index fce81db3277..526a14b1ab4 100644 --- a/Content.Server/Wires/WiresSystem.cs +++ b/Content.Server/Wires/WiresSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Tools.Components; using Content.Shared.UserInterface; using Content.Shared.Wires; @@ -702,7 +703,7 @@ private void TryDoWireAction(EntityUid target, EntityUid user, EntityUid toolEnt { NeedHand = true, BreakOnDamage = true, - BreakOnUserMove = true + BreakOnMove = true }; _doAfter.TryStartDoAfter(args); diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs index 72d99460fe2..a9f224ad5d9 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs @@ -11,6 +11,7 @@ using Content.Shared.DeviceLinking.Events; using Content.Shared.Placeable; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Research.Components; using Content.Shared.Xenoarchaeology.Equipment; using Content.Shared.Xenoarchaeology.XenoArtifacts; diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs index 6606f284327..f841ea910e7 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs @@ -7,7 +7,9 @@ using Content.Server.Xenoarchaeology.XenoArtifacts; using Content.Shared.Body.Components; using Content.Shared.Damage; +using Content.Shared.Power; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Content.Shared.Xenoarchaeology.Equipment; using Robust.Shared.Collections; using Robust.Shared.Random; @@ -25,6 +27,7 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; /// <inheritdoc/> public override void Initialize() @@ -92,7 +95,7 @@ public void FinishCrushing(Entity<ArtifactCrusherComponent, EntityStorageCompone var coords = Transform(ent).Coordinates; foreach (var contained in contents) { - if (crusher.CrushingWhitelist.IsValid(contained, EntityManager)) + if (_whitelistSystem.IsWhitelistPass(crusher.CrushingWhitelist, contained)) { var amount = _random.Next(crusher.MinFragments, crusher.MaxFragments); var stacks = _stack.SpawnMultiple(crusher.FragmentStackProtoId, amount, coords); diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DamageNearbyArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DamageNearbyArtifactSystem.cs index a2023a18d48..f231120ad5b 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DamageNearbyArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DamageNearbyArtifactSystem.cs @@ -1,6 +1,7 @@ -using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; +using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Events; using Content.Shared.Damage; +using Content.Shared.Whitelist; using Robust.Shared.Random; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems; @@ -10,6 +11,7 @@ public sealed class BreakWindowArtifactSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; /// <inheritdoc/> public override void Initialize() @@ -24,7 +26,7 @@ private void OnActivated(EntityUid uid, DamageNearbyArtifactComponent component, ents.Add(args.Activator.Value); foreach (var ent in ents) { - if (component.Whitelist != null && !component.Whitelist.IsValid(ent)) + if (_whitelistSystem.IsWhitelistFail(component.Whitelist, ent)) continue; if (!_random.Prob(component.DamageChance)) diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/EmpArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/EmpArtifactSystem.cs index d4ed8272aa3..970743f4848 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/EmpArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/EmpArtifactSystem.cs @@ -1,12 +1,14 @@ using Content.Server.Emp; using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Events; +using Robust.Server.GameObjects; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems; public sealed class EmpArtifactSystem : EntitySystem { [Dependency] private readonly EmpSystem _emp = default!; + [Dependency] private readonly TransformSystem _transform = default!; /// <inheritdoc/> public override void Initialize() @@ -16,6 +18,6 @@ public override void Initialize() private void OnActivate(EntityUid uid, EmpArtifactComponent component, ArtifactActivatedEvent args) { - _emp.EmpPulse(Transform(uid).MapPosition, component.Range, component.EnergyConsumption, component.DisableDuration); + _emp.EmpPulse(_transform.GetMapCoordinates(uid), component.Range, component.EnergyConsumption, component.DisableDuration); } -} \ No newline at end of file +} diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/SpawnArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/SpawnArtifactSystem.cs index fcb33ae41fd..c2622837872 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/SpawnArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/SpawnArtifactSystem.cs @@ -32,7 +32,7 @@ private void OnActivate(EntityUid uid, SpawnArtifactComponent component, Artifac if (component.Spawns is not {} spawns) return; - var artifactCord = Transform(uid).MapPosition; + var artifactCord = _transform.GetMapCoordinates(uid); foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random)) { var dx = _random.NextFloat(-component.Range, component.Range); diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ThrowArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ThrowArtifactSystem.cs index 57a30a2fd9e..8708e0ff4e8 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ThrowArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ThrowArtifactSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Throwing; +using Robust.Server.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; using Robust.Shared.Random; @@ -16,6 +17,7 @@ public sealed class ThrowArtifactSystem : EntitySystem [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly ThrowingSystem _throwing = default!; [Dependency] private readonly TileSystem _tile = default!; + [Dependency] private readonly TransformSystem _transform = default!; /// <inheritdoc/> public override void Initialize() @@ -50,7 +52,7 @@ private void OnActivated(EntityUid uid, ThrowArtifactComponent component, Artifa var tempXform = Transform(ent); - var foo = tempXform.MapPosition.Position - xform.MapPosition.Position; + var foo = _transform.GetMapCoordinates(ent, xform: tempXform).Position - _transform.GetMapCoordinates(uid, xform: xform).Position; _throwing.TryThrow(ent, foo*2, component.ThrowStrength, uid, 0); } } diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs index aa2a16aa1b2..019e09bbbbc 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs @@ -3,12 +3,14 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.Interaction; using Content.Shared.Tools.Components; +using Content.Shared.Tools.Systems; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; public sealed class ArtifactElectricityTriggerSystem : EntitySystem { [Dependency] private readonly ArtifactSystem _artifactSystem = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; public override void Initialize() { @@ -42,7 +44,7 @@ private void OnInteractUsing(EntityUid uid, ArtifactElectricityTriggerComponent if (args.Handled) return; - if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.ContainsAny("Pulsing")) + if (!_toolSystem.HasQuality(args.Used, "Pulsing")) return; args.Handled = _artifactSystem.TryActivateArtifact(uid, args.User); diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactMagnetTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactMagnetTriggerSystem.cs index c6f56a27508..a585a9ef452 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactMagnetTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactMagnetTriggerSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Salvage; using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.Clothing; +using Content.Shared.Item.ItemToggle.Components; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; @@ -29,11 +30,11 @@ public override void Update(float frameTime) _toActivate.Clear(); - //assume that there's more instruments than artifacts - var query = EntityQueryEnumerator<MagbootsComponent, TransformComponent>(); - while (query.MoveNext(out _, out var magboot, out var magXform)) + //assume that there's more magboots than artifacts + var query = EntityQueryEnumerator<MagbootsComponent, TransformComponent, ItemToggleComponent>(); + while (query.MoveNext(out _, out var magboot, out var magXform, out var toggle)) { - if (!magboot.On) + if (!toggle.Activated) continue; var artiQuery = EntityQueryEnumerator<ArtifactMagnetTriggerComponent, TransformComponent>(); diff --git a/Content.Server/Zombies/InitialInfectedExemptComponent.cs b/Content.Server/Zombies/InitialInfectedExemptComponent.cs deleted file mode 100644 index f2dfda3f872..00000000000 --- a/Content.Server/Zombies/InitialInfectedExemptComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Zombies; - -[RegisterComponent] -public sealed partial class InitialInfectedExemptComponent : Component -{ - -} diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 9e294c6aa58..2ddff91d40b 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -28,6 +28,8 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Systems; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; @@ -224,17 +226,11 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) _damageable.SetAllDamage(target, damageablecomp, 0); _mobState.ChangeMobState(target, MobState.Alive); - var factionComp = EnsureComp<NpcFactionMemberComponent>(target); - foreach (var id in new List<string>(factionComp.Factions)) - { - _faction.RemoveFaction(target, id); - } + _faction.ClearFactions(target, dirty: false); _faction.AddFaction(target, "Zombie"); //gives it the funny "Zombie ___" name. - var meta = MetaData(target); - zombiecomp.BeforeZombifiedEntityName = meta.EntityName; - _metaData.SetEntityName(target, Loc.GetString("zombie-name-prefix", ("target", meta.EntityName)), meta); + _nameMod.RefreshNameModifiers(target); _identity.QueueIdentityUpdate(target); diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 552fd2781c0..371c6f1222a 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Popups; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Zombies; @@ -34,9 +35,9 @@ public sealed partial class ZombieSystem : SharedZombieSystem [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AutoEmoteSystem _autoEmote = default!; [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; public const SlotFlags ProtectiveSlots = SlotFlags.FEET | @@ -281,7 +282,7 @@ public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombi _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false); _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent); - _metaData.SetEntityName(target, zombiecomp.BeforeZombifiedEntityName); + _nameMod.RefreshNameModifiers(target); return true; } diff --git a/Content.Server/_NF/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/_NF/Shuttles/Systems/ShuttleSystem.cs new file mode 100644 index 00000000000..afa65ea7e2f --- /dev/null +++ b/Content.Server/_NF/Shuttles/Systems/ShuttleSystem.cs @@ -0,0 +1,87 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. +using Content.Server._NF.Station.Components; +using Content.Server.Shuttles.Components; +using Content.Server.Station.Components; +using Content.Shared._NF.Shuttles.Events; +using Robust.Shared.Physics.Components; + +namespace Content.Server.Shuttles.Systems; + +public sealed partial class ShuttleSystem +{ + private const float SpaceFrictionStrength = 0.0015f; + private const float AnchorDampeningStrength = 0.5f; + private void NfInitialize() + { + SubscribeLocalEvent<ShuttleConsoleComponent, SetInertiaDampeningRequest>(OnSetInertiaDampening); + } + + private void OnSetInertiaDampening(EntityUid uid, ShuttleConsoleComponent component, SetInertiaDampeningRequest args) + { + // Ensure that the entity requested is a valid shuttle (stations should not be togglable) + if (!EntityManager.TryGetComponent(uid, out TransformComponent? transform) || + !transform.GridUid.HasValue || + !EntityManager.TryGetComponent(transform.GridUid, out PhysicsComponent? physicsComponent) || + !EntityManager.TryGetComponent(transform.GridUid, out ShuttleComponent? shuttleComponent)) + return; + + if (args.Mode == InertiaDampeningMode.Query) + { + _console.RefreshShuttleConsoles(transform.GridUid.Value); + return; + } + + if (!EntityManager.HasComponent<ShuttleComponent>(transform.GridUid) || + EntityManager.TryGetComponent<StationDataComponent>(_station.GetOwningStation(transform.GridUid), out var stationData) + && stationData.StationConfig != null) + return; + + var linearDampeningStrength = args.Mode switch + { + InertiaDampeningMode.Off => SpaceFrictionStrength, + InertiaDampeningMode.Dampen => shuttleComponent.LinearDamping, + InertiaDampeningMode.Anchor => AnchorDampeningStrength, + _ => shuttleComponent.LinearDamping, // other values: default to some sane behaviour (assume normal dampening) + }; + + var angularDampeningStrength = args.Mode switch + { + InertiaDampeningMode.Off => SpaceFrictionStrength, + InertiaDampeningMode.Dampen => shuttleComponent.AngularDamping, + InertiaDampeningMode.Anchor => AnchorDampeningStrength, + _ => shuttleComponent.AngularDamping, // other values: default to some sane behaviour (assume normal dampening) + }; + + _physics.SetLinearDamping(transform.GridUid.Value, physicsComponent, linearDampeningStrength); + _physics.SetAngularDamping(transform.GridUid.Value, physicsComponent, angularDampeningStrength); + _console.RefreshShuttleConsoles(transform.GridUid.Value); + } + + public InertiaDampeningMode NfGetInertiaDampeningMode(EntityUid entity) + { + if (!EntityManager.TryGetComponent<TransformComponent>(entity, out var xform)) + return InertiaDampeningMode.Dampen; + + var owningStation = _station.GetOwningStation(xform.GridUid); + + // Not a shuttle, shouldn't be togglable + if (!EntityManager.HasComponent<ShuttleComponent>(xform.GridUid) || + EntityManager.TryGetComponent<StationDataComponent>(owningStation, out var stationData) + && stationData.StationConfig != null) + return InertiaDampeningMode.Station; + + if (!EntityManager.TryGetComponent(xform.GridUid, out PhysicsComponent? physicsComponent)) + return InertiaDampeningMode.Dampen; + + if (physicsComponent.LinearDamping >= AnchorDampeningStrength) + return InertiaDampeningMode.Anchor; + + if (physicsComponent.LinearDamping <= SpaceFrictionStrength) + return InertiaDampeningMode.Off; + + return InertiaDampeningMode.Dampen; + } + +} diff --git a/Content.Server/_NF/Station/Components/StationDampeningComponent.cs b/Content.Server/_NF/Station/Components/StationDampeningComponent.cs new file mode 100644 index 00000000000..f6e184077a9 --- /dev/null +++ b/Content.Server/_NF/Station/Components/StationDampeningComponent.cs @@ -0,0 +1,10 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. + +namespace Content.Server._NF.Station.Components; + +[RegisterComponent] +public sealed partial class StationDampeningComponent : Component +{ +} diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index dad042f9706..265965af94e 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -105,5 +105,11 @@ public enum LogType /// <remarks> /// This is a default value used by <c>PlayerRateLimitManager</c>, though users can use different log types. /// </remarks> - RateLimited = 91 + RateLimited = 91, + InteractUsing = 92, + + /// <summary> + /// Storage & entity-storage related interactions + /// </summary> + Storage = 93, } diff --git a/Content.Shared.Database/TypedHwid.cs b/Content.Shared.Database/TypedHwid.cs new file mode 100644 index 00000000000..253375e9dbc --- /dev/null +++ b/Content.Shared.Database/TypedHwid.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.Database; + +/// <summary> +/// Represents a raw HWID value together with its type. +/// </summary> +[Serializable] +public sealed class ImmutableTypedHwid(ImmutableArray<byte> hwid, HwidType type) +{ + public readonly ImmutableArray<byte> Hwid = hwid; + public readonly HwidType Type = type; + + public override string ToString() + { + var b64 = Convert.ToBase64String(Hwid.AsSpan()); + return Type == HwidType.Modern ? $"V2-{b64}" : b64; + } + + public static bool TryParse(string value, [NotNullWhen(true)] out ImmutableTypedHwid? hwid) + { + var type = HwidType.Legacy; + if (value.StartsWith("V2-", StringComparison.Ordinal)) + { + value = value["V2-".Length..]; + type = HwidType.Modern; + } + + var array = new byte[GetBase64ByteLength(value)]; + if (!Convert.TryFromBase64String(value, array, out _)) + { + hwid = null; + return false; + } + + // ReSharper disable once UseCollectionExpression + // Do not use collection expression, C# compiler is weird and it fails sandbox. + hwid = new ImmutableTypedHwid(ImmutableArray.Create(array), type); + return true; + } + + private static int GetBase64ByteLength(string value) + { + // Why is .NET like this man wtf. + return 3 * (value.Length / 4) - value.TakeLast(2).Count(c => c == '='); + } +} + +/// <summary> +/// Represents different types of HWIDs as exposed by the engine. +/// </summary> +public enum HwidType +{ + /// <summary> + /// The legacy HWID system. Should only be used for checking old existing database bans. + /// </summary> + Legacy = 0, + + /// <summary> + /// The modern HWID system. + /// </summary> + Modern = 1, +} diff --git a/Content.Shared/Abilities/Goliath/GoliathSummonTentacleAction.cs b/Content.Shared/Abilities/Goliath/GoliathSummonTentacleAction.cs new file mode 100644 index 00000000000..abdb0d5e108 --- /dev/null +++ b/Content.Shared/Abilities/Goliath/GoliathSummonTentacleAction.cs @@ -0,0 +1,31 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Abilities.Goliath; + +public sealed partial class GoliathSummonTentacleAction : EntityWorldTargetActionEvent +{ + /// <summary> + /// The ID of the entity that is spawned. + /// </summary> + [DataField] + public EntProtoId EntityId = "EffectGoliathTentacleSpawn"; + + /// <summary> + /// Directions determining where the entities will spawn. + /// </summary> + [DataField] + public List<Direction> OffsetDirections = new() + { + Direction.North, + Direction.South, + Direction.East, + Direction.West, + }; + + /// <summary> + /// How many entities will spawn beyond the original one at the target location? + /// </summary> + [DataField] + public int ExtraSpawns = 3; +}; diff --git a/Content.Shared/Abilities/Goliath/GoliathTentacleSystem.cs b/Content.Shared/Abilities/Goliath/GoliathTentacleSystem.cs new file mode 100644 index 00000000000..98dbc5e18a4 --- /dev/null +++ b/Content.Shared/Abilities/Goliath/GoliathTentacleSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared.Directions; +using Content.Shared.Maps; +using Content.Shared.Physics; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Network; +using Robust.Shared.Random; + +namespace Content.Shared.Abilities.Goliath; + +public sealed class GoliathTentacleSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly TurfSystem _turf = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + /// <inheritdoc/> + public override void Initialize() + { + SubscribeLocalEvent<GoliathSummonTentacleAction>(OnSummonAction); + } + + private void OnSummonAction(GoliathSummonTentacleAction args) + { + if (args.Handled || args.Coords is not { } coords) + return; + + // TODO: animation + + _popup.PopupPredicted(Loc.GetString("tentacle-ability-use-popup", ("entity", args.Performer)), args.Performer, args.Performer, type: PopupType.SmallCaution); + _stun.TryStun(args.Performer, TimeSpan.FromSeconds(0.8f), false); + + List<EntityCoordinates> spawnPos = new(); + spawnPos.Add(coords); + + var dirs = new List<Direction>(); + dirs.AddRange(args.OffsetDirections); + + for (var i = 0; i < 3; i++) + { + var dir = _random.PickAndTake(dirs); + spawnPos.Add(coords.Offset(dir)); + } + + if (_transform.GetGrid(coords) is not { } grid || !TryComp<MapGridComponent>(grid, out var gridComp)) + return; + + foreach (var pos in spawnPos) + { + if (!_map.TryGetTileRef(grid, gridComp, pos, out var tileRef) || + tileRef.IsSpace() || + _turf.IsTileBlocked(tileRef, CollisionGroup.Impassable)) + { + continue; + } + + if (_net.IsServer) + Spawn(args.EntityId, pos); + } + + args.Handled = true; + } +} diff --git a/Content.Shared/Access/Components/AccessToggleComponent.cs b/Content.Shared/Access/Components/AccessToggleComponent.cs new file mode 100644 index 00000000000..60a606ac7ea --- /dev/null +++ b/Content.Shared/Access/Components/AccessToggleComponent.cs @@ -0,0 +1,11 @@ +using Content.Shared.Access.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Access.Components; + +/// <summary> +/// Toggles an access provider with <c>ItemToggle</c>. +/// Requires <see cref="AccessComponent"/>. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(AccessToggleSystem))] +public sealed partial class AccessToggleComponent : Component; diff --git a/Content.Shared/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs index 39d5d9d27f3..22d97169af3 100644 --- a/Content.Shared/Access/Components/IdCardComponent.cs +++ b/Content.Shared/Access/Components/IdCardComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.PDA; using Content.Shared.StatusIcon; using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; namespace Content.Shared.Access.Components; @@ -11,34 +11,39 @@ namespace Content.Shared.Access.Components; [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)] public sealed partial class IdCardComponent : Component { - [DataField("fullName"), ViewVariables(VVAccess.ReadWrite)] + [DataField] [AutoNetworkedField] // FIXME Friends public string? FullName; - [DataField("jobTitle")] + [DataField] [AutoNetworkedField] - [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite), ViewVariables(VVAccess.ReadWrite)] - public string? JobTitle; + [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)] + public LocId? JobTitle; + + private string? _jobTitle; + + [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWriteExecute)] + public string? LocalizedJobTitle { set => _jobTitle = value; get => _jobTitle ?? Loc.GetString(JobTitle ?? string.Empty); } /// <summary> /// The state of the job icon rsi. /// </summary> - [DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))] + [DataField] [AutoNetworkedField] - public string JobIcon = "JobIconUnknown"; + public ProtoId<JobIconPrototype> JobIcon = "JobIconUnknown"; /// <summary> /// The unlocalized names of the departments associated with the job /// </summary> - [DataField("jobDepartments")] + [DataField] [AutoNetworkedField] public List<LocId> JobDepartments = new(); /// <summary> /// Determines if accesses from this card should be logged by <see cref="AccessReaderComponent"/> /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool BypassLogging; [DataField] @@ -46,4 +51,7 @@ public sealed partial class IdCardComponent : Component [DataField] public LocId FullNameLocId = "access-id-card-component-owner-full-name-job-title-text"; + + [DataField] + public bool CanMicrowave = true; } diff --git a/Content.Shared/Access/SharedAgentIDCardSystem.cs b/Content.Shared/Access/SharedAgentIDCardSystem.cs index d027a3937f5..b035bdff342 100644 --- a/Content.Shared/Access/SharedAgentIDCardSystem.cs +++ b/Content.Shared/Access/SharedAgentIDCardSystem.cs @@ -1,3 +1,5 @@ +using Content.Shared.StatusIcon; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Access.Systems @@ -23,17 +25,29 @@ public enum AgentIDCardUiKey : byte [Serializable, NetSerializable] public sealed class AgentIDCardBoundUserInterfaceState : BoundUserInterfaceState { - public readonly HashSet<string> Icons; public string CurrentName { get; } public string CurrentJob { get; } public string CurrentJobIconId { get; } + public uint? CurrentNumber { get; } // DeltaV - public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId, HashSet<string> icons) + public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId, uint? currentNumber = null) // DeltaV - Added currentNumber { - Icons = icons; CurrentName = currentName; CurrentJob = currentJob; CurrentJobIconId = currentJobIconId; + CurrentNumber = currentNumber; // DeltaV + } + } + + // DeltaV - Add number change message + [Serializable, NetSerializable] + public sealed class AgentIDCardNumberChangedMessage : BoundUserInterfaceMessage + { + public uint Number { get; } + + public AgentIDCardNumberChangedMessage(uint number) + { + Number = number; } } @@ -62,9 +76,9 @@ public AgentIDCardJobChangedMessage(string job) [Serializable, NetSerializable] public sealed class AgentIDCardJobIconChangedMessage : BoundUserInterfaceMessage { - public string JobIconId { get; } + public ProtoId<JobIconPrototype> JobIconId { get; } - public AgentIDCardJobIconChangedMessage(string jobIconId) + public AgentIDCardJobIconChangedMessage(ProtoId<JobIconPrototype> jobIconId) { JobIconId = jobIconId; } diff --git a/Content.Shared/Access/Systems/AccessReaderSystem.cs b/Content.Shared/Access/Systems/AccessReaderSystem.cs index 3670e24bd32..dd0bf7bde2e 100644 --- a/Content.Shared/Access/Systems/AccessReaderSystem.cs +++ b/Content.Shared/Access/Systems/AccessReaderSystem.cs @@ -11,6 +11,7 @@ using Robust.Shared.Containers; using Robust.Shared.GameStates; using Content.Shared.GameTicking; +using Content.Shared.IdentityManagement; using Robust.Shared.Collections; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -24,7 +25,6 @@ public sealed class AccessReaderSystem : EntitySystem [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly SharedGameTicker _gameTicker = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly SharedIdCardSystem _idCardSystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedStationRecordsSystem _recordsSystem = default!; @@ -395,9 +395,12 @@ private void LogAccess(Entity<AccessReaderComponent> ent, EntityUid accessor) string? name = null; // TODO pass the ID card on IsAllowed() instead of using this expensive method // Set name if the accessor has a card and that card has a name and allows itself to be recorded - if (_idCardSystem.TryFindIdCard(accessor, out var idCard) - && idCard.Comp is { BypassLogging: false, FullName: not null }) - name = idCard.Comp.FullName; + var getIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(ent, accessor, true); + RaiseLocalEvent(getIdentityShortInfoEvent); + if (getIdentityShortInfoEvent.Title != null) + { + name = getIdentityShortInfoEvent.Title; + } name ??= Loc.GetString("access-reader-unknown-id"); diff --git a/Content.Shared/Access/Systems/AccessToggleSystem.cs b/Content.Shared/Access/Systems/AccessToggleSystem.cs new file mode 100644 index 00000000000..564aca06812 --- /dev/null +++ b/Content.Shared/Access/Systems/AccessToggleSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Access.Components; +using Content.Shared.Item.ItemToggle.Components; + +namespace Content.Shared.Access.Systems; + +public sealed class AccessToggleSystem : EntitySystem +{ + [Dependency] private readonly SharedAccessSystem _access = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<AccessToggleComponent, ItemToggledEvent>(OnToggled); + } + + private void OnToggled(Entity<AccessToggleComponent> ent, ref ItemToggledEvent args) + { + _access.SetAccessEnabled(ent, args.Activated); + } +} diff --git a/Content.Shared/Access/Systems/IdExaminableSystem.cs b/Content.Shared/Access/Systems/IdExaminableSystem.cs index 333272e27ac..7ca96befa85 100644 --- a/Content.Shared/Access/Systems/IdExaminableSystem.cs +++ b/Content.Shared/Access/Systems/IdExaminableSystem.cs @@ -66,7 +66,7 @@ public string GetMessage(EntityUid uid) private string GetNameAndJob(IdCardComponent id) { - var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})"; + var jobSuffix = string.IsNullOrWhiteSpace(id.LocalizedJobTitle) ? string.Empty : $" ({id.LocalizedJobTitle})"; var val = string.IsNullOrWhiteSpace(id.FullName) ? Loc.GetString(id.NameLocId, diff --git a/Content.Shared/Access/Systems/SharedIdCardSystem.cs b/Content.Shared/Access/Systems/SharedIdCardSystem.cs index bfde70f5041..c031902b6ed 100644 --- a/Content.Shared/Access/Systems/SharedIdCardSystem.cs +++ b/Content.Shared/Access/Systems/SharedIdCardSystem.cs @@ -1,7 +1,9 @@ +using System.Globalization; using Content.Shared.Access.Components; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.Roles; @@ -20,7 +22,9 @@ public abstract class SharedIdCardSystem : EntitySystem public override void Initialize() { base.Initialize(); + SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo); } private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args) @@ -28,6 +32,23 @@ private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args) UpdateEntityName(uid, id); } + private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent ev) + { + if (ev.Handled) + { + return; + } + + string? title = null; + if (TryFindIdCard(ev.ForActor, out var idCard) && !(ev.RequestForAccessLogging && idCard.Comp.BypassLogging)) + { + title = ExtractFullTitle(idCard); + } + + ev.Title = title; + ev.Handled = true; + } + /// <summary> /// Attempt to find an ID card on an entity. This will look in the entity itself, in the entity's hands, and /// in the entity's inventory. @@ -82,6 +103,7 @@ public bool TryGetIdCard(EntityUid uid, out Entity<IdCardComponent> idCard) /// </summary> /// <remarks> /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. + /// Actually works with the LocalizedJobTitle DataField and not with JobTitle. /// </remarks> public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null) { @@ -100,9 +122,9 @@ public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? jobTitle = null; } - if (id.JobTitle == jobTitle) + if (id.LocalizedJobTitle == jobTitle) return true; - id.JobTitle = jobTitle; + id.LocalizedJobTitle = jobTitle; Dirty(uid, id); UpdateEntityName(uid, id); @@ -114,7 +136,7 @@ public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? return true; } - public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null) + public bool TryChangeJobIcon(EntityUid uid, JobIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null) { if (!Resolve(uid, ref id)) { @@ -204,7 +226,7 @@ private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null) if (!Resolve(uid, ref id)) return; - var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})"; + var jobSuffix = string.IsNullOrWhiteSpace(id.LocalizedJobTitle) ? string.Empty : $" ({id.LocalizedJobTitle})"; var val = string.IsNullOrWhiteSpace(id.FullName) ? Loc.GetString(id.NameLocId, @@ -214,4 +236,10 @@ private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null) ("jobSuffix", jobSuffix)); _metaSystem.SetEntityName(uid, val); } + + private static string ExtractFullTitle(IdCardComponent idCardComponent) + { + return $"{idCardComponent.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(idCardComponent.LocalizedJobTitle ?? string.Empty)})" + .Trim(); + } } diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs index 6cc50bc21b4..6ff86604589 100644 --- a/Content.Shared/Actions/ActionEvents.cs +++ b/Content.Shared/Actions/ActionEvents.cs @@ -101,6 +101,13 @@ public RequestPerformActionEvent(NetEntity action, NetCoordinates entityCoordina Action = action; EntityCoordinatesTarget = entityCoordinatesTarget; } + + public RequestPerformActionEvent(NetEntity action, NetEntity entityTarget, NetCoordinates entityCoordinatesTarget) + { + Action = action; + EntityTarget = entityTarget; + EntityCoordinatesTarget = entityCoordinatesTarget; + } } /// <summary> @@ -144,6 +151,27 @@ public abstract partial class WorldTargetActionEvent : BaseActionEvent public EntityCoordinates Target; } +/// <summary> +/// This is the type of event that gets raised when an <see cref="EntityWorldTargetActionComponent"/> is performed. +/// The <see cref="BaseActionEvent.Performer"/>, <see cref="Entity"/>, and <see cref="Coords"/> +/// fields will automatically be filled out by the <see cref="SharedActionsSystem"/>. +/// </summary> +/// <remarks> +/// To define a new action for some system, you need to create an event that inherits from this class. +/// </remarks> +public abstract partial class EntityWorldTargetActionEvent : BaseActionEvent +{ + /// <summary> + /// The entity that the user targeted. + /// </summary> + public EntityUid? Entity; + + /// <summary> + /// The coordinates of the location that the user targeted. + /// </summary> + public EntityCoordinates? Coords; +} + /// <summary> /// Base class for events that are raised when an action gets performed. This should not generally be used outside of the action /// system. @@ -159,5 +187,10 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs /// <summary> /// The action the event belongs to. /// </summary> - public EntityUid Action; + public Entity<BaseActionComponent> Action; + + /// <summary> + /// Should we toggle the action entity? + /// </summary> + public bool Toggle; } diff --git a/Content.Shared/Actions/ActionGrantComponent.cs b/Content.Shared/Actions/ActionGrantComponent.cs new file mode 100644 index 00000000000..94c3a0bbd1d --- /dev/null +++ b/Content.Shared/Actions/ActionGrantComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Actions; + +/// <summary> +/// Grants actions on MapInit and removes them on shutdown +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(ActionGrantSystem))] +public sealed partial class ActionGrantComponent : Component +{ + [DataField(required: true), AutoNetworkedField, AlwaysPushInheritance] + public List<EntProtoId> Actions = new(); + + [DataField, AutoNetworkedField] + public List<EntityUid> ActionEntities = new(); +} diff --git a/Content.Shared/Actions/ActionGrantSystem.cs b/Content.Shared/Actions/ActionGrantSystem.cs new file mode 100644 index 00000000000..f73ecf8a460 --- /dev/null +++ b/Content.Shared/Actions/ActionGrantSystem.cs @@ -0,0 +1,48 @@ +namespace Content.Shared.Actions; + +/// <summary> +/// <see cref="ActionGrantComponent"/> +/// </summary> +public sealed class ActionGrantSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<ActionGrantComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<ActionGrantComponent, ComponentShutdown>(OnShutdown); + SubscribeLocalEvent<ItemActionGrantComponent, GetItemActionsEvent>(OnItemGet); + } + + private void OnItemGet(Entity<ItemActionGrantComponent> ent, ref GetItemActionsEvent args) + { + if (!TryComp(ent.Owner, out ActionGrantComponent? grant)) + return; + + foreach (var action in grant.ActionEntities) + { + args.AddAction(action); + } + } + + private void OnMapInit(Entity<ActionGrantComponent> ent, ref MapInitEvent args) + { + foreach (var action in ent.Comp.Actions) + { + EntityUid? actionEnt = null; + _actions.AddAction(ent.Owner, ref actionEnt, action); + + if (actionEnt != null) + ent.Comp.ActionEntities.Add(actionEnt.Value); + } + } + + private void OnShutdown(Entity<ActionGrantComponent> ent, ref ComponentShutdown args) + { + foreach (var actionEnt in ent.Comp.ActionEntities) + { + _actions.RemoveAction(ent.Owner, actionEnt); + } + } +} diff --git a/Content.Shared/Actions/BaseActionComponent.cs b/Content.Shared/Actions/BaseActionComponent.cs index 9156f747f5c..720900b7840 100644 --- a/Content.Shared/Actions/BaseActionComponent.cs +++ b/Content.Shared/Actions/BaseActionComponent.cs @@ -66,6 +66,11 @@ public abstract partial class BaseActionComponent : Component // TODO serialization public (TimeSpan Start, TimeSpan End)? Cooldown; + /// <summary> + /// If true, the action will have an initial cooldown applied upon addition. + /// </summary> + [DataField] public bool StartDelay = false; + /// <summary> /// Time interval between action uses. /// </summary> diff --git a/Content.Shared/Actions/EntityWorldTargetActionComponent.cs b/Content.Shared/Actions/EntityWorldTargetActionComponent.cs new file mode 100644 index 00000000000..3cfa60d030a --- /dev/null +++ b/Content.Shared/Actions/EntityWorldTargetActionComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Actions; + +/// <summary> +/// Used on action entities to define an action that triggers when targeting an entity or entity coordinates. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class EntityWorldTargetActionComponent : BaseTargetActionComponent +{ + public override BaseActionEvent? BaseEvent => Event; + + /// <summary> + /// The local-event to raise when this action is performed. + /// </summary> + [DataField] + [NonSerialized] + public EntityWorldTargetActionEvent? Event; + + /// <summary> + /// Determines which entities are valid targets for this action. + /// </summary> + /// <remarks>No whitelist check when null.</remarks> + [DataField] public EntityWhitelist? Whitelist; + + /// <summary> + /// Whether this action considers the user as a valid target entity when using this action. + /// </summary> + [DataField] public bool CanTargetSelf = true; +} + +[Serializable, NetSerializable] +public sealed class EntityWorldTargetActionComponentState( + EntityWorldTargetActionComponent component, + IEntityManager entManager) + : BaseActionComponentState(component, entManager) +{ + public EntityWhitelist? Whitelist = component.Whitelist; + public bool CanTargetSelf = component.CanTargetSelf; +} diff --git a/Content.Shared/Actions/Events/ActionComponentChangeEvent.cs b/Content.Shared/Actions/Events/ActionComponentChangeEvent.cs new file mode 100644 index 00000000000..c9c4db145da --- /dev/null +++ b/Content.Shared/Actions/Events/ActionComponentChangeEvent.cs @@ -0,0 +1,27 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Actions.Events; + +/// <summary> +/// Adds / removes the component upon action. +/// </summary> +[Virtual] +public partial class ActionComponentChangeEvent : InstantActionEvent +{ + [DataField(required: true)] + public ComponentRegistry Components = new(); +} + +/// <summary> +/// Similar to <see cref="ActionComponentChangeEvent"/> except raises an event to attempt to relay it. +/// </summary> +public sealed partial class RelayedActionComponentChangeEvent : ActionComponentChangeEvent +{ + +} + +[ByRefEvent] +public record struct AttemptRelayActionComponentChangeEvent +{ + public EntityUid? Target; +} diff --git a/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs b/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs index cdcba6740c2..80ef0b0f6ce 100644 --- a/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs +++ b/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs @@ -35,10 +35,7 @@ public sealed partial class PsionicHealOtherPowerActionEvent : EntityTargetActio public bool DoRevive; [DataField] - public bool BreakOnUserMove = true; - - [DataField] - public bool BreakOnTargetMove = false; + public bool BreakOnMove = true; [DataField] public float UseDelay = 8f; diff --git a/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs new file mode 100644 index 00000000000..57c47026be0 --- /dev/null +++ b/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Map; + +namespace Content.Shared.Actions.Events; + +[ByRefEvent] +public record struct ValidateActionEntityWorldTargetEvent( + EntityUid User, + EntityUid? Target, + EntityCoordinates? Coords, + bool Cancelled = false); diff --git a/Content.Shared/Actions/ItemActionGrantComponent.cs b/Content.Shared/Actions/ItemActionGrantComponent.cs new file mode 100644 index 00000000000..d1769b51a2f --- /dev/null +++ b/Content.Shared/Actions/ItemActionGrantComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Actions; + +/// <summary> +/// Works in tandem with <see cref="ActionGrantComponent"/> by granting those actions to the equipper entity. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(ActionGrantSystem))] +public sealed partial class ItemActionGrantComponent : Component +{ + [DataField(required: true), AutoNetworkedField, AlwaysPushInheritance] + public List<EntProtoId> Actions = new(); +} diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 785a4478d56..42cd51f2c9f 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Inventory.Events; using Content.Shared.Mind; using Content.Shared.Rejuvenate; +using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -24,11 +25,11 @@ public abstract class SharedActionsSystem : EntitySystem [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -37,11 +38,15 @@ public override void Initialize() SubscribeLocalEvent<InstantActionComponent, MapInitEvent>(OnActionMapInit); SubscribeLocalEvent<EntityTargetActionComponent, MapInitEvent>(OnActionMapInit); SubscribeLocalEvent<WorldTargetActionComponent, MapInitEvent>(OnActionMapInit); + SubscribeLocalEvent<EntityWorldTargetActionComponent, MapInitEvent>(OnActionMapInit); SubscribeLocalEvent<InstantActionComponent, ComponentShutdown>(OnActionShutdown); SubscribeLocalEvent<EntityTargetActionComponent, ComponentShutdown>(OnActionShutdown); SubscribeLocalEvent<WorldTargetActionComponent, ComponentShutdown>(OnActionShutdown); + SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentShutdown>(OnActionShutdown); + SubscribeLocalEvent<ActionsComponent, ActionComponentChangeEvent>(OnActionCompChange); + SubscribeLocalEvent<ActionsComponent, RelayedActionComponentChangeEvent>(OnRelayActionCompChange); SubscribeLocalEvent<ActionsComponent, DidEquipEvent>(OnDidEquip); SubscribeLocalEvent<ActionsComponent, DidEquipHandEvent>(OnHandEquipped); SubscribeLocalEvent<ActionsComponent, DidUnequipEvent>(OnDidUnequip); @@ -55,10 +60,12 @@ public override void Initialize() SubscribeLocalEvent<InstantActionComponent, ComponentGetState>(OnInstantGetState); SubscribeLocalEvent<EntityTargetActionComponent, ComponentGetState>(OnEntityTargetGetState); SubscribeLocalEvent<WorldTargetActionComponent, ComponentGetState>(OnWorldTargetGetState); + SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentGetState>(OnEntityWorldTargetGetState); SubscribeLocalEvent<InstantActionComponent, GetActionDataEvent>(OnGetActionData); SubscribeLocalEvent<EntityTargetActionComponent, GetActionDataEvent>(OnGetActionData); SubscribeLocalEvent<WorldTargetActionComponent, GetActionDataEvent>(OnGetActionData); + SubscribeLocalEvent<EntityWorldTargetActionComponent, GetActionDataEvent>(OnGetActionData); SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest); } @@ -101,6 +108,11 @@ private void OnWorldTargetGetState(EntityUid uid, WorldTargetActionComponent com args.State = new WorldTargetActionComponentState(component, EntityManager); } + private void OnEntityWorldTargetGetState(EntityUid uid, EntityWorldTargetActionComponent component, ref ComponentGetState args) + { + args.State = new EntityWorldTargetActionComponentState(component, EntityManager); + } + private void OnGetActionData<T>(EntityUid uid, T component, ref GetActionDataEvent args) where T : BaseActionComponent { args.Action = component; @@ -441,6 +453,34 @@ private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArg } break; + case EntityWorldTargetActionComponent entityWorldAction: + { + var actionEntity = GetEntity(ev.EntityTarget); + var actionCoords = GetCoordinates(ev.EntityCoordinatesTarget); + + if (actionEntity is null && actionCoords is null) + { + Log.Error($"Attempted to perform an entity-world-targeted action without an entity or world coordinates! Action: {name}"); + return; + } + + var entWorldAction = new Entity<EntityWorldTargetActionComponent>(actionEnt, entityWorldAction); + + if (!ValidateEntityWorldTarget(user, actionEntity, actionCoords, entWorldAction)) + return; + + _adminLogger.Add(LogType.Action, + $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):provider}) targeted at {ToPrettyString(actionEntity):target} {actionCoords:target}."); + + if (entityWorldAction.Event != null) + { + entityWorldAction.Event.Entity = actionEntity; + entityWorldAction.Event.Coords = actionCoords; + Dirty(actionEnt, entityWorldAction); + performEvent = entityWorldAction.Event; + } + break; + } case InstantActionComponent instantAction: if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null)) return; @@ -452,19 +492,20 @@ private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArg break; } - if (performEvent != null) - { - performEvent.Performer = user; - performEvent.Action = actionEnt; - } - // All checks passed. Perform the action! PerformAction(user, component, actionEnt, action, performEvent, curTime); } public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity<EntityTargetActionComponent> actionEnt) { - if (!ValidateEntityTargetBase(user, target, actionEnt)) + var comp = actionEnt.Comp; + if (!ValidateEntityTargetBase(user, + target, + comp.Whitelist, + comp.CheckCanInteract, + comp.CanTargetSelf, + comp.CheckCanAccess, + comp.Range)) return false; var ev = new ValidateActionEntityTargetEvent(user, target); @@ -472,24 +513,27 @@ public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity<Entity return !ev.Cancelled; } - private bool ValidateEntityTargetBase(EntityUid user, EntityUid target, EntityTargetActionComponent action) + private bool ValidateEntityTargetBase(EntityUid user, + EntityUid? targetEntity, + EntityWhitelist? whitelist, + bool checkCanInteract, + bool canTargetSelf, + bool checkCanAccess, + float range) { - if (!target.IsValid() || Deleted(target)) + if (targetEntity is not { } target || !target.IsValid() || Deleted(target)) return false; - if (action.Whitelist != null && !action.Whitelist.IsValid(target, EntityManager)) + if (_whitelistSystem.IsWhitelistFail(whitelist, target)) return false; - if (action.Blacklist != null && action.Blacklist.IsValid(target, EntityManager)) - return false; - - if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, target)) + if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target)) return false; if (user == target) - return action.CanTargetSelf; + return canTargetSelf; - if (!action.CheckCanAccess) + if (!checkCanAccess) { // even if we don't check for obstructions, we may still need to check the range. var xform = Transform(user); @@ -498,19 +542,20 @@ private bool ValidateEntityTargetBase(EntityUid user, EntityUid target, EntityTa if (xform.MapID != targetXform.MapID) return false; - if (action.Range <= 0) + if (range <= 0) return true; var distance = (_transformSystem.GetWorldPosition(xform) - _transformSystem.GetWorldPosition(targetXform)).Length(); - return distance <= action.Range; + return distance <= range; } - return _interactionSystem.InRangeAndAccessible(user, target, range: action.Range); + return _interactionSystem.InRangeAndAccessible(user, target, range: range); } public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity<WorldTargetActionComponent> action) { - if (!ValidateWorldTargetBase(user, coords, action)) + var comp = action.Comp; + if (!ValidateWorldTargetBase(user, coords, comp.CheckCanInteract, comp.CheckCanAccess, comp.Range)) return false; var ev = new ValidateActionWorldTargetEvent(user, coords); @@ -518,12 +563,19 @@ public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity return !ev.Cancelled; } - private bool ValidateWorldTargetBase(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action) + private bool ValidateWorldTargetBase(EntityUid user, + EntityCoordinates? entityCoordinates, + bool checkCanInteract, + bool checkCanAccess, + float range) { - if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null)) + if (entityCoordinates is not { } coords) return false; - if (!action.CheckCanAccess) + if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, null)) + return false; + + if (!checkCanAccess) { // even if we don't check for obstructions, we may still need to check the range. var xform = Transform(user); @@ -531,13 +583,40 @@ private bool ValidateWorldTargetBase(EntityUid user, EntityCoordinates coords, W if (xform.MapID != coords.GetMapId(EntityManager)) return false; - if (action.Range <= 0) + if (range <= 0) return true; - return coords.InRange(EntityManager, _transformSystem, Transform(user).Coordinates, action.Range); + return coords.InRange(EntityManager, _transformSystem, Transform(user).Coordinates, range); } - return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range); + return _interactionSystem.InRangeUnobstructed(user, coords, range: range); + } + + public bool ValidateEntityWorldTarget(EntityUid user, + EntityUid? entity, + EntityCoordinates? coords, + Entity<EntityWorldTargetActionComponent> action) + { + var comp = action.Comp; + var entityValidated = ValidateEntityTargetBase(user, + entity, + comp.Whitelist, + comp.CheckCanInteract, + comp.CanTargetSelf, + comp.CheckCanAccess, + comp.Range); + + var worldValidated + = ValidateWorldTargetBase(user, coords, comp.CheckCanInteract, comp.CheckCanAccess, comp.Range); + + if (!entityValidated && !worldValidated) + return false; + + var ev = new ValidateActionEntityWorldTargetEvent(user, + entityValidated ? entity : null, + worldValidated ? coords : null); + RaiseLocalEvent(action, ref ev); + return !ev.Cancelled; } public void PerformAction(EntityUid performer, ActionsComponent? component, EntityUid actionId, BaseActionComponent action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = true) @@ -558,6 +637,8 @@ public void PerformAction(EntityUid performer, ActionsComponent? component, Enti // This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use). actionEvent.Handled = false; var target = performer; + actionEvent.Performer = performer; + actionEvent.Action = (actionId, action); if (!action.RaiseOnUser && action.Container != null && !HasComp<MindComponent>(action.Container)) target = action.Container.Value; @@ -570,10 +651,14 @@ public void PerformAction(EntityUid performer, ActionsComponent? component, Enti return; // no interaction occurred. // play sound, reduce charges, start cooldown, and mark as dirty (if required). + if (actionEvent?.Toggle == true) + { + action.Toggled = !action.Toggled; + } - _audio.PlayPredicted(action.Sound, performer,predicted ? performer : null); + _audio.PlayPredicted(action.Sound, performer, predicted ? performer : null); - var dirty = toggledBefore == action.Toggled; + var dirty = toggledBefore != action.Toggled; if (action.Charges != null) { @@ -594,10 +679,11 @@ public void PerformAction(EntityUid performer, ActionsComponent? component, Enti action.Cooldown = (curTime, curTime + action.UseDelay.Value); } - Dirty(actionId, action); - - if (dirty && component != null) - Dirty(performer, component); + if (dirty) + { + Dirty(actionId, action); + UpdateAction(actionId, action); + } var ev = new ActionPerformedEvent(performer); RaiseLocalEvent(actionId, ref ev); @@ -695,6 +781,9 @@ public bool AddActionDirect(EntityUid performer, if (action.AttachedEntity != null) RemoveAction(action.AttachedEntity.Value, actionId, action: action); + if (action.StartDelay && action.UseDelay != null) + SetCooldown(actionId, action.UseDelay.Value); + DebugTools.AssertOwner(performer, comp); comp ??= EnsureComp<ActionsComponent>(performer); action.AttachedEntity = performer; @@ -879,8 +968,64 @@ protected virtual void ActionRemoved(EntityUid performer, EntityUid actionId, Ac // See client-side system for UI code. } + public bool ValidAction(BaseActionComponent action, bool canReach = true) + { + if (!action.Enabled) + return false; + + if (action.Charges.HasValue && action.Charges <= 0) + return false; + + var curTime = GameTiming.CurTime; + if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime) + return false; + + return canReach || action is BaseTargetActionComponent { CheckCanAccess: false }; + } + #endregion + private void OnRelayActionCompChange(Entity<ActionsComponent> ent, ref RelayedActionComponentChangeEvent args) + { + if (args.Handled) + return; + + var ev = new AttemptRelayActionComponentChangeEvent(); + RaiseLocalEvent(ent.Owner, ref ev); + var target = ev.Target ?? ent.Owner; + + args.Handled = true; + args.Toggle = true; + + if (!args.Action.Comp.Toggled) + { + EntityManager.AddComponents(target, args.Components); + } + else + { + EntityManager.RemoveComponents(target, args.Components); + } + } + + private void OnActionCompChange(Entity<ActionsComponent> ent, ref ActionComponentChangeEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + args.Toggle = true; + var target = ent.Owner; + + if (!args.Action.Comp.Toggled) + { + EntityManager.AddComponents(target, args.Components); + } + else + { + EntityManager.RemoveComponents(target, args.Components); + } + } + #region EquipHandlers private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args) { diff --git a/Content.Shared/Administration/AdminFrozenComponent.cs b/Content.Shared/Administration/AdminFrozenComponent.cs index 164cf764c86..bfcf1db5261 100644 --- a/Content.Shared/Administration/AdminFrozenComponent.cs +++ b/Content.Shared/Administration/AdminFrozenComponent.cs @@ -2,8 +2,13 @@ namespace Content.Shared.Administration; -[RegisterComponent, Access(typeof(AdminFrozenSystem))] -[NetworkedComponent] +[RegisterComponent, Access(typeof(SharedAdminFrozenSystem))] +[NetworkedComponent, AutoGenerateComponentState] public sealed partial class AdminFrozenComponent : Component { + /// <summary> + /// Whether the player is also muted. + /// </summary> + [DataField, AutoNetworkedField] + public bool Muted; } diff --git a/Content.Shared/Administration/BanPanelEuiState.cs b/Content.Shared/Administration/BanPanelEuiState.cs index dd10068e5da..74c340566b5 100644 --- a/Content.Shared/Administration/BanPanelEuiState.cs +++ b/Content.Shared/Administration/BanPanelEuiState.cs @@ -25,7 +25,7 @@ public sealed class CreateBanRequest : EuiMessageBase { public string? Player { get; set; } public string? IpAddress { get; set; } - public byte[]? Hwid { get; set; } + public ImmutableTypedHwid? Hwid { get; set; } public uint Minutes { get; set; } public string Reason { get; set; } public NoteSeverity Severity { get; set; } @@ -34,7 +34,7 @@ public sealed class CreateBanRequest : EuiMessageBase public bool UseLastHwid { get; set; } public bool Erase { get; set; } - public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, byte[]? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase) + public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase) { Player = player; IpAddress = ipAddress == null ? null : $"{ipAddress.Value.Item1}/{ipAddress.Value.Item2}"; diff --git a/Content.Shared/Administration/Events/BabyJailChangedEvent.cs b/Content.Shared/Administration/Events/BabyJailChangedEvent.cs new file mode 100644 index 00000000000..bbe3495f77d --- /dev/null +++ b/Content.Shared/Administration/Events/BabyJailChangedEvent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization; + +/* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + +namespace Content.Shared.Administration.Events; + +[Serializable, NetSerializable] +public sealed class BabyJailStatus +{ + public bool Enabled; + public bool ShowReason; + public int MaxAccountAgeHours; + public int MaxOverallHours; +} + +[Serializable, NetSerializable] +public sealed class BabyJailChangedEvent(BabyJailStatus status) : EntityEventArgs +{ + public BabyJailStatus Status = status; +} diff --git a/Content.Shared/Administration/PlayerPanelEuiState.cs b/Content.Shared/Administration/PlayerPanelEuiState.cs new file mode 100644 index 00000000000..186b992e4ef --- /dev/null +++ b/Content.Shared/Administration/PlayerPanelEuiState.cs @@ -0,0 +1,54 @@ +using Content.Shared.Eui; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using YamlDotNet.Serialization.Callbacks; + +namespace Content.Shared.Administration; + +[Serializable, NetSerializable] +public sealed class PlayerPanelEuiState(NetUserId guid, + string username, + TimeSpan playtime, + int? totalNotes, + int? totalBans, + int? totalRoleBans, + int sharedConnections, + bool? whitelisted, + bool canFreeze, + bool frozen, + bool canAhelp) + : EuiStateBase +{ + public readonly NetUserId Guid = guid; + public readonly string Username = username; + public readonly TimeSpan Playtime = playtime; + public readonly int? TotalNotes = totalNotes; + public readonly int? TotalBans = totalBans; + public readonly int? TotalRoleBans = totalRoleBans; + public readonly int SharedConnections = sharedConnections; + public readonly bool? Whitelisted = whitelisted; + public readonly bool CanFreeze = canFreeze; + public readonly bool Frozen = frozen; + public readonly bool CanAhelp = canAhelp; +} + + +[Serializable, NetSerializable] +public sealed class PlayerPanelFreezeMessage : EuiMessageBase +{ + public readonly bool Mute; + + public PlayerPanelFreezeMessage(bool mute = false) + { + Mute = mute; + } +} + +[Serializable, NetSerializable] +public sealed class PlayerPanelLogsMessage : EuiMessageBase; + +[Serializable, NetSerializable] +public sealed class PlayerPanelDeleteMessage : EuiMessageBase; + +[Serializable, NetSerializable] +public sealed class PlayerPanelRejuvenationMessage: EuiMessageBase; diff --git a/Content.Shared/Administration/AdminFrozenSystem.cs b/Content.Shared/Administration/SharedAdminFrozenSystem.cs similarity index 78% rename from Content.Shared/Administration/AdminFrozenSystem.cs rename to Content.Shared/Administration/SharedAdminFrozenSystem.cs index 4ec9600b0bd..2fa22e00052 100644 --- a/Content.Shared/Administration/AdminFrozenSystem.cs +++ b/Content.Shared/Administration/SharedAdminFrozenSystem.cs @@ -1,15 +1,17 @@ using Content.Shared.ActionBlocker; +using Content.Shared.Emoting; using Content.Shared.Interaction.Events; using Content.Shared.Item; using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Events; using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Speech; using Content.Shared.Throwing; namespace Content.Shared.Administration; -public sealed class AdminFrozenSystem : EntitySystem +public abstract class SharedAdminFrozenSystem : EntitySystem { [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly PullingSystem _pulling = default!; @@ -28,6 +30,16 @@ public override void Initialize() SubscribeLocalEvent<AdminFrozenComponent, PullAttemptEvent>(OnPullAttempt); SubscribeLocalEvent<AdminFrozenComponent, AttackAttemptEvent>(OnAttempt); SubscribeLocalEvent<AdminFrozenComponent, ChangeDirectionAttemptEvent>(OnAttempt); + SubscribeLocalEvent<AdminFrozenComponent, EmoteAttemptEvent>(OnEmoteAttempt); + SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt); + } + + private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, SpeakAttemptEvent args) + { + if (!component.Muted) + return; + + args.Cancel(); } private void OnAttempt(EntityUid uid, AdminFrozenComponent component, CancellableEntityEventArgs args) @@ -62,4 +74,10 @@ private void UpdateCanMove(EntityUid uid, AdminFrozenComponent component, Entity { _blocker.UpdateCanMove(uid); } + + private void OnEmoteAttempt(EntityUid uid, AdminFrozenComponent component, EmoteAttemptEvent args) + { + if (component.Muted) + args.Cancel(); + } } diff --git a/Content.Shared/Antag/IAntagStatusIconComponent.cs b/Content.Shared/Antag/IAntagStatusIconComponent.cs deleted file mode 100644 index 981937c9163..00000000000 --- a/Content.Shared/Antag/IAntagStatusIconComponent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Content.Shared.StatusIcon; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Antag; - -public interface IAntagStatusIconComponent -{ - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } - - public bool IconVisibleToGhost { get; set; } -} - diff --git a/Content.Shared/Antag/ShowAntagIconsComponent.cs b/Content.Shared/Antag/ShowAntagIconsComponent.cs new file mode 100644 index 00000000000..c451b69c3e0 --- /dev/null +++ b/Content.Shared/Antag/ShowAntagIconsComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Antag; + +/// <summary> +/// Determines whether Someone can see antags icons +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class ShowAntagIconsComponent: Component; diff --git a/Content.Shared/Atmos/Components/MovedByPressureComponent.cs b/Content.Shared/Atmos/Components/MovedByPressureComponent.cs new file mode 100644 index 00000000000..8a4e2c6d4c9 --- /dev/null +++ b/Content.Shared/Atmos/Components/MovedByPressureComponent.cs @@ -0,0 +1,31 @@ +namespace Content.Shared.Atmos.Components; + +// Unfortunately can't be friends yet due to magboots. +[RegisterComponent] +public sealed partial class MovedByPressureComponent : Component +{ + public const float MoveForcePushRatio = 1f; + public const float MoveForceForcePushRatio = 1f; + public const float ProbabilityOffset = 25f; + public const float ProbabilityBasePercent = 10f; + public const float ThrowForce = 100f; + + /// <summary> + /// Accumulates time when yeeted by high pressure deltas. + /// </summary> + [DataField] + public float Accumulator; + + [DataField] + public bool Enabled { get; set; } = true; + + [DataField] + public float PressureResistance { get; set; } = 1f; + + [DataField] + public float MoveResist { get; set; } = 100f; + + [ViewVariables(VVAccess.ReadWrite)] + public int LastHighPressureMovementAirCycle { get; set; } = 0; +} + diff --git a/Content.Shared/Atmos/GasMixture.cs b/Content.Shared/Atmos/GasMixture.cs index a676ed67204..0f1efba9766 100644 --- a/Content.Shared/Atmos/GasMixture.cs +++ b/Content.Shared/Atmos/GasMixture.cs @@ -96,6 +96,11 @@ public GasMixture(float[] moles, float temp, float volume = Atmospherics.CellVol Volume = volume; } + public GasMixture(GasMixture toClone) + { + CopyFrom(toClone); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void MarkImmutable() { @@ -197,9 +202,12 @@ public GasMixture RemoveVolume(float vol) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CopyFromMutable(GasMixture sample) + public void CopyFrom(GasMixture sample) { - if (Immutable) return; + if (Immutable) + return; + + Volume = sample.Volume; sample.Moles.CopyTo(Moles, 0); Temperature = sample.Temperature; } diff --git a/Content.Shared/Beeper/Components/BeeperComponent.cs b/Content.Shared/Beeper/Components/BeeperComponent.cs index 54d242709c4..f6efbb10f3e 100644 --- a/Content.Shared/Beeper/Components/BeeperComponent.cs +++ b/Content.Shared/Beeper/Components/BeeperComponent.cs @@ -10,15 +10,12 @@ namespace Content.Shared.Beeper.Components; /// This is used for an item that beeps based on /// proximity to a specified component. /// </summary> +/// <remarks> +/// Requires <c>ItemToggleComponent</c> to control it. +/// </remarks> [RegisterComponent, NetworkedComponent, Access(typeof(BeeperSystem)), AutoGenerateComponentState] public sealed partial class BeeperComponent : Component { - /// <summary> - /// Whether or not it's on. - /// </summary> - [DataField, AutoNetworkedField] - public bool Enabled = true; - /// <summary> /// How much to scale the interval by (< 0 = min, > 1 = max) /// </summary> @@ -56,7 +53,7 @@ public sealed partial class BeeperComponent : Component /// Is the beep muted /// </summary> [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public bool IsMuted = false; + public bool IsMuted; /// <summary> /// The sound played when the locator beeps. diff --git a/Content.Shared/Beeper/Systems/BeeperSystem.cs b/Content.Shared/Beeper/Systems/BeeperSystem.cs index c51eef4da9d..a52e19f7552 100644 --- a/Content.Shared/Beeper/Systems/BeeperSystem.cs +++ b/Content.Shared/Beeper/Systems/BeeperSystem.cs @@ -1,5 +1,7 @@ using Content.Shared.Beeper.Components; using Content.Shared.FixedPoint; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; using Robust.Shared.Timing; @@ -11,34 +13,20 @@ namespace Content.Shared.Beeper.Systems; public sealed class BeeperSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly INetManager _net = default!; - - public override void Initialize() - { - } + [Dependency] private readonly ItemToggleSystem _toggle = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; public override void Update(float frameTime) { - var query = EntityQueryEnumerator<BeeperComponent>(); - while (query.MoveNext(out var uid, out var beeper)) + var query = EntityQueryEnumerator<BeeperComponent, ItemToggleComponent>(); + while (query.MoveNext(out var uid, out var beeper, out var toggle)) { - if (!beeper.Enabled) - continue; - RunUpdate_Internal(uid, beeper); + if (toggle.Activated) + RunUpdate_Internal(uid, beeper); } } - public void SetEnable(EntityUid owner, bool isEnabled, BeeperComponent? beeper = null) - { - if (!Resolve(owner, ref beeper) || beeper.Enabled == isEnabled) - return; - beeper.Enabled = isEnabled; - - RunUpdate_Internal(owner, beeper); - Dirty(owner, beeper); - } - public void SetIntervalScaling(EntityUid owner, BeeperComponent beeper, FixedPoint2 newScaling) { newScaling = FixedPoint2.Clamp(newScaling, 0, 1); @@ -70,6 +58,7 @@ public void SetMute(EntityUid owner, bool isMuted, BeeperComponent? comp = null) if (!Resolve(owner, ref comp)) return; comp.IsMuted = isMuted; + Dirty(owner, comp); } private void UpdateBeepInterval(EntityUid owner, BeeperComponent beeper) @@ -91,19 +80,17 @@ public void ForceUpdate(EntityUid owner, BeeperComponent? beeper = null) private void RunUpdate_Internal(EntityUid owner, BeeperComponent beeper) { - if (!beeper.Enabled) - { + if (!_toggle.IsActivated(owner)) return; - } + UpdateBeepInterval(owner, beeper); if (beeper.NextBeep >= _timing.CurTime) return; + var beepEvent = new BeepPlayedEvent(beeper.IsMuted); RaiseLocalEvent(owner, ref beepEvent); if (!beeper.IsMuted && _net.IsServer) - { _audio.PlayPvs(beeper.BeepSound, owner); - } beeper.LastBeepTime = _timing.CurTime; } } diff --git a/Content.Shared/Beeper/Systems/ProximityBeeperSystem.cs b/Content.Shared/Beeper/Systems/ProximityBeeperSystem.cs index bd857d4c29b..ed3c6366c13 100644 --- a/Content.Shared/Beeper/Systems/ProximityBeeperSystem.cs +++ b/Content.Shared/Beeper/Systems/ProximityBeeperSystem.cs @@ -1,7 +1,6 @@ using Content.Shared.Beeper.Components; -using Content.Shared.Interaction.Events; +using Content.Shared.Item.ItemToggle; using Content.Shared.Pinpointer; -using Content.Shared.PowerCell; using Content.Shared.ProximityDetection; using Content.Shared.ProximityDetection.Components; using Content.Shared.ProximityDetection.Systems; @@ -9,20 +8,17 @@ namespace Content.Shared.Beeper.Systems; /// <summary> -/// This handles logic for implementing proximity beeper as a handheld tool /> +/// This handles controlling a beeper from proximity detector events. /// </summary> public sealed class ProximityBeeperSystem : EntitySystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedPowerCellSystem _powerCell = default!; [Dependency] private readonly ProximityDetectionSystem _proximity = default!; [Dependency] private readonly BeeperSystem _beeper = default!; /// <inheritdoc/> public override void Initialize() { - SubscribeLocalEvent<ProximityBeeperComponent, UseInHandEvent>(OnUseInHand); - SubscribeLocalEvent<ProximityBeeperComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); SubscribeLocalEvent<ProximityBeeperComponent, NewProximityTargetEvent>(OnNewProximityTarget); SubscribeLocalEvent<ProximityBeeperComponent, ProximityTargetUpdatedEvent>(OnProximityTargetUpdate); } @@ -33,82 +29,16 @@ private void OnProximityTargetUpdate(EntityUid owner, ProximityBeeperComponent p return; if (args.Target == null) { - _beeper.SetEnable(owner, false, beeper); + _beeper.SetMute(owner, true, beeper); return; } - _beeper.SetIntervalScaling(owner,args.Distance/args.Detector.Range, beeper); - _beeper.SetEnable(owner, true, beeper); - } - - private void OnNewProximityTarget(EntityUid owner, ProximityBeeperComponent proxBeeper, ref NewProximityTargetEvent args) - { - _beeper.SetEnable(owner, args.Target != null); - } - private void OnUseInHand(EntityUid uid, ProximityBeeperComponent proxBeeper, UseInHandEvent args) - { - if (args.Handled) - return; - args.Handled = TryToggle(uid, proxBeeper, user: args.User); + _beeper.SetIntervalScaling(owner, args.Distance / args.Detector.Range, beeper); + _beeper.SetMute(owner, false, beeper); } - private void OnPowerCellSlotEmpty(EntityUid uid, ProximityBeeperComponent beeper, ref PowerCellSlotEmptyEvent args) - { - if (_proximity.GetEnable(uid)) - TryDisable(uid); - } - public bool TryEnable(EntityUid owner, BeeperComponent? beeper = null, ProximityDetectorComponent? detector = null, - PowerCellDrawComponent? draw = null,EntityUid? user = null) - { - if (!Resolve(owner, ref beeper, ref detector)) - return false; - if (Resolve(owner, ref draw, false) && !_powerCell.HasActivatableCharge(owner, battery: draw, user: user)) - return false; - Enable(owner, beeper, detector, draw); - return true; - } - private void Enable(EntityUid owner, BeeperComponent beeper, - ProximityDetectorComponent detector, PowerCellDrawComponent? draw) - { - _proximity.SetEnable(owner, true, detector); - _appearance.SetData(owner, ProximityBeeperVisuals.Enabled, true); - _powerCell.SetPowerCellDrawEnabled(owner, true, draw); - } - - - /// <summary> - /// Disables the proximity beeper - /// </summary> - public bool TryDisable(EntityUid owner,BeeperComponent? beeper = null, ProximityDetectorComponent? detector = null, PowerCellDrawComponent? draw = null) - { - if (!Resolve(owner, ref beeper, ref detector)) - return false; - - if (!detector.Enabled) - return false; - Disable(owner, beeper, detector, draw); - return true; - } - private void Disable(EntityUid owner, BeeperComponent beeper, - ProximityDetectorComponent detector, PowerCellDrawComponent? draw) - { - _proximity.SetEnable(owner, false, detector); - _appearance.SetData(owner, ProximityBeeperVisuals.Enabled, false); - _beeper.SetEnable(owner, false, beeper); - _powerCell.SetPowerCellDrawEnabled(owner, false, draw); - } - - /// <summary> - /// toggles the proximity beeper - /// </summary> - public bool TryToggle(EntityUid owner, ProximityBeeperComponent? proxBeeper = null, BeeperComponent? beeper = null, ProximityDetectorComponent? detector = null, - PowerCellDrawComponent? draw = null, EntityUid? user = null) + private void OnNewProximityTarget(EntityUid owner, ProximityBeeperComponent proxBeeper, ref NewProximityTargetEvent args) { - if (!Resolve(owner, ref proxBeeper, ref beeper, ref detector)) - return false; - - return detector.Enabled - ? TryDisable(owner, beeper, detector, draw) - : TryEnable(owner, beeper, detector, draw,user); + _beeper.SetMute(owner, args.Target != null); } } diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index 59241499a11..c323f3dfb72 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -220,8 +220,8 @@ private bool CanBuckle(EntityUid buckleUid, return false; // Does it pass the Whitelist - if (strapComp.Whitelist != null && - !_whitelistSystem.IsValid(strapComp.Whitelist, buckleUid) || strapComp.Blacklist != null && _whitelistSystem.IsValid(strapComp.Blacklist, buckleUid)) + if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) || + _whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid)) { if (popup) _popup.PopupClient(Loc.GetString("buckle-component-cannot-fit-message"), user, PopupType.Medium); diff --git a/Content.Shared/Burial/BurialSystem.cs b/Content.Shared/Burial/BurialSystem.cs index 1116c6797b2..45a89f3be97 100644 --- a/Content.Shared/Burial/BurialSystem.cs +++ b/Content.Shared/Burial/BurialSystem.cs @@ -45,8 +45,7 @@ private void OnInteractUsing(EntityUid uid, GraveComponent component, InteractUs { var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.DigDelay / shovel.SpeedModifier, new GraveDiggingDoAfterEvent(), uid, target: args.Target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = true, BreakOnHandChange = true @@ -166,8 +165,7 @@ private void OnRelayMovement(EntityUid uid, GraveComponent component, ref Contai var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Entity, component.DigDelay / component.DigOutByHandModifier, new GraveDiggingDoAfterEvent(), uid, target: uid) { NeedHand = false, - BreakOnUserMove = true, - BreakOnTargetMove = false, + BreakOnMove = true, BreakOnHandChange = false, BreakOnDamage = false }; diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index a02cbb6419f..0ca289f4175 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -281,6 +281,13 @@ public static readonly CVarDef<bool> public static readonly CVarDef<int> SoftMaxPlayers = CVarDef.Create("game.soft_max_players", 30, CVar.SERVERONLY | CVar.ARCHIVE); + /// <summary> + /// If a player gets denied connection to the server, + /// how long they are forced to wait before attempting to reconnect. + /// </summary> + public static readonly CVarDef<int> GameServerFullReconnectDelay = + CVarDef.Create("game.server_full_reconnect_delay", 30, CVar.SERVERONLY); + /// <summary> /// Whether or not panic bunker is currently enabled. /// </summary> @@ -314,10 +321,10 @@ public static readonly CVarDef<bool> CVarDef.Create("game.panic_bunker.show_reason", false, CVar.SERVERONLY); /// <summary> - /// Minimum age of the account (from server's PoV, so from first-seen date) in minutes. + /// Minimum age of the account (from server's PoV, so from first-seen date) in hours. /// </summary> public static readonly CVarDef<int> PanicBunkerMinAccountAge = - CVarDef.Create("game.panic_bunker.min_account_age", 1440, CVar.SERVERONLY); + CVarDef.Create("game.panic_bunker.min_account_age", 24, CVar.SERVERONLY); /// <summary> /// Minimal overall played time. @@ -338,6 +345,48 @@ public static readonly CVarDef<bool> public static readonly CVarDef<bool> BypassBunkerWhitelist = CVarDef.Create("game.panic_bunker.whitelisted_can_bypass", true, CVar.SERVERONLY); + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + + /// <summary> + /// Whether the baby jail is currently enabled. + /// </summary> + public static readonly CVarDef<bool> BabyJailEnabled = + CVarDef.Create("game.baby_jail.enabled", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER); + + /// <summary> + /// Show reason of disconnect for user or not. + /// </summary> + public static readonly CVarDef<bool> BabyJailShowReason = + CVarDef.Create("game.baby_jail.show_reason", false, CVar.SERVERONLY); + + /// <summary> + /// Maximum age of the account (from server's PoV, so from first-seen date) in hours that can access baby + /// jailed servers. + /// </summary> + public static readonly CVarDef<int> BabyJailMaxAccountAge = + CVarDef.Create("game.baby_jail.max_account_age", 24, CVar.SERVERONLY); + + /// <summary> + /// Maximum overall played time allowed to access baby jailed servers. + /// </summary> + public static readonly CVarDef<int> BabyJailMaxOverallHours = + CVarDef.Create("game.baby_jail.max_overall_hours", 2, CVar.SERVERONLY); + + /// <summary> + /// A custom message that will be used for connections denied due to the baby jail. + /// If not empty, then will overwrite <see cref="BabyJailShowReason"/> + /// </summary> + public static readonly CVarDef<string> BabyJailCustomReason = + CVarDef.Create("game.baby_jail.custom_reason", string.Empty, CVar.SERVERONLY); + + /// <summary> + /// Allow bypassing the baby jail if the user is whitelisted. + /// </summary> + public static readonly CVarDef<bool> BypassBabyJailWhitelist = + CVarDef.Create("game.baby_jail.whitelisted_can_bypass", true, CVar.SERVERONLY); + /// <summary> /// Make people bonk when trying to climb certain objects like tables. /// </summary> @@ -925,6 +974,15 @@ public static readonly CVarDef<bool> public static readonly CVarDef<bool> ServerBanErasePlayer = CVarDef.Create("admin.server_ban_erase_player", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED); + /// <summary> + /// Minimum players sharing a connection required to create an alert. -1 to disable the alert. + /// </summary> + /// <remarks> + /// If you set this to 0 or 1 then it will alert on every connection, so probably don't do that. + /// </remarks> + public static readonly CVarDef<int> AdminAlertMinPlayersSharingConnection = + CVarDef.Create("admin.alert.min_players_sharing_connection", -1, CVar.SERVERONLY); + /// <summary> /// Minimum explosion intensity to create an admin alert message. -1 to disable the alert. /// </summary> @@ -985,6 +1043,15 @@ public static readonly CVarDef<bool> public static readonly CVarDef<bool> AdminUseCustomNamesAdminRank = CVarDef.Create("admin.use_custom_names_admin_rank", true, CVar.SERVERONLY); + /// <summary> + /// Determines whether admins count towards the total playercount when determining whether the server is over <see cref="SoftMaxPlayers"/> + /// Ideally this should be used in conjuction with <see cref="AdminBypassPlayers"/>. + /// This also applies to playercount limits in whitelist conditions + /// If false, then admins will not be considered when checking whether the playercount is already above the soft player cap + /// </summary> + public static readonly CVarDef<bool> AdminsCountForMaxPlayers = + CVarDef.Create("admin.admins_count_for_max_players", false, CVar.SERVERONLY); + /* * AHELP */ @@ -1508,22 +1575,11 @@ public static readonly CVarDef<bool> CVarDef.Create("whitelist.enabled", false, CVar.REPLICATED); /// <summary> - /// The loc string to display as a disconnect reason when someone is not whitelisted. - /// </summary> - public static readonly CVarDef<string> WhitelistReason = - CVarDef.Create("whitelist.reason", "whitelist-not-whitelisted", CVar.SERVERONLY); - - /// <summary> - /// If the playercount is below this number, the whitelist will not apply. - /// </summary> - public static readonly CVarDef<int> WhitelistMinPlayers = - CVarDef.Create("whitelist.min_players", 0, CVar.SERVERONLY); - - /// <summary> - /// If the playercount is above this number, the whitelist will not apply. + /// Specifies the whitelist prototypes to be used by the server. This should be a comma-separated list of prototypes. + /// If a whitelists conditions to be active fail (for example player count), the next whitelist will be used instead. If no whitelist is valid, the player will be allowed to connect. /// </summary> - public static readonly CVarDef<int> WhitelistMaxPlayers = - CVarDef.Create("whitelist.max_players", int.MaxValue, CVar.SERVERONLY); + public static readonly CVarDef<string> WhitelistPrototypeList = + CVarDef.Create("whitelist.prototype_list", "basicWhitelist", CVar.SERVERONLY); /* * VOTE @@ -1644,7 +1700,7 @@ public static readonly CVarDef<int> /// Whether the arrivals terminal should be on a planet map. /// </summary> public static readonly CVarDef<bool> ArrivalsPlanet = - CVarDef.Create("shuttle.arrivals_planet", true, CVar.SERVERONLY); + CVarDef.Create("shuttle.arrivals_planet", false, CVar.SERVERONLY); /// <summary> /// Whether the arrivals shuttle is enabled. @@ -1985,7 +2041,7 @@ public static readonly CVarDef<int> /// Disables all vision filters for species like Vulpkanin or Harpies. There are good reasons someone might want to disable these. /// </summary> public static readonly CVarDef<bool> NoVisionFilters = - CVarDef.Create("accessibility.no_vision_filters", false, CVar.CLIENTONLY | CVar.ARCHIVE); + CVarDef.Create("accessibility.no_vision_filters", true, CVar.CLIENTONLY | CVar.ARCHIVE); /* * CHAT @@ -2170,13 +2226,7 @@ public static readonly CVarDef<string> /// Don't show rules to localhost/loopback interface. /// </summary> public static readonly CVarDef<bool> RulesExemptLocal = - CVarDef.Create("rules.exempt_local", true, CVar.CLIENT); - - /// <summary> - /// The next time the rules will popup for this client, expressed in minutes - /// </summary> - public static readonly CVarDef<string> RulesNextPopupTime = - CVarDef.Create("rules.next_popup_time", "Jan 1, 1997", CVar.CLIENTONLY | CVar.ARCHIVE); + CVarDef.Create("rules.exempt_local", false, CVar.SERVERONLY); /* diff --git a/Content.Shared/Cargo/CargoOrderData.cs b/Content.Shared/Cargo/CargoOrderData.cs index ce05d922362..7b3490630e6 100644 --- a/Content.Shared/Cargo/CargoOrderData.cs +++ b/Content.Shared/Cargo/CargoOrderData.cs @@ -63,6 +63,11 @@ public CargoOrderData(int orderId, string productId, string productName, int pri Reason = reason; } + public void SetApproverData(string? approver) + { + Approver = approver; + } + public void SetApproverData(string? fullName, string? jobTitle) { var sb = new StringBuilder(); diff --git a/Content.Shared/Cargo/Components/BankClientComponent.cs b/Content.Shared/Cargo/Components/BankClientComponent.cs new file mode 100644 index 00000000000..4fd70855034 --- /dev/null +++ b/Content.Shared/Cargo/Components/BankClientComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Cargo; +using Robust.Shared.GameStates; + +namespace Content.Shared.Cargo.Components; + +/// <summary> +/// Makes an entity a client of the station's bank account. +/// When its balance changes it will have <see cref="BankBalanceUpdatedEvent"/> raised on it. +/// Other systems can then use this for logic or to update ui states. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem))] +[AutoGenerateComponentState] +public sealed partial class BankClientComponent : Component +{ + /// <summary> + /// The balance updated for the last station this entity was a part of. + /// </summary> + [DataField, AutoNetworkedField] + public int Balance; +} + +/// <summary> +/// Raised on an entity with <see cref="BankClientComponent"/> when the bank's balance is updated. +/// </summary> +[ByRefEvent] +public record struct BankBalanceUpdatedEvent(EntityUid Station, int Balance); diff --git a/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs index bf4907b0dd4..b40b03672ef 100644 --- a/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs +++ b/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs @@ -31,7 +31,7 @@ public sealed partial class CargoBountyPrototype : IPrototype /// <summary> /// The entries that must be satisfied for the cargo bounty to be complete. /// </summary> - [DataField( required: true)] + [DataField(required: true)] public List<CargoBountyItemEntry> Entries = new(); /// <summary> @@ -50,6 +50,12 @@ public readonly partial record struct CargoBountyItemEntry() [DataField(required: true)] public EntityWhitelist Whitelist { get; init; } = default!; + /// <summary> + /// A blacklist that can be used to exclude items in the whitelist. + /// </summary> + [DataField] + public EntityWhitelist? Blacklist { get; init; } = null; + // todo: implement some kind of simple generic condition system /// <summary> diff --git a/Content.Shared/CartridgeLoader/CartridgeUiMessage.cs b/Content.Shared/CartridgeLoader/CartridgeUiMessage.cs index 1155030f938..31ac8bd2d06 100644 --- a/Content.Shared/CartridgeLoader/CartridgeUiMessage.cs +++ b/Content.Shared/CartridgeLoader/CartridgeUiMessage.cs @@ -17,4 +17,7 @@ public CartridgeUiMessage(CartridgeMessageEvent messageEvent) public abstract class CartridgeMessageEvent : EntityEventArgs { public NetEntity LoaderUid; + + [NonSerialized] + public EntityUid Actor; } diff --git a/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs b/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs index 9dc507b7e51..86bbb655474 100644 --- a/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs +++ b/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Serialization; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV +using Robust.Shared.Serialization; namespace Content.Shared.CartridgeLoader.Cartridges; @@ -10,9 +11,15 @@ public sealed class LogProbeUiState : BoundUserInterfaceState /// </summary> public List<PulledAccessLog> PulledLogs; - public LogProbeUiState(List<PulledAccessLog> pulledLogs) + /// <summary> + /// DeltaV: The NanoChat data if a card was scanned, null otherwise + /// </summary> + public NanoChatData? NanoChatData { get; } + + public LogProbeUiState(List<PulledAccessLog> pulledLogs, NanoChatData? nanoChatData = null) // DeltaV - NanoChat support { PulledLogs = pulledLogs; + NanoChatData = nanoChatData; // DeltaV } } diff --git a/Content.Shared/Charges/Systems/SharedChargesSystem.cs b/Content.Shared/Charges/Systems/SharedChargesSystem.cs index 5de1383cde0..7f95ef184e4 100644 --- a/Content.Shared/Charges/Systems/SharedChargesSystem.cs +++ b/Content.Shared/Charges/Systems/SharedChargesSystem.cs @@ -5,10 +5,14 @@ namespace Content.Shared.Charges.Systems; public abstract class SharedChargesSystem : EntitySystem { + protected EntityQuery<LimitedChargesComponent> Query; + public override void Initialize() { base.Initialize(); + Query = GetEntityQuery<LimitedChargesComponent>(); + SubscribeLocalEvent<LimitedChargesComponent, ExaminedEvent>(OnExamine); } @@ -30,9 +34,9 @@ protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, Ex /// <summary> /// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges. /// </summary> - public void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null) + public virtual void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null) { - if (!Resolve(uid, ref comp, false)) + if (!Query.Resolve(uid, ref comp, false)) return; var old = comp.Charges; @@ -47,7 +51,7 @@ public void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null) { // can't be empty if there are no limited charges - if (!Resolve(uid, ref comp, false)) + if (!Query.Resolve(uid, ref comp, false)) return false; return comp.Charges <= 0; @@ -56,10 +60,24 @@ public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null) /// <summary> /// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge. /// </summary> - public virtual void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null) + public void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null) + { + AddCharges(uid, -1, comp); + } + + /// <summary> + /// Checks IsEmpty and uses a charge if it isn't empty. + /// </summary> + public bool TryUseCharge(Entity<LimitedChargesComponent?> ent) { - if (Resolve(uid, ref comp, false)) - AddCharges(uid, -1, comp); + if (!Query.Resolve(ent, ref ent.Comp, false)) + return true; + + if (IsEmpty(ent, ent.Comp)) + return false; + + UseCharge(ent, ent.Comp); + return true; } /// <summary> @@ -80,7 +98,6 @@ public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedCh /// </summary> public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null) { - if (Resolve(uid, ref comp, false)) - AddCharges(uid, -chargesUsed, comp); + AddCharges(uid, -chargesUsed, comp); } } diff --git a/Content.Shared/Chat/SharedChatEvents.cs b/Content.Shared/Chat/SharedChatEvents.cs index 6d5c435e055..c187fd59a8d 100644 --- a/Content.Shared/Chat/SharedChatEvents.cs +++ b/Content.Shared/Chat/SharedChatEvents.cs @@ -1,4 +1,4 @@ -using Content.Shared.Speech; +using Content.Shared.Speech; using Robust.Shared.Prototypes; using Content.Shared.Inventory; @@ -8,14 +8,14 @@ namespace Content.Shared.Chat; /// This event should be sent everytime an entity talks (Radio, local chat, etc...). /// The event is sent to both the entity itself, and all clothing (For stuff like voice masks). /// </summary> -public sealed class TransformSpeakerSpeechEvent : EntityEventArgs, IInventoryRelayEvent +public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelayEvent { public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; public EntityUid Sender; - public string? VoiceName; + public string VoiceName; public ProtoId<SpeechVerbPrototype>? SpeechVerb; - public TransformSpeakerSpeechEvent(EntityUid sender, string? name = null) + public TransformSpeakerNameEvent(EntityUid sender, string name) { Sender = sender; VoiceName = name; diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index 14cb37d18bb..ff2a30ccbdb 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using System.Text.RegularExpressions; using Content.Shared.Popups; using Content.Shared.Radio; using Content.Shared.Speech; @@ -70,25 +71,19 @@ public SpeechVerbPrototype GetSpeechVerb(EntityUid source, string message, Speec if (!Resolve(source, ref speech, false)) return _prototypeManager.Index<SpeechVerbPrototype>(DefaultSpeechVerb); - var evt = new TransformSpeakerSpeechEvent(source); - RaiseLocalEvent(source, evt); - - SpeechVerbPrototype? speechProto = null; - if (evt.SpeechVerb != null && _prototypeManager.TryIndex(evt.SpeechVerb, out var evntProto)) - speechProto = evntProto; - // check for a suffix-applicable speech verb + SpeechVerbPrototype? current = null; foreach (var (str, id) in speech.SuffixSpeechVerbs) { - var proto = _prototypeManager.Index<SpeechVerbPrototype>(id); - if (message.EndsWith(Loc.GetString(str)) && proto.Priority >= (speechProto?.Priority ?? 0)) + var proto = _prototypeManager.Index(id); + if (message.EndsWith(Loc.GetString(str)) && proto.Priority >= (current?.Priority ?? 0)) { - speechProto = proto; + current = proto; } } // if no applicable suffix verb return the normal one used by the entity - return speechProto ?? _prototypeManager.Index<SpeechVerbPrototype>(speech.SpeechVerb); + return current ?? _prototypeManager.Index(speech.SpeechVerb); } /// <summary> @@ -245,6 +240,18 @@ public static string InjectTagInsideTag(ChatMessage message, string outerTag, st return rawmsg; } + + /// <summary> + /// Injects a tag around all found instances of a specific string in a ChatMessage. + /// Excludes strings inside other tags and brackets. + /// </summary> + public static string InjectTagAroundString(ChatMessage message, string targetString, string tag, string? tagParameter) + { + var rawmsg = message.WrappedMessage; + rawmsg = Regex.Replace(rawmsg, "(?i)(" + targetString + ")(?-i)(?![^[]*])", $"[{tag}={tagParameter}]$1[/{tag}]"); + return rawmsg; + } + public static string GetStringInsideTag(ChatMessage message, string tag) { var rawmsg = message.WrappedMessage; diff --git a/Content.Shared/Chat/SharedSuicideSystem.cs b/Content.Shared/Chat/SharedSuicideSystem.cs new file mode 100644 index 00000000000..15f75c526b1 --- /dev/null +++ b/Content.Shared/Chat/SharedSuicideSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Mobs.Components; +using Robust.Shared.Prototypes; +using System.Linq; + +namespace Content.Shared.Chat; + +public sealed class SharedSuicideSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + /// <summary> + /// Applies lethal damage spread out across the damage types given. + /// </summary> + public void ApplyLethalDamage(Entity<DamageableComponent> target, DamageSpecifier damageSpecifier) + { + // Create a new damageSpecifier so that we don't make alterations to the original DamageSpecifier + // Failing to do this will permanently change a weapon's damage making it insta-kill people + var appliedDamageSpecifier = new DamageSpecifier(damageSpecifier); + if (!TryComp<MobThresholdsComponent>(target, out var mobThresholds)) + return; + + // Mob thresholds are sorted from alive -> crit -> dead, + // grabbing the last key will give us how much damage is needed to kill a target from zero + // The exact lethal damage amount is adjusted based on their current damage taken + var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage; + var totalDamage = appliedDamageSpecifier.GetTotal(); + + // Removing structural because it causes issues against entities that cannot take structural damage, + // then getting the total to use in calculations for spreading out damage. + appliedDamageSpecifier.DamageDict.Remove("Structural"); + + // Split the total amount of damage needed to kill the target by every damage type in the DamageSpecifier + foreach (var (key, value) in appliedDamageSpecifier.DamageDict) + appliedDamageSpecifier.DamageDict[key] = Math.Ceiling((double) (value * lethalAmountOfDamage / totalDamage)); + + _damageableSystem.TryChangeDamage(target, appliedDamageSpecifier, true, origin: target); + } + + /// <summary> + /// Applies lethal damage in a single type, specified by a single damage type. + /// </summary> + public void ApplyLethalDamage(Entity<DamageableComponent> target, ProtoId<DamageTypePrototype>? damageType) + { + if (!TryComp<MobThresholdsComponent>(target, out var mobThresholds)) + return; + + // Mob thresholds are sorted from alive -> crit -> dead, + // grabbing the last key will give us how much damage is needed to kill a target from zero + // The exact lethal damage amount is adjusted based on their current damage taken + var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage; + + // We don't want structural damage for the same reasons listed above + if (!_prototypeManager.TryIndex(damageType, out var damagePrototype) || damagePrototype.ID == "Structural") + { + Log.Error($"{nameof(SharedSuicideSystem)} could not find the damage type prototype associated with {damageType}. Falling back to Blunt"); + damagePrototype = _prototypeManager.Index<DamageTypePrototype>("Blunt"); + } + + var damage = new DamageSpecifier(damagePrototype, lethalAmountOfDamage); + _damageableSystem.TryChangeDamage(target, damage, true, origin: target); + } +} diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs index 05ef84bbaf7..17d52f0ad93 100644 --- a/Content.Shared/Chemistry/Components/HyposprayComponent.cs +++ b/Content.Shared/Chemistry/Components/HyposprayComponent.cs @@ -11,11 +11,6 @@ public sealed partial class HyposprayComponent : Component [DataField] public string SolutionName = "hypospray"; - // TODO: This should be on clumsycomponent. - [DataField] - [ViewVariables(VVAccess.ReadWrite)] - public float ClumsyFailChance = 0.5f; - [DataField] [ViewVariables(VVAccess.ReadWrite)] public FixedPoint2 TransferAmount = FixedPoint2.New(5); diff --git a/Content.Shared/Chemistry/Events/HyposprayEvents.cs b/Content.Shared/Chemistry/Events/HyposprayEvents.cs new file mode 100644 index 00000000000..e8ed081a577 --- /dev/null +++ b/Content.Shared/Chemistry/Events/HyposprayEvents.cs @@ -0,0 +1,38 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Chemistry.Hypospray.Events; + +public abstract partial class BeforeHyposprayInjectsTargetEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public EntityUid EntityUsingHypospray; + public readonly EntityUid Hypospray; + public EntityUid TargetGettingInjected; + public string? InjectMessageOverride; + + public BeforeHyposprayInjectsTargetEvent(EntityUid user, EntityUid hypospray, EntityUid target) + { + EntityUsingHypospray = user; + Hypospray = hypospray; + TargetGettingInjected = target; + InjectMessageOverride = null; + } +} + +/// <summary> +/// This event is raised on the user using the hypospray before the hypospray is injected. +/// The event is triggered on the user and all their clothing. +/// </summary> +public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent +{ + public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } +} + +/// <summary> +/// This event is raised on the target before the hypospray is injected. +/// The event is triggered on the target itself and all its clothing. +/// </summary> +public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent +{ + public TargetBeforeHyposprayInjectsEvent (EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } +} diff --git a/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs index f2b13d34881..f9dfa9b2734 100644 --- a/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs +++ b/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; @@ -197,9 +198,7 @@ private List<string> PerformReaction(Entity<SolutionComponent> soln, ReactionPro private void OnReaction(Entity<SolutionComponent> soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions) { - var args = new ReagentEffectArgs(soln, null, soln.Comp.Solution, - reagent, - unitReactions, EntityManager, null, 1f); + var args = new EntityEffectReagentArgs(soln, EntityManager, null, soln.Comp.Solution, unitReactions, reagent, null, 1f); var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos); @@ -213,7 +212,7 @@ private void OnReaction(Entity<SolutionComponent> soln, ReactionPrototype reacti if (effect.ShouldLog) { - var entity = args.SolutionEntity; + var entity = args.TargetEntity; _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, $"Reaction effect {effect.GetType().Name:effect} of reaction {reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}"); } diff --git a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs index f4e9619b172..351aa50b0f3 100644 --- a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs +++ b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs @@ -1,5 +1,6 @@ using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -59,7 +60,7 @@ public sealed partial class ReactionPrototype : IPrototype, IComparable<Reaction /// <summary> /// Effects to be triggered when the reaction occurs. /// </summary> - [DataField("effects", serverOnly: true)] public List<ReagentEffect> Effects = new(); + [DataField("effects", serverOnly: true)] public List<EntityEffect> Effects = new(); /// <summary> /// How dangerous is this effect? Stuff like bicaridine should be low, while things like methamphetamine diff --git a/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs b/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs index 310129c6fc7..cabdee93c1a 100644 --- a/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs +++ b/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Chemistry.Reagent; +using Content.Shared.EntityEffects; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; @@ -33,7 +34,7 @@ public sealed partial class ReactiveReagentEffectEntry public HashSet<string>? Reagents = null; [DataField("effects", required: true)] - public List<ReagentEffect> Effects = default!; + public List<EntityEffect> Effects = default!; [DataField("groups", readOnly: true, serverOnly: true, customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<ReactionMethod>, ReactiveGroupPrototype>))] diff --git a/Content.Shared/Chemistry/ReactiveSystem.cs b/Content.Shared/Chemistry/ReactiveSystem.cs index 57b008de0c7..edd75bb0b4c 100644 --- a/Content.Shared/Chemistry/ReactiveSystem.cs +++ b/Content.Shared/Chemistry/ReactiveSystem.cs @@ -3,102 +3,107 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; +using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Shared.Chemistry +namespace Content.Shared.Chemistry; + +[UsedImplicitly] +public sealed class ReactiveSystem : EntitySystem { - [UsedImplicitly] - public sealed class ReactiveSystem : EntitySystem - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method) + public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method) + { + foreach (var reagent in solution.Contents.ToArray()) { - foreach (var reagent in solution.Contents.ToArray()) - { - ReactionEntity(uid, method, reagent, solution); - } + ReactionEntity(uid, method, reagent, solution); } + } - public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source) - { - // We throw if the reagent specified doesn't exist. - var proto = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype); - ReactionEntity(uid, method, proto, reagentQuantity, source); - } + public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source) + { + // We throw if the reagent specified doesn't exist. + var proto = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype); + ReactionEntity(uid, method, proto, reagentQuantity, source); + } - public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto, - ReagentQuantity reagentQuantity, Solution? source) - { - if (!TryComp(uid, out ReactiveComponent? reactive)) - return; + public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto, + ReagentQuantity reagentQuantity, Solution? source) + { + if (!TryComp(uid, out ReactiveComponent? reactive)) + return; - // If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified. - var args = new ReagentEffectArgs(uid, null, source, proto, - source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, EntityManager, method, 1f); + // If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified. + var args = new EntityEffectReagentArgs(uid, EntityManager, null, source, source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, proto, method, 1f); - // First, check if the reagent wants to apply any effects. - if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null) + // First, check if the reagent wants to apply any effects. + if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null) + { + foreach (var (key, val) in proto.ReactiveEffects) { - foreach (var (key, val) in proto.ReactiveEffects) - { - if (!val.Methods.Contains(method)) - continue; + if (!val.Methods.Contains(method)) + continue; - if (!reactive.ReactiveGroups.ContainsKey(key)) - continue; + if (!reactive.ReactiveGroups.ContainsKey(key)) + continue; - if (!reactive.ReactiveGroups[key].Contains(method)) + if (!reactive.ReactiveGroups[key].Contains(method)) + continue; + + foreach (var effect in val.Effects) + { + if (!effect.ShouldApply(args, _robustRandom)) continue; - foreach (var effect in val.Effects) + if (effect.ShouldLog) { - if (!effect.ShouldApply(args, _robustRandom)) - continue; - - if (effect.ShouldLog) - { - var entity = args.SolutionEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}"); - } - - effect.Effect(args); + var entity = args.TargetEntity; + _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, + $"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}"); } + + effect.Effect(args); } } + } - // Then, check if the prototype has any effects it can apply as well. - if (reactive.Reactions != null) + // Then, check if the prototype has any effects it can apply as well. + if (reactive.Reactions != null) + { + foreach (var entry in reactive.Reactions) { - foreach (var entry in reactive.Reactions) - { - if (!entry.Methods.Contains(method)) - continue; + if (!entry.Methods.Contains(method)) + continue; + + if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID)) + continue; - if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID)) + foreach (var effect in entry.Effects) + { + if (!effect.ShouldApply(args, _robustRandom)) continue; - foreach (var effect in entry.Effects) + if (effect.ShouldLog) { - if (!effect.ShouldApply(args, _robustRandom)) - continue; - - if (effect.ShouldLog) - { - var entity = args.SolutionEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}"); - } - - effect.Effect(args); + var entity = args.TargetEntity; + _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, + $"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}"); } + + effect.Effect(args); } } } } } +public enum ReactionMethod +{ +Touch, +Injection, +Ingestion, +} diff --git a/Content.Shared/Chemistry/Reagent/ReagentEffect.cs b/Content.Shared/Chemistry/Reagent/ReagentEffect.cs deleted file mode 100644 index 41eea55cca4..00000000000 --- a/Content.Shared/Chemistry/Reagent/ReagentEffect.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Linq; -using System.Text.Json.Serialization; -using Content.Shared.Chemistry.Components; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Localizations; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.Chemistry.Reagent -{ - /// <summary> - /// Reagent effects describe behavior that occurs when a reagent is ingested and metabolized by some - /// organ. They only trigger when all of <see cref="Conditions"/> are satisfied. - /// </summary> - [ImplicitDataDefinitionForInheritors] - [MeansImplicitUse] - public abstract partial class ReagentEffect - { - [JsonPropertyName("id")] private protected string _id => this.GetType().Name; - /// <summary> - /// The list of conditions required for the effect to activate. Not required. - /// </summary> - [JsonPropertyName("conditions")] - [DataField("conditions")] - public ReagentEffectCondition[]? Conditions; - - public virtual string ReagentEffectFormat => "guidebook-reagent-effect-description"; - - protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys); // => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability)); - - /// <summary> - /// What's the chance, from 0 to 1, that this effect will occur? - /// </summary> - [JsonPropertyName("probability")] - [DataField("probability")] - public float Probability = 1.0f; - - [JsonIgnore] - [DataField("logImpact")] - public virtual LogImpact LogImpact { get; private set; } = LogImpact.Low; - - /// <summary> - /// Should this reagent effect log at all? - /// </summary> - [JsonIgnore] - [DataField("shouldLog")] - public virtual bool ShouldLog { get; private set; } = false; - - public abstract void Effect(ReagentEffectArgs args); - - /// <summary> - /// Produces a localized, bbcode'd guidebook description for this effect. - /// </summary> - /// <returns></returns> - public string? GuidebookEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var effect = ReagentEffectGuidebookText(prototype, entSys); - if (effect is null) - return null; - - return Loc.GetString(ReagentEffectFormat, ("effect", effect), ("chance", Probability), - ("conditionCount", Conditions?.Length ?? 0), - ("conditions", - ContentLocalizationManager.FormatList(Conditions?.Select(x => x.GuidebookExplanation(prototype)).ToList() ?? - new List<string>()))); - } - } - - public static class ReagentEffectExt - { - public static bool ShouldApply(this ReagentEffect effect, ReagentEffectArgs args, - IRobustRandom? random = null) - { - if (random == null) - random = IoCManager.Resolve<IRobustRandom>(); - - if (effect.Probability < 1.0f && !random.Prob(effect.Probability)) - return false; - - if (effect.Conditions != null) - { - foreach (var cond in effect.Conditions) - { - if (!cond.Condition(args)) - return false; - } - } - - return true; - } - } - - public enum ReactionMethod - { - Touch, - Injection, - Ingestion, - } - - public readonly record struct ReagentEffectArgs( - EntityUid SolutionEntity, - EntityUid? OrganEntity, - Solution? Source, - ReagentPrototype? Reagent, - FixedPoint2 Quantity, - IEntityManager EntityManager, - ReactionMethod? Method, - float Scale = 1f, - float QuantityMultiplier = 1f - ); -} diff --git a/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs b/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs deleted file mode 100644 index a32b4fa8e3a..00000000000 --- a/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Chemistry.Reagent -{ - [ImplicitDataDefinitionForInheritors] - [MeansImplicitUse] - public abstract partial class ReagentEffectCondition - { - [JsonPropertyName("id")] private protected string _id => this.GetType().Name; - - public abstract bool Condition(ReagentEffectArgs args); - - /// <summary> - /// Effect explanations are of the form "[chance to] [action] when [condition] and [condition]" - /// </summary> - /// <param name="prototype"></param> - /// <returns></returns> - public abstract string GuidebookExplanation(IPrototypeManager prototype); - } -} diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index df1b1aa20b4..08bee30a0f1 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -1,10 +1,11 @@ -using System.Collections.Frozen; +using System.Collections.Frozen; using System.Linq; using System.Text.Json.Serialization; using Content.Shared.Administration.Logs; using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; +using Content.Shared.EntityEffects; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Nutrition; @@ -134,7 +135,7 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype public List<ITileReaction> TileReactions = new(0); [DataField("plantMetabolism", serverOnly: true)] - public List<ReagentEffect> PlantMetabolisms = new(0); + public List<EntityEffect> PlantMetabolisms = new(0); [DataField] public float PricePerUnit; @@ -170,7 +171,7 @@ public void ReactionPlant(EntityUid? plantHolder, ReagentQuantity amount, Soluti var entMan = IoCManager.Resolve<IEntityManager>(); var random = IoCManager.Resolve<IRobustRandom>(); - var args = new ReagentEffectArgs(plantHolder.Value, null, solution, this, amount.Quantity, entMan, null, 1f); + var args = new EntityEffectReagentArgs(plantHolder.Value, entMan, null, solution, amount.Quantity, this, null, 1f); foreach (var plantMetabolizable in PlantMetabolisms) { if (!plantMetabolizable.ShouldApply(args, random)) @@ -178,7 +179,7 @@ public void ReactionPlant(EntityUid? plantHolder, ReagentQuantity amount, Soluti if (plantMetabolizable.ShouldLog) { - var entity = args.SolutionEntity; + var entity = args.TargetEntity; entMan.System<SharedAdminLogSystem>().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<TransformComponent>(entity).Coordinates:coordinates}"); } @@ -220,7 +221,7 @@ public sealed partial class ReagentEffectsEntry /// </summary> [JsonPropertyName("effects")] [DataField("effects", required: true)] - public ReagentEffect[] Effects = default!; + public EntityEffect[] Effects = default!; public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys) { @@ -254,6 +255,6 @@ public sealed partial class ReactiveReagentEffectEntry public HashSet<ReactionMethod> Methods = default!; [DataField("effects", required: true)] - public ReagentEffect[] Effects = default!; + public EntityEffect[] Effects = default!; } } diff --git a/Content.Shared/Chemistry/SharedChemMaster.cs b/Content.Shared/Chemistry/SharedChemMaster.cs index 762131d7612..8fca24fcdb0 100644 --- a/Content.Shared/Chemistry/SharedChemMaster.cs +++ b/Content.Shared/Chemistry/SharedChemMaster.cs @@ -94,7 +94,10 @@ public enum ChemMasterReagentAmount U1 = 1, U5 = 5, U10 = 10, + U15 = 15, U25 = 25, + U30 = 30, + U45 = 45, U50 = 50, U100 = 100, All, diff --git a/Content.Shared/Climbing/Components/BonkableComponent.cs b/Content.Shared/Climbing/Components/BonkableComponent.cs index 5e97396fbad..cb4839cae71 100644 --- a/Content.Shared/Climbing/Components/BonkableComponent.cs +++ b/Content.Shared/Climbing/Components/BonkableComponent.cs @@ -1,5 +1,4 @@ using Content.Shared.Damage; -using Robust.Shared.Audio; using Robust.Shared.GameStates; namespace Content.Shared.Climbing.Components; @@ -8,39 +7,18 @@ namespace Content.Shared.Climbing.Components; /// Makes entity do damage and stun entities with ClumsyComponent /// upon DragDrop or Climb interactions. /// </summary> -[RegisterComponent, NetworkedComponent, Access(typeof(Systems.BonkSystem))] +[RegisterComponent, NetworkedComponent] public sealed partial class BonkableComponent : Component { /// <summary> - /// Chance of bonk triggering if the user is clumsy. + /// How long to stun players on bonk, in seconds. /// </summary> - [DataField("bonkClumsyChance")] - public float BonkClumsyChance = 0.5f; + [DataField] + public TimeSpan BonkTime = TimeSpan.FromSeconds(2); /// <summary> - /// Sound to play when bonking. + /// How much damage to apply on bonk. /// </summary> - /// <seealso cref="Bonk"/> - [DataField("bonkSound")] - public SoundSpecifier? BonkSound; - - /// <summary> - /// How long to stun players on bonk, in seconds. - /// </summary> - /// <seealso cref="Bonk"/> - [DataField("bonkTime")] - public float BonkTime = 2; - - /// <summary> - /// How much damage to apply on bonk. - /// </summary> - /// <seealso cref="Bonk"/> - [DataField("bonkDamage")] + [DataField] public DamageSpecifier? BonkDamage; - - /// <summary> - /// How long it takes to bonk. - /// </summary> - [DataField("bonkDelay")] - public float BonkDelay = 1.5f; } diff --git a/Content.Shared/Climbing/Events/BeforeClimbEvents.cs b/Content.Shared/Climbing/Events/BeforeClimbEvents.cs new file mode 100644 index 00000000000..85c40f9427c --- /dev/null +++ b/Content.Shared/Climbing/Events/BeforeClimbEvents.cs @@ -0,0 +1,36 @@ +using Content.Shared.Inventory; +using Content.Shared.Climbing.Components; + +namespace Content.Shared.Climbing.Events; + +public abstract partial class BeforeClimbEvent : CancellableEntityEventArgs +{ + public readonly EntityUid GettingPutOnTable; + public readonly EntityUid PuttingOnTable; + public readonly Entity<ClimbableComponent> BeingClimbedOn; + + public BeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity<ClimbableComponent> beingClimbedOn) + { + GettingPutOnTable = gettingPutOntable; + PuttingOnTable = puttingOnTable; + BeingClimbedOn = beingClimbedOn; + } +} + +/// <summary> +/// This event is raised on the the person either getting put on or going on the table. +/// The event is also called on their clothing as well. +/// </summary> +public sealed class SelfBeforeClimbEvent : BeforeClimbEvent, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public SelfBeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity<ClimbableComponent> beingClimbedOn) : base(gettingPutOntable, puttingOnTable, beingClimbedOn) { } +} + +/// <summary> +/// This event is raised on the thing being climbed on. +/// </summary> +public sealed class TargetBeforeClimbEvent : BeforeClimbEvent +{ + public TargetBeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity<ClimbableComponent> beingClimbedOn) : base(gettingPutOntable, puttingOnTable, beingClimbedOn) { } +} diff --git a/Content.Shared/Climbing/Systems/BonkSystem.cs b/Content.Shared/Climbing/Systems/BonkSystem.cs deleted file mode 100644 index c7c89a3b7fa..00000000000 --- a/Content.Shared/Climbing/Systems/BonkSystem.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Content.Shared.CCVar; -using Content.Shared.Climbing.Components; -using Content.Shared.Climbing.Events; -using Content.Shared.Damage; -using Content.Shared.DoAfter; -using Content.Shared.DragDrop; -using Content.Shared.Hands.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Components; -using Content.Shared.Popups; -using Content.Shared.Stunnable; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Configuration; -using Robust.Shared.Player; -using Robust.Shared.Serialization; - -namespace Content.Shared.Climbing.Systems; - -public sealed partial class BonkSystem : EntitySystem -{ - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly SharedStunSystem _stunSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent<BonkableComponent, BonkDoAfterEvent>(OnBonkDoAfter); - SubscribeLocalEvent<BonkableComponent, AttemptClimbEvent>(OnAttemptClimb); - } - - private void OnBonkDoAfter(EntityUid uid, BonkableComponent component, BonkDoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Args.Used == null) - return; - - TryBonk(args.Args.Used.Value, uid, component, source: args.Args.User); - - args.Handled = true; - } - - - public bool TryBonk(EntityUid user, EntityUid bonkableUid, BonkableComponent? bonkableComponent = null, EntityUid? source = null) - { - if (!Resolve(bonkableUid, ref bonkableComponent, false)) - return false; - - // BONK! - var userName = Identity.Entity(user, EntityManager); - var bonkableName = Identity.Entity(bonkableUid, EntityManager); - - if (user == source) - { - // Non-local, non-bonking players - var othersMessage = Loc.GetString("bonkable-success-message-others", ("user", userName), ("bonkable", bonkableName)); - // Local, bonking player - var selfMessage = Loc.GetString("bonkable-success-message-user", ("user", userName), ("bonkable", bonkableName)); - - _popupSystem.PopupPredicted(selfMessage, othersMessage, user, user); - } - else if (source != null) - { - // Local, non-bonking player (dragger) - _popupSystem.PopupClient(Loc.GetString("bonkable-success-message-others", ("user", userName), ("bonkable", bonkableName)), user, source.Value); - // Non-local, non-bonking players - _popupSystem.PopupEntity(Loc.GetString("bonkable-success-message-others", ("user", userName), ("bonkable", bonkableName)), user, Filter.Pvs(user).RemoveWhereAttachedEntity(e => e == user || e == source.Value), true); - // Non-local, bonking player - _popupSystem.PopupEntity(Loc.GetString("bonkable-success-message-user", ("user", userName), ("bonkable", bonkableName)), user, user); - } - - - - if (source != null) - _audioSystem.PlayPredicted(bonkableComponent.BonkSound, bonkableUid, source); - else - _audioSystem.PlayPvs(bonkableComponent.BonkSound, bonkableUid); - - _stunSystem.TryParalyze(user, TimeSpan.FromSeconds(bonkableComponent.BonkTime), true); - - if (bonkableComponent.BonkDamage is { } bonkDmg) - _damageableSystem.TryChangeDamage(user, bonkDmg, true, origin: user); - - return true; - - } - - private bool TryStartBonk(EntityUid uid, EntityUid user, EntityUid climber, BonkableComponent? bonkableComponent = null) - { - if (!Resolve(uid, ref bonkableComponent, false)) - return false; - - if (!HasComp<ClumsyComponent>(climber) || !HasComp<HandsComponent>(user)) - return false; - - if (!_cfg.GetCVar(CCVars.GameTableBonk)) - { - // Not set to always bonk, try clumsy roll. - if (!_interactionSystem.TryRollClumsy(climber, bonkableComponent.BonkClumsyChance)) - return false; - } - - var doAfterArgs = new DoAfterArgs(EntityManager, user, bonkableComponent.BonkDelay, new BonkDoAfterEvent(), uid, target: uid, used: climber) - { - BreakOnTargetMove = true, - BreakOnUserMove = true, - BreakOnDamage = true, - DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget - }; - - return _doAfter.TryStartDoAfter(doAfterArgs); - } - - private void OnAttemptClimb(EntityUid uid, BonkableComponent component, ref AttemptClimbEvent args) - { - if (args.Cancelled) - return; - - if (TryStartBonk(uid, args.User, args.Climber, component)) - args.Cancelled = true; - } - - [Serializable, NetSerializable] - private sealed partial class BonkDoAfterEvent : SimpleDoAfterEvent - { - } -} diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index d2b5a25aee7..722d97213f5 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -226,8 +226,7 @@ public bool TryClimb( target: climbable, used: entityToMove) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget }; @@ -254,6 +253,18 @@ private void Climb(EntityUid uid, EntityUid user, EntityUid climbable, bool sile if (!Resolve(climbable, ref comp, false)) return; + var selfEvent = new SelfBeforeClimbEvent(uid, user, (climbable, comp)); + RaiseLocalEvent(uid, selfEvent); + + if (selfEvent.Cancelled) + return; + + var targetEvent = new TargetBeforeClimbEvent(uid, user, (climbable, comp)); + RaiseLocalEvent(climbable, targetEvent); + + if (targetEvent.Cancelled) + return; + if (!ReplaceFixtures(uid, climbing, fixtures)) return; diff --git a/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs b/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs index c3c4baf19d0..866ce38a572 100644 --- a/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs +++ b/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs @@ -3,20 +3,18 @@ namespace Content.Shared.Clothing; +/// <summary> +/// Modifies speed when worn and activated. +/// Supports <c>ItemToggleComponent</c>. +/// </summary> [RegisterComponent, NetworkedComponent, Access(typeof(ClothingSpeedModifierSystem))] public sealed partial class ClothingSpeedModifierComponent : Component { - [DataField("walkModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)] + [DataField] public float WalkModifier = 1.0f; - [DataField("sprintModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)] + [DataField] public float SprintModifier = 1.0f; - - /// <summary> - /// Is this clothing item currently 'actively' slowing you down? - /// e.g. magboots can be turned on and off. - /// </summary> - [DataField("enabled")] public bool Enabled = true; } [Serializable, NetSerializable] @@ -25,12 +23,9 @@ public sealed class ClothingSpeedModifierComponentState : ComponentState public float WalkModifier; public float SprintModifier; - public bool Enabled; - - public ClothingSpeedModifierComponentState(float walkModifier, float sprintModifier, bool enabled) + public ClothingSpeedModifierComponentState(float walkModifier, float sprintModifier) { WalkModifier = walkModifier; SprintModifier = sprintModifier; - Enabled = enabled; } } diff --git a/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs b/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs index 66ff5c624a4..670bf99ef90 100644 --- a/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs +++ b/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs @@ -1,11 +1,10 @@ -using Content.Shared.Actions; using Content.Shared.Clothing.Components; using Content.Shared.Examine; -using Content.Shared.IdentityManagement; using Content.Shared.Inventory; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Movement.Systems; using Content.Shared.PowerCell; -using Content.Shared.Toggleable; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -15,12 +14,12 @@ namespace Content.Shared.Clothing; public sealed class ClothingSpeedModifierSystem : EntitySystem { - [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly ClothingSpeedModifierSystem _clothingSpeedModifier = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly SharedPowerCellSystem _powerCell = default!; public override void Initialize() @@ -31,39 +30,12 @@ public override void Initialize() SubscribeLocalEvent<ClothingSpeedModifierComponent, ComponentHandleState>(OnHandleState); SubscribeLocalEvent<ClothingSpeedModifierComponent, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent>>(OnRefreshMoveSpeed); SubscribeLocalEvent<ClothingSpeedModifierComponent, GetVerbsEvent<ExamineVerb>>(OnClothingVerbExamine); - - SubscribeLocalEvent<ToggleClothingSpeedComponent, GetVerbsEvent<ActivationVerb>>(AddToggleVerb); - SubscribeLocalEvent<ToggleClothingSpeedComponent, GetItemActionsEvent>(OnGetActions); - SubscribeLocalEvent<ToggleClothingSpeedComponent, ToggleClothingSpeedEvent>(OnToggleSpeed); - SubscribeLocalEvent<ToggleClothingSpeedComponent, MapInitEvent>(OnMapInit); - SubscribeLocalEvent<ToggleClothingSpeedComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); + SubscribeLocalEvent<ClothingSpeedModifierComponent, ItemToggledEvent>(OnToggled); } - // Public API - - public void SetClothingSpeedModifierEnabled(EntityUid uid, bool enabled, ClothingSpeedModifierComponent? component = null) - { - if (!Resolve(uid, ref component, false)) - return; - - if (component.Enabled != enabled) - { - component.Enabled = enabled; - Dirty(uid, component); - - // inventory system will automatically hook into the event raised by this and update accordingly - if (_container.TryGetContainingContainer(uid, out var container)) - { - _movementSpeed.RefreshMovementSpeedModifiers(container.Owner); - } - } - } - - // Event handlers - private void OnGetState(EntityUid uid, ClothingSpeedModifierComponent component, ref ComponentGetState args) { - args.State = new ClothingSpeedModifierComponentState(component.WalkModifier, component.SprintModifier, component.Enabled); + args.State = new ClothingSpeedModifierComponentState(component.WalkModifier, component.SprintModifier); } private void OnHandleState(EntityUid uid, ClothingSpeedModifierComponent component, ref ComponentHandleState args) @@ -71,17 +43,15 @@ private void OnHandleState(EntityUid uid, ClothingSpeedModifierComponent compone if (args.Current is not ClothingSpeedModifierComponentState state) return; - var diff = component.Enabled != state.Enabled || - !MathHelper.CloseTo(component.SprintModifier, state.SprintModifier) || + var diff = !MathHelper.CloseTo(component.SprintModifier, state.SprintModifier) || !MathHelper.CloseTo(component.WalkModifier, state.WalkModifier); component.WalkModifier = state.WalkModifier; component.SprintModifier = state.SprintModifier; - component.Enabled = state.Enabled; // Avoid raising the event for the container if nothing changed. // We'll still set the values in case they're slightly different but within tolerance. - if (diff && _container.TryGetContainingContainer(uid, out var container)) + if (diff && _container.TryGetContainingContainer((uid, null, null), out var container)) { _movementSpeed.RefreshMovementSpeedModifiers(container.Owner); } @@ -89,10 +59,8 @@ private void OnHandleState(EntityUid uid, ClothingSpeedModifierComponent compone private void OnRefreshMoveSpeed(EntityUid uid, ClothingSpeedModifierComponent component, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args) { - if (!component.Enabled) - return; - - args.Args.ModifySpeed(component.WalkModifier, component.SprintModifier); + if (_toggle.IsActivated(uid)) + args.Args.ModifySpeed(component.WalkModifier, component.SprintModifier); } private void OnClothingVerbExamine(EntityUid uid, ClothingSpeedModifierComponent component, GetVerbsEvent<ExamineVerb> args) @@ -142,60 +110,15 @@ private void OnClothingVerbExamine(EntityUid uid, ClothingSpeedModifierComponent _examine.AddDetailedExamineVerb(args, component, msg, Loc.GetString("clothing-speed-examinable-verb-text"), "/Textures/Interface/VerbIcons/outfit.svg.192dpi.png", Loc.GetString("clothing-speed-examinable-verb-message")); } - private void OnMapInit(Entity<ToggleClothingSpeedComponent> uid, ref MapInitEvent args) - { - _actions.AddAction(uid, ref uid.Comp.ToggleActionEntity, uid.Comp.ToggleAction); - } - - private void OnToggleSpeed(Entity<ToggleClothingSpeedComponent> uid, ref ToggleClothingSpeedEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - SetSpeedToggleEnabled(uid, !uid.Comp.Enabled, args.Performer); - } - - private void SetSpeedToggleEnabled(Entity<ToggleClothingSpeedComponent> uid, bool value, EntityUid? user) + private void OnToggled(Entity<ClothingSpeedModifierComponent> ent, ref ItemToggledEvent args) { - if (uid.Comp.Enabled == value) - return; - - TryComp<PowerCellDrawComponent>(uid, out var draw); - if (value && !_powerCell.HasDrawCharge(uid, draw, user: user)) - return; + // make sentient boots slow or fast too + _movementSpeed.RefreshMovementSpeedModifiers(ent); - uid.Comp.Enabled = value; - - _appearance.SetData(uid, ToggleVisuals.Toggled, uid.Comp.Enabled); - _actions.SetToggled(uid.Comp.ToggleActionEntity, uid.Comp.Enabled); - _clothingSpeedModifier.SetClothingSpeedModifierEnabled(uid.Owner, uid.Comp.Enabled); - _powerCell.SetPowerCellDrawEnabled(uid, uid.Comp.Enabled, draw); - Dirty(uid, uid.Comp); - } - - private void AddToggleVerb(Entity<ToggleClothingSpeedComponent> uid, ref GetVerbsEvent<ActivationVerb> args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - var user = args.User; - ActivationVerb verb = new() + if (_container.TryGetContainingContainer((ent.Owner, null, null), out var container)) { - Text = Loc.GetString("toggle-clothing-verb-text", - ("entity", Identity.Entity(uid, EntityManager))), - Act = () => SetSpeedToggleEnabled(uid, !uid.Comp.Enabled, user) - }; - args.Verbs.Add(verb); - } - - private void OnGetActions(Entity<ToggleClothingSpeedComponent> uid, ref GetItemActionsEvent args) - { - args.AddAction(ref uid.Comp.ToggleActionEntity, uid.Comp.ToggleAction); - } - - private void OnPowerCellSlotEmpty(Entity<ToggleClothingSpeedComponent> uid, ref PowerCellSlotEmptyEvent args) - { - SetSpeedToggleEnabled(uid, false, null); + // inventory system will automatically hook into the event raised by this and update accordingly + _movementSpeed.RefreshMovementSpeedModifiers(container.Owner); + } } } diff --git a/Content.Shared/Clothing/Components/AntiGravityClothingComponent.cs b/Content.Shared/Clothing/Components/AntiGravityClothingComponent.cs new file mode 100644 index 00000000000..a8fcbdd2eb3 --- /dev/null +++ b/Content.Shared/Clothing/Components/AntiGravityClothingComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// <summary> +/// This is used for clothing that makes an entity weightless when worn. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class AntiGravityClothingComponent : Component; diff --git a/Content.Shared/Clothing/Components/CursedMaskComponent.cs b/Content.Shared/Clothing/Components/CursedMaskComponent.cs new file mode 100644 index 00000000000..6073bdf5fc5 --- /dev/null +++ b/Content.Shared/Clothing/Components/CursedMaskComponent.cs @@ -0,0 +1,65 @@ +using Content.Shared.Damage; +using Content.Shared.NPC.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Clothing.Components; + +/// <summary> +/// This is used for a mask that takes over the host when worn. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(SharedCursedMaskSystem))] +public sealed partial class CursedMaskComponent : Component +{ + /// <summary> + /// The current expression shown. Used to determine which effect is applied. + /// </summary> + [DataField] + public CursedMaskExpression CurrentState = CursedMaskExpression.Neutral; + + /// <summary> + /// Speed modifier applied when the "Joy" expression is present. + /// </summary> + [DataField] + public float JoySpeedModifier = 1.15f; + + /// <summary> + /// Damage modifier applied when the "Despair" expression is present. + /// </summary> + [DataField] + public DamageModifierSet DespairDamageModifier = new(); + + /// <summary> + /// Whether or not the mask is currently attached to an NPC. + /// </summary> + [DataField] + public bool HasNpc; + + /// <summary> + /// The mind that was booted from the wearer when the mask took over. + /// </summary> + [DataField] + public EntityUid? StolenMind; + + [DataField] + public ProtoId<NpcFactionPrototype> CursedMaskFaction = "SimpleHostile"; + + [DataField] + public HashSet<ProtoId<NpcFactionPrototype>> OldFactions = new(); +} + +[Serializable, NetSerializable] +public enum CursedMaskVisuals : byte +{ + State +} + +[Serializable, NetSerializable] +public enum CursedMaskExpression : byte +{ + Neutral, + Joy, + Despair, + Anger +} diff --git a/Content.Shared/Clothing/Components/FactionClothingComponent.cs b/Content.Shared/Clothing/Components/FactionClothingComponent.cs new file mode 100644 index 00000000000..d49ee4f81d6 --- /dev/null +++ b/Content.Shared/Clothing/Components/FactionClothingComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.NPC.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Clothing.Components; + +/// <summary> +/// When equipped, adds the wearer to a faction. +/// When removed, removes the wearer from a faction. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(FactionClothingSystem))] +public sealed partial class FactionClothingComponent : Component +{ + /// <summary> + /// Faction to add and remove. + /// </summary> + [DataField(required: true)] + public ProtoId<NpcFactionPrototype> Faction = string.Empty; + + /// <summary> + /// If true, the wearer was already part of the faction. + /// This prevents wrongly removing them after removing the item. + /// </summary> + [DataField] + public bool AlreadyMember; +} diff --git a/Content.Shared/Clothing/Components/PilotedByClothingComponent.cs b/Content.Shared/Clothing/Components/PilotedByClothingComponent.cs new file mode 100644 index 00000000000..a5303ac1212 --- /dev/null +++ b/Content.Shared/Clothing/Components/PilotedByClothingComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Clothing.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// <summary> +/// Disables client-side physics prediction for this entity. +/// Without this, movement with <see cref="PilotedClothingSystem"/> is very rubberbandy. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class PilotedByClothingComponent : Component +{ +} diff --git a/Content.Shared/Clothing/Components/PilotedClothingComponent.cs b/Content.Shared/Clothing/Components/PilotedClothingComponent.cs new file mode 100644 index 00000000000..a349e4e485e --- /dev/null +++ b/Content.Shared/Clothing/Components/PilotedClothingComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// <summary> +/// Allows an entity stored in this clothing item to pass inputs to the entity wearing it. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class PilotedClothingComponent : Component +{ + /// <summary> + /// Whitelist for entities that are allowed to act as pilots when inside this entity. + /// </summary> + [DataField] + public EntityWhitelist? PilotWhitelist; + + /// <summary> + /// Should movement input be relayed from the pilot to the target? + /// </summary> + [DataField] + public bool RelayMovement = true; + + + /// <summary> + /// Reference to the entity contained in the clothing and acting as pilot. + /// </summary> + [DataField, AutoNetworkedField] + public EntityUid? Pilot; + + /// <summary> + /// Reference to the entity wearing this clothing who will be controlled by the pilot. + /// </summary> + [DataField, AutoNetworkedField] + public EntityUid? Wearer; + + public bool IsActive => Pilot != null && Wearer != null; +} diff --git a/Content.Shared/Clothing/Components/StealthClothingComponent.cs b/Content.Shared/Clothing/Components/StealthClothingComponent.cs deleted file mode 100644 index fedf48b36ed..00000000000 --- a/Content.Shared/Clothing/Components/StealthClothingComponent.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Clothing.EntitySystems; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Clothing.Components; - -/// <summary> -/// Adds StealthComponent to the user when enabled, either by an action or the system's SetEnabled method. -/// </summary> -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(StealthClothingSystem))] -public sealed partial class StealthClothingComponent : Component -{ - /// <summary> - /// Whether stealth effect is enabled. - /// </summary> - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public bool Enabled; - - /// <summary> - /// Number added to MinVisibility when stealthed, to make the user not fully invisible. - /// </summary> - [DataField("visibility"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public float Visibility; - - [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string ToggleAction = "ActionTogglePhaseCloak"; - - /// <summary> - /// The action for enabling and disabling stealth. - /// </summary> - [DataField, AutoNetworkedField] public EntityUid? ToggleActionEntity; -} - -/// <summary> -/// When stealth is enabled, disables it. -/// When it is disabled, raises <see cref="AttemptStealthEvent"/> before enabling. -/// Put any checks in a handler for that event to cancel it. -/// </summary> -public sealed partial class ToggleStealthEvent : InstantActionEvent -{ -} diff --git a/Content.Shared/Clothing/Components/ToggleClothingComponent.cs b/Content.Shared/Clothing/Components/ToggleClothingComponent.cs new file mode 100644 index 00000000000..04bc3ed4e83 --- /dev/null +++ b/Content.Shared/Clothing/Components/ToggleClothingComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Actions; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Toggleable; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Clothing.Components; + +/// <summary> +/// Clothing that can be enabled and disabled with an action. +/// Requires <see cref="ItemToggleComponent"/>. +/// </summary> +/// <remarks> +/// Not to be confused with <see cref="ToggleableClothingComponent"/> for hardsuit helmets and such. +/// </remarks> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(ToggleClothingSystem))] +public sealed partial class ToggleClothingComponent : Component +{ + /// <summary> + /// The action to add when equipped, even if not worn. + /// This must raise <see cref="ToggleActionEvent"/> to then get handled. + /// </summary> + [DataField(required: true)] + public EntProtoId<InstantActionComponent> Action = string.Empty; + + [DataField, AutoNetworkedField] + public EntityUid? ActionEntity; + + /// <summary> + /// If true, automatically disable the clothing after unequipping it. + /// </summary> + [DataField] + public bool DisableOnUnequip; +} + +/// <summary> +/// Raised on the clothing when being equipped to see if it should add the action. +/// </summary> +[ByRefEvent] +public record struct ToggleClothingCheckEvent(EntityUid User, bool Cancelled = false); diff --git a/Content.Shared/Clothing/Components/ToggleClothingSpeedComponent.cs b/Content.Shared/Clothing/Components/ToggleClothingSpeedComponent.cs deleted file mode 100644 index 90b2d7322e0..00000000000 --- a/Content.Shared/Clothing/Components/ToggleClothingSpeedComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Shared.Actions; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Clothing.Components; - -/// <summary> -/// This is used for a clothing item that gives a speed modification that is toggleable. -/// </summary> -[RegisterComponent, NetworkedComponent, Access(typeof(ClothingSpeedModifierSystem)), AutoGenerateComponentState] -public sealed partial class ToggleClothingSpeedComponent : Component -{ - /// <summary> - /// The action for toggling the clothing. - /// </summary> - [DataField] - public EntProtoId ToggleAction = "ActionToggleSpeedBoots"; - - /// <summary> - /// The action entity - /// </summary> - [DataField, AutoNetworkedField] - public EntityUid? ToggleActionEntity; - - /// <summary> - /// The state of the toggle. - /// </summary> - [DataField, AutoNetworkedField] - public bool Enabled; -} - -public sealed partial class ToggleClothingSpeedEvent : InstantActionEvent -{ - -} diff --git a/Content.Shared/Clothing/EntitySystems/AntiGravityClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/AntiGravityClothingSystem.cs new file mode 100644 index 00000000000..c5b2ee3dfc8 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/AntiGravityClothingSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Gravity; +using Content.Shared.Inventory; + +namespace Content.Shared.Clothing.EntitySystems; + +public sealed class AntiGravityClothingSystem : EntitySystem +{ + /// <inheritdoc/> + public override void Initialize() + { + SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<IsWeightlessEvent>>(OnIsWeightless); + } + + private void OnIsWeightless(Entity<AntiGravityClothingComponent> ent, ref InventoryRelayedEvent<IsWeightlessEvent> args) + { + if (args.Args.Handled) + return; + + args.Args.Handled = true; + args.Args.IsWeightless = true; + } +} diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index 9e3f917e96f..d03cd095bb7 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -165,7 +165,7 @@ private void OnHandleState(EntityUid uid, ClothingComponent component, ref Compo if (args.Current is ClothingComponentState state) { SetEquippedPrefix(uid, state.EquippedPrefix, component); - if (component.InSlot != null && _containerSys.TryGetContainingContainer(uid, out var container)) + if (component.InSlot != null && _containerSys.TryGetContainingContainer((uid, null, null), out var container)) { CheckEquipmentForLayerHide(uid, container.Owner); } diff --git a/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs new file mode 100644 index 00000000000..76b7b9aa66d --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory.Events; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; + +namespace Content.Shared.Clothing.EntitySystems; + +/// <summary> +/// Handles <see cref="FactionClothingComponent"/> faction adding and removal. +/// </summary> +public sealed class FactionClothingSystem : EntitySystem +{ + [Dependency] private readonly NpcFactionSystem _faction = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<FactionClothingComponent, GotEquippedEvent>(OnEquipped); + SubscribeLocalEvent<FactionClothingComponent, GotUnequippedEvent>(OnUnequipped); + } + + private void OnEquipped(Entity<FactionClothingComponent> ent, ref GotEquippedEvent args) + { + TryComp<NpcFactionMemberComponent>(args.Equipee, out var factionComp); + var faction = (args.Equipee, factionComp); + ent.Comp.AlreadyMember = _faction.IsMember(faction, ent.Comp.Faction); + + _faction.AddFaction(faction, ent.Comp.Faction); + } + + private void OnUnequipped(Entity<FactionClothingComponent> ent, ref GotUnequippedEvent args) + { + if (ent.Comp.AlreadyMember) + { + ent.Comp.AlreadyMember = false; + return; + } + + _faction.RemoveFaction(args.Equipee, ent.Comp.Faction); + } +} diff --git a/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs new file mode 100644 index 00000000000..49df7aee943 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs @@ -0,0 +1,169 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory.Events; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Timing; + +namespace Content.Shared.Clothing.EntitySystems; + +public sealed partial class PilotedClothingSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedMoverController _moverController = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<PilotedClothingComponent, EntInsertedIntoContainerMessage>(OnEntInserted); + SubscribeLocalEvent<PilotedClothingComponent, EntRemovedFromContainerMessage>(OnEntRemoved); + SubscribeLocalEvent<PilotedClothingComponent, GotEquippedEvent>(OnEquipped); + SubscribeLocalEvent<PilotedClothingComponent, GotUnequippedEvent>(OnUnequipped); + } + + private void OnEntInserted(Entity<PilotedClothingComponent> entity, ref EntInsertedIntoContainerMessage args) + { + // Make sure the entity was actually inserted into storage and not a different container. + if (!TryComp(entity, out StorageComponent? storage) || args.Container != storage.Container) + return; + + // Check potential pilot against whitelist, if one exists. + if (_whitelist.IsWhitelistFail(entity.Comp.PilotWhitelist, args.Entity)) + return; + + entity.Comp.Pilot = args.Entity; + Dirty(entity); + + // Attempt to setup control link, if Pilot and Wearer are both present. + StartPiloting(entity); + } + + private void OnEntRemoved(Entity<PilotedClothingComponent> entity, ref EntRemovedFromContainerMessage args) + { + // Make sure the removed entity is actually the pilot. + if (args.Entity != entity.Comp.Pilot) + return; + + StopPiloting(entity); + entity.Comp.Pilot = null; + Dirty(entity); + } + + private void OnEquipped(Entity<PilotedClothingComponent> entity, ref GotEquippedEvent args) + { + if (!TryComp(entity, out ClothingComponent? clothing)) + return; + + // Make sure the clothing item was equipped to the right slot, and not just held in a hand. + var isCorrectSlot = (clothing.Slots & args.SlotFlags) != Inventory.SlotFlags.NONE; + if (!isCorrectSlot) + return; + + entity.Comp.Wearer = args.Equipee; + Dirty(entity); + + // Attempt to setup control link, if Pilot and Wearer are both present. + StartPiloting(entity); + } + + private void OnUnequipped(Entity<PilotedClothingComponent> entity, ref GotUnequippedEvent args) + { + StopPiloting(entity); + + entity.Comp.Wearer = null; + Dirty(entity); + } + + /// <summary> + /// Attempts to establish movement/interaction relay connection(s) from Pilot to Wearer. + /// If either is missing, fails and returns false. + /// </summary> + private bool StartPiloting(Entity<PilotedClothingComponent> entity) + { + // Make sure we have both a Pilot and a Wearer + if (entity.Comp.Pilot == null || entity.Comp.Wearer == null) + return false; + + if (!_timing.IsFirstTimePredicted) + return false; + + var pilotEnt = entity.Comp.Pilot.Value; + var wearerEnt = entity.Comp.Wearer.Value; + + // Add component to block prediction of wearer + EnsureComp<PilotedByClothingComponent>(wearerEnt); + + if (entity.Comp.RelayMovement) + { + // Establish movement input relay. + _moverController.SetRelay(pilotEnt, wearerEnt); + } + + var pilotEv = new StartedPilotingClothingEvent(entity, wearerEnt); + RaiseLocalEvent(pilotEnt, ref pilotEv); + + var wearerEv = new StartingBeingPilotedByClothing(entity, pilotEnt); + RaiseLocalEvent(wearerEnt, ref wearerEv); + + return true; + } + + /// <summary> + /// Removes components from the Pilot and Wearer to stop the control relay. + /// Returns false if a connection does not already exist. + /// </summary> + private bool StopPiloting(Entity<PilotedClothingComponent> entity) + { + if (entity.Comp.Pilot == null || entity.Comp.Wearer == null) + return false; + + // Clean up components on the Pilot + var pilotEnt = entity.Comp.Pilot.Value; + RemCompDeferred<RelayInputMoverComponent>(pilotEnt); + + // Clean up components on the Wearer + var wearerEnt = entity.Comp.Wearer.Value; + RemCompDeferred<MovementRelayTargetComponent>(wearerEnt); + RemCompDeferred<PilotedByClothingComponent>(wearerEnt); + + // Raise an event on the Pilot + var pilotEv = new StoppedPilotingClothingEvent(entity, wearerEnt); + RaiseLocalEvent(pilotEnt, ref pilotEv); + + // Raise an event on the Wearer + var wearerEv = new StoppedBeingPilotedByClothing(entity, pilotEnt); + RaiseLocalEvent(wearerEnt, ref wearerEv); + + return true; + } +} + +/// <summary> +/// Raised on the Pilot when they gain control of the Wearer. +/// </summary> +[ByRefEvent] +public record struct StartedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer); + +/// <summary> +/// Raised on the Pilot when they lose control of the Wearer, +/// due to the Pilot exiting the clothing or the clothing being unequipped by the Wearer. +/// </summary> +[ByRefEvent] +public record struct StoppedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer); + +/// <summary> +/// Raised on the Wearer when the Pilot gains control of them. +/// </summary> +[ByRefEvent] +public record struct StartingBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot); + +/// <summary> +/// Raised on the Wearer when the Pilot loses control of them +/// due to the Pilot exiting the clothing or the clothing being unequipped by the Wearer. +/// </summary> +[ByRefEvent] +public record struct StoppedBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot); diff --git a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs index 5bf1e6739e9..f5df04037ee 100644 --- a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs @@ -12,9 +12,10 @@ public abstract class SharedChameleonClothingSystem : EntitySystem { [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedItemSystem _itemSystem = default!; [Dependency] private readonly ClothingSystem _clothingSystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedItemSystem _itemSystem = default!; + [Dependency] private readonly TagSystem _tag = default!; public override void Initialize() { @@ -81,7 +82,7 @@ public bool IsValidTarget(EntityPrototype proto, SlotFlags chameleonSlot = SlotF return false; // check if it is marked as valid chameleon target - if (!proto.TryGetComponent(out TagComponent? tags, _factory) || !tags.Tags.Contains("WhitelistChameleon")) + if (!proto.TryGetComponent(out TagComponent? tag, _factory) || !_tag.HasTag(tag, "WhitelistChameleon")) return false; // check if it's valid clothing diff --git a/Content.Shared/Clothing/EntitySystems/StealthClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/StealthClothingSystem.cs deleted file mode 100644 index e96d9f866aa..00000000000 --- a/Content.Shared/Clothing/EntitySystems/StealthClothingSystem.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Clothing.Components; -using Content.Shared.Inventory.Events; -using Content.Shared.Stealth; -using Content.Shared.Stealth.Components; - -namespace Content.Shared.Clothing.EntitySystems; - -/// <summary> -/// Handles the toggle action and disables stealth when clothing is unequipped. -/// </summary> -public sealed class StealthClothingSystem : EntitySystem -{ - [Dependency] private readonly SharedStealthSystem _stealth = default!; - [Dependency] private readonly ActionContainerSystem _actionContainer = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<StealthClothingComponent, GetItemActionsEvent>(OnGetItemActions); - SubscribeLocalEvent<StealthClothingComponent, ToggleStealthEvent>(OnToggleStealth); - SubscribeLocalEvent<StealthClothingComponent, AfterAutoHandleStateEvent>(OnHandleState); - SubscribeLocalEvent<StealthClothingComponent, GotUnequippedEvent>(OnUnequipped); - SubscribeLocalEvent<StealthClothingComponent, MapInitEvent>(OnMapInit); - } - - private void OnMapInit(EntityUid uid, StealthClothingComponent component, MapInitEvent args) - { - _actionContainer.EnsureAction(uid, ref component.ToggleActionEntity, component.ToggleAction); - Dirty(uid, component); - } - - /// <summary> - /// Sets the clothing's stealth effect for the user. - /// </summary> - /// <returns>True if it was changed, false otherwise</returns> - public bool SetEnabled(EntityUid uid, EntityUid user, bool enabled, StealthClothingComponent? comp = null) - { - if (!Resolve(uid, ref comp) || comp.Enabled == enabled) - return false; - - // TODO remove this when clothing unequip on delete is less sus - // prevent debug assert when ending round and its disabled - if (MetaData(user).EntityLifeStage >= EntityLifeStage.Terminating) - return false; - - comp.Enabled = enabled; - Dirty(uid, comp); - - var stealth = EnsureComp<StealthComponent>(user); - // slightly visible, but doesn't change when moving so it's ok - var visibility = enabled ? stealth.MinVisibility + comp.Visibility : stealth.MaxVisibility; - _stealth.SetVisibility(user, visibility, stealth); - _stealth.SetEnabled(user, enabled, stealth); - return true; - } - - /// <summary> - /// Raise <see cref="AddStealthActionEvent"/> then add the toggle action if it was not cancelled. - /// </summary> - private void OnGetItemActions(EntityUid uid, StealthClothingComponent comp, GetItemActionsEvent args) - { - var ev = new AddStealthActionEvent(args.User); - RaiseLocalEvent(uid, ev); - if (ev.Cancelled) - return; - - args.AddAction(ref comp.ToggleActionEntity, comp.ToggleAction); - } - - /// <summary> - /// Raises <see cref="AttemptStealthEvent"/> if enabling. - /// </summary> - private void OnToggleStealth(EntityUid uid, StealthClothingComponent comp, ToggleStealthEvent args) - { - args.Handled = true; - var user = args.Performer; - if (comp.Enabled) - { - SetEnabled(uid, user, false, comp); - return; - } - - var ev = new AttemptStealthEvent(user); - RaiseLocalEvent(uid, ev); - if (ev.Cancelled) - return; - - SetEnabled(uid, user, true, comp); - } - - /// <summary> - /// Calls <see cref="SetEnabled"/> when server sends new state. - /// </summary> - private void OnHandleState(EntityUid uid, StealthClothingComponent comp, ref AfterAutoHandleStateEvent args) - { - // SetEnabled checks if it is the same, so change it to before state was received from the server - var enabled = comp.Enabled; - comp.Enabled = !enabled; - var user = Transform(uid).ParentUid; - SetEnabled(uid, user, enabled, comp); - } - - /// <summary> - /// Force unstealths the user, doesnt remove StealthComponent since other things might use it - /// </summary> - private void OnUnequipped(EntityUid uid, StealthClothingComponent comp, GotUnequippedEvent args) - { - SetEnabled(uid, args.Equipee, false, comp); - } -} - -/// <summary> -/// Raised on the stealth clothing when attempting to add an action. -/// </summary> -public sealed class AddStealthActionEvent : CancellableEntityEventArgs -{ - /// <summary> - /// User that equipped the stealth clothing. - /// </summary> - public EntityUid User; - - public AddStealthActionEvent(EntityUid user) - { - User = user; - } -} - -/// <summary> -/// Raised on the stealth clothing when the user is attemping to enable it. -/// </summary> -public sealed class AttemptStealthEvent : CancellableEntityEventArgs -{ - /// <summary> - /// User that is attempting to enable the stealth clothing. - /// </summary> - public EntityUid User; - - public AttemptStealthEvent(EntityUid user) - { - User = user; - } -} diff --git a/Content.Shared/Clothing/EntitySystems/ToggleClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleClothingSystem.cs new file mode 100644 index 00000000000..9889376c9d4 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/ToggleClothingSystem.cs @@ -0,0 +1,58 @@ +using Content.Shared.Actions; +using Content.Shared.Clothing; +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Toggleable; + +namespace Content.Shared.Clothing.EntitySystems; + +/// <summary> +/// Handles adding and using a toggle action for <see cref="ToggleClothingComponent"/>. +/// </summary> +public sealed class ToggleClothingSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ToggleClothingComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<ToggleClothingComponent, GetItemActionsEvent>(OnGetActions); + SubscribeLocalEvent<ToggleClothingComponent, ToggleActionEvent>(OnToggleAction); + SubscribeLocalEvent<ToggleClothingComponent, ClothingGotUnequippedEvent>(OnUnequipped); + } + + private void OnMapInit(Entity<ToggleClothingComponent> ent, ref MapInitEvent args) + { + var (uid, comp) = ent; + // test funny + if (string.IsNullOrEmpty(comp.Action)) + return; + + _actions.AddAction(uid, ref comp.ActionEntity, comp.Action); + _actions.SetToggled(comp.ActionEntity, _toggle.IsActivated(ent.Owner)); + Dirty(uid, comp); + } + + private void OnGetActions(Entity<ToggleClothingComponent> ent, ref GetItemActionsEvent args) + { + var ev = new ToggleClothingCheckEvent(args.User); + RaiseLocalEvent(ent, ref ev); + if (!ev.Cancelled) + args.AddAction(ent.Comp.ActionEntity); + } + + private void OnToggleAction(Entity<ToggleClothingComponent> ent, ref ToggleActionEvent args) + { + args.Handled = _toggle.Toggle(ent.Owner, args.Performer); + } + + private void OnUnequipped(Entity<ToggleClothingComponent> ent, ref ClothingGotUnequippedEvent args) + { + if (ent.Comp.DisableOnUnequip) + _toggle.TryDeactivate(ent.Owner, args.Wearer); + } +} diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 4abe7bc876a..4cb127bb102 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -103,7 +103,7 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { BreakOnDamage = true, - BreakOnTargetMove = true, + BreakOnMove = true, // This should just re-use the BUI range checks & cancel the do after if the BUI closes. But that is all // server-side at the moment. // TODO BUI REFACTOR. diff --git a/Content.Shared/Clothing/MagbootsComponent.cs b/Content.Shared/Clothing/MagbootsComponent.cs index 0d074ff38b6..4bef74fd335 100644 --- a/Content.Shared/Clothing/MagbootsComponent.cs +++ b/Content.Shared/Clothing/MagbootsComponent.cs @@ -1,23 +1,25 @@ using Content.Shared.Alert; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Clothing; -[RegisterComponent, NetworkedComponent(), AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent] [Access(typeof(SharedMagbootsSystem))] public sealed partial class MagbootsComponent : Component { [DataField] - public EntProtoId ToggleAction = "ActionToggleMagboots"; - - [DataField, AutoNetworkedField] - public EntityUid? ToggleActionEntity; + public ProtoId<AlertPrototype> MagbootsAlert = "Magboots"; - [DataField("on"), AutoNetworkedField] - public bool On; + /// <summary> + /// If true, the user must be standing on a grid or planet map to experience the weightlessness-canceling effect + /// </summary> + [DataField] + public bool RequiresGrid = true; + /// <summary> + /// Slot the clothing has to be worn in to work. + /// </summary> [DataField] - public ProtoId<AlertPrototype> MagbootsAlert = "Magboots"; + public string Slot = "shoes"; } diff --git a/Content.Shared/Clothing/MagbootsSystem.cs b/Content.Shared/Clothing/MagbootsSystem.cs new file mode 100644 index 00000000000..ef5ac671e42 --- /dev/null +++ b/Content.Shared/Clothing/MagbootsSystem.cs @@ -0,0 +1,90 @@ +using Content.Shared.Actions; +using Content.Shared.Alert; +using Content.Shared.Atmos.Components; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Gravity; +using Content.Shared.Inventory; +using Content.Shared.Item; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; +using Robust.Shared.Containers; + +namespace Content.Shared.Clothing; + +public sealed class SharedMagbootsSystem : EntitySystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly ClothingSystem _clothing = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedGravitySystem _gravity = default!; + [Dependency] private readonly SharedItemSystem _item = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<MagbootsComponent, ItemToggledEvent>(OnToggled); + SubscribeLocalEvent<MagbootsComponent, ClothingGotEquippedEvent>(OnGotEquipped); + SubscribeLocalEvent<MagbootsComponent, ClothingGotUnequippedEvent>(OnGotUnequipped); + SubscribeLocalEvent<MagbootsComponent, IsWeightlessEvent>(OnIsWeightless); + SubscribeLocalEvent<MagbootsComponent, InventoryRelayedEvent<IsWeightlessEvent>>(OnIsWeightless); + } + + private void OnToggled(Entity<MagbootsComponent> ent, ref ItemToggledEvent args) + { + var (uid, comp) = ent; + // only stick to the floor if being worn in the correct slot + if (_container.TryGetContainingContainer((uid, null, null), out var container) && + _inventory.TryGetSlotEntity(container.Owner, comp.Slot, out var worn) + && uid == worn) + { + UpdateMagbootEffects(container.Owner, ent, args.Activated); + } + + var prefix = args.Activated ? "on" : null; + _item.SetHeldPrefix(ent, prefix); + _clothing.SetEquippedPrefix(ent, prefix); + } + + private void OnGotUnequipped(Entity<MagbootsComponent> ent, ref ClothingGotUnequippedEvent args) + { + UpdateMagbootEffects(args.Wearer, ent, false); + } + + private void OnGotEquipped(Entity<MagbootsComponent> ent, ref ClothingGotEquippedEvent args) + { + UpdateMagbootEffects(args.Wearer, ent, _toggle.IsActivated(ent.Owner)); + } + + public void UpdateMagbootEffects(EntityUid user, Entity<MagbootsComponent> ent, bool state) + { + // TODO: public api for this and add access + if (TryComp<MovedByPressureComponent>(user, out var moved)) + moved.Enabled = !state; + + if (state) + _alerts.ShowAlert(user, ent.Comp.MagbootsAlert); + else + _alerts.ClearAlert(user, ent.Comp.MagbootsAlert); + } + + private void OnIsWeightless(Entity<MagbootsComponent> ent, ref IsWeightlessEvent args) + { + if (args.Handled || !_toggle.IsActivated(ent.Owner)) + return; + + // do not cancel weightlessness if the person is in off-grid. + if (ent.Comp.RequiresGrid && !_gravity.IsWeightless(ent.Owner)) + return; + + args.IsWeightless = false; + args.Handled = true; + } + + private void OnIsWeightless(Entity<MagbootsComponent> ent, ref InventoryRelayedEvent<IsWeightlessEvent> args) + { + OnIsWeightless(ent, ref args.Args); + } +} diff --git a/Content.Shared/Clothing/SharedCursedMaskSystem.cs b/Content.Shared/Clothing/SharedCursedMaskSystem.cs new file mode 100644 index 00000000000..8ba83be151c --- /dev/null +++ b/Content.Shared/Clothing/SharedCursedMaskSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Damage; +using Content.Shared.Examine; +using Content.Shared.Inventory; +using Content.Shared.Movement.Systems; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Clothing; + +/// <summary> +/// This handles <see cref="CursedMaskComponent"/> +/// </summary> +public abstract class SharedCursedMaskSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + + /// <inheritdoc/> + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<CursedMaskComponent, ClothingGotEquippedEvent>(OnClothingEquip); + SubscribeLocalEvent<CursedMaskComponent, ClothingGotUnequippedEvent>(OnClothingUnequip); + SubscribeLocalEvent<CursedMaskComponent, ExaminedEvent>(OnExamine); + + SubscribeLocalEvent<CursedMaskComponent, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent>>(OnMovementSpeedModifier); + SubscribeLocalEvent<CursedMaskComponent, InventoryRelayedEvent<DamageModifyEvent>>(OnModifyDamage); + } + + private void OnClothingEquip(Entity<CursedMaskComponent> ent, ref ClothingGotEquippedEvent args) + { + RandomizeCursedMask(ent, args.Wearer); + TryTakeover(ent, args.Wearer); + } + + protected virtual void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args) + { + RandomizeCursedMask(ent, args.Wearer); + } + + private void OnExamine(Entity<CursedMaskComponent> ent, ref ExaminedEvent args) + { + args.PushMarkup(Loc.GetString($"cursed-mask-examine-{ent.Comp.CurrentState.ToString()}")); + } + + private void OnMovementSpeedModifier(Entity<CursedMaskComponent> ent, ref InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args) + { + if (ent.Comp.CurrentState == CursedMaskExpression.Joy) + args.Args.ModifySpeed(ent.Comp.JoySpeedModifier); + } + + private void OnModifyDamage(Entity<CursedMaskComponent> ent, ref InventoryRelayedEvent<DamageModifyEvent> args) + { + if (ent.Comp.CurrentState == CursedMaskExpression.Despair) + args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, ent.Comp.DespairDamageModifier); + } + + protected void RandomizeCursedMask(Entity<CursedMaskComponent> ent, EntityUid wearer) + { + var random = new System.Random((int) _timing.CurTick.Value); + ent.Comp.CurrentState = random.Pick(Enum.GetValues<CursedMaskExpression>()); + _appearance.SetData(ent, CursedMaskVisuals.State, ent.Comp.CurrentState); + _movementSpeedModifier.RefreshMovementSpeedModifiers(wearer); + } + + protected virtual void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer) + { + + } +} diff --git a/Content.Shared/Clothing/SharedMagbootsSystem.cs b/Content.Shared/Clothing/SharedMagbootsSystem.cs deleted file mode 100644 index 27fb0c1a502..00000000000 --- a/Content.Shared/Clothing/SharedMagbootsSystem.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Clothing.EntitySystems; -using Content.Shared.Inventory; -using Content.Shared.Item; -using Content.Shared.Slippery; -using Content.Shared.Toggleable; -using Content.Shared.Verbs; -using Robust.Shared.Containers; - -namespace Content.Shared.Clothing; - -public abstract class SharedMagbootsSystem : EntitySystem -{ - [Dependency] private readonly ClothingSpeedModifierSystem _clothingSpeedModifier = default!; - [Dependency] private readonly ClothingSystem _clothing = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly SharedActionsSystem _sharedActions = default!; - [Dependency] private readonly SharedActionsSystem _actionContainer = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedContainerSystem _sharedContainer = default!; - [Dependency] private readonly SharedItemSystem _item = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent<MagbootsComponent, GetVerbsEvent<ActivationVerb>>(AddToggleVerb); - SubscribeLocalEvent<MagbootsComponent, InventoryRelayedEvent<SlipAttemptEvent>>(OnSlipAttempt); - SubscribeLocalEvent<MagbootsComponent, GetItemActionsEvent>(OnGetActions); - SubscribeLocalEvent<MagbootsComponent, ToggleMagbootsEvent>(OnToggleMagboots); - SubscribeLocalEvent<MagbootsComponent, MapInitEvent>(OnMapInit); - } - - private void OnMapInit(EntityUid uid, MagbootsComponent component, MapInitEvent args) - { - _actionContainer.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction); - Dirty(uid, component); - } - - private void OnToggleMagboots(EntityUid uid, MagbootsComponent component, ToggleMagbootsEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - - ToggleMagboots(uid, component); - } - - private void ToggleMagboots(EntityUid uid, MagbootsComponent magboots) - { - magboots.On = !magboots.On; - - if (_sharedContainer.TryGetContainingContainer(uid, out var container) && - _inventory.TryGetSlotEntity(container.Owner, "shoes", out var entityUid) && entityUid == uid) - UpdateMagbootEffects(container.Owner, uid, true, magboots); - - if (TryComp<ItemComponent>(uid, out var item)) - { - _item.SetHeldPrefix(uid, magboots.On ? "on" : null, component: item); - _clothing.SetEquippedPrefix(uid, magboots.On ? "on" : null); - } - - _appearance.SetData(uid, ToggleVisuals.Toggled, magboots.On); - OnChanged(uid, magboots); - Dirty(uid, magboots); - } - - protected virtual void UpdateMagbootEffects(EntityUid parent, EntityUid uid, bool state, MagbootsComponent? component) { } - - protected void OnChanged(EntityUid uid, MagbootsComponent component) - { - _sharedActions.SetToggled(component.ToggleActionEntity, component.On); - _clothingSpeedModifier.SetClothingSpeedModifierEnabled(uid, component.On); - } - - private void AddToggleVerb(EntityUid uid, MagbootsComponent component, GetVerbsEvent<ActivationVerb> args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - ActivationVerb verb = new(); - verb.Text = Loc.GetString("toggle-magboots-verb-get-data-text"); - verb.Act = () => ToggleMagboots(uid, component); - // TODO VERB ICON add toggle icon? maybe a computer on/off symbol? - args.Verbs.Add(verb); - } - - private void OnSlipAttempt(EntityUid uid, MagbootsComponent component, InventoryRelayedEvent<SlipAttemptEvent> args) - { - if (component.On) - args.Args.Cancel(); - } - - private void OnGetActions(EntityUid uid, MagbootsComponent component, GetItemActionsEvent args) - { - args.AddAction(ref component.ToggleActionEntity, component.ToggleAction); - } -} - -public sealed partial class ToggleMagbootsEvent : InstantActionEvent {} diff --git a/Content.Shared/Clumsy/ClumsyComponent.cs b/Content.Shared/Clumsy/ClumsyComponent.cs new file mode 100644 index 00000000000..c71f5d0008a --- /dev/null +++ b/Content.Shared/Clumsy/ClumsyComponent.cs @@ -0,0 +1,61 @@ +using Content.Shared.Damage; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clumsy; + +/// <summary> +/// A simple clumsy tag-component. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ClumsyComponent : Component +{ + + // Standard options. Try to fit these in if you can! + + /// <summary> + /// Sound to play when clumsy interactions fail. + /// </summary> + [DataField] + public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); + + /// <summary> + /// Default chance to fail a clumsy interaction. + /// If a system needs to use something else, add a new variable in the component, do not modify this percentage. + /// </summary> + [DataField, AutoNetworkedField] + public float ClumsyDefaultCheck = 0.5f; + + /// <summary> + /// Default stun time. + /// If a system needs to use something else, add a new variable in the component, do not modify this number. + /// </summary> + [DataField, AutoNetworkedField] + public TimeSpan ClumsyDefaultStunTime = TimeSpan.FromSeconds(2.5); + + // Specific options + + /// <summary> + /// Sound to play after hitting your head on a table. Ouch! + /// </summary> + [DataField] + public SoundCollectionSpecifier TableBonkSound = new SoundCollectionSpecifier("TrayHit"); + + /// <summary> + /// Stun time after failing to shoot a gun. + /// </summary> + [DataField, AutoNetworkedField] + public TimeSpan GunShootFailStunTime = TimeSpan.FromSeconds(3); + + /// <summary> + /// Stun time after failing to shoot a gun. + /// </summary> + [DataField, AutoNetworkedField] + public DamageSpecifier? GunShootFailDamage; + + /// <summary> + /// Noise to play after failing to shoot a gun. Boom! + /// </summary> + [DataField] + public SoundSpecifier GunShootFailSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"); +} diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs new file mode 100644 index 00000000000..e034458197f --- /dev/null +++ b/Content.Shared/Clumsy/ClumsySystem.cs @@ -0,0 +1,146 @@ +using Content.Shared.CCVar; +using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Climbing.Components; +using Content.Shared.Climbing.Events; +using Content.Shared.Damage; +using Content.Shared.IdentityManagement; +using Content.Shared.Medical; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Content.Shared.Weapons.Ranged.Events; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Clumsy; + +public sealed class ClumsySystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override void Initialize() + { + SubscribeLocalEvent<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent); + SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent); + SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent); + SubscribeLocalEvent<ClumsyComponent, SelfBeforeClimbEvent>(OnBeforeClimbEvent); + } + + // If you add more clumsy interactions add them in this section! + #region Clumsy interaction events + private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeHyposprayInjectsEvent args) + { + // Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof... + if (!_random.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + args.TargetGettingInjected = args.EntityUsingHypospray; + args.InjectMessageOverride = "hypospray-component-inject-self-clumsy-message"; + _audio.PlayPvs(ent.Comp.ClumsySound, ent); + } + + private void BeforeDefibrillatorZapsEvent(Entity<ClumsyComponent> ent, ref SelfBeforeDefibrillatorZapsEvent args) + { + // Clumsy people sometimes defib themselves! + if (!_random.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + args.DefibTarget = args.EntityUsingDefib; + _audio.PlayPvs(ent.Comp.ClumsySound, ent); + + } + + private void BeforeGunShotEvent(Entity<ClumsyComponent> ent, ref SelfBeforeGunShotEvent args) + { + // Clumsy people sometimes can't shoot :( + + if (args.Gun.Comp.ClumsyProof) + return; + + if (!_random.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + if (ent.Comp.GunShootFailDamage != null) + _damageable.TryChangeDamage(ent, ent.Comp.GunShootFailDamage, origin: ent); + + _stun.TryParalyze(ent, ent.Comp.GunShootFailStunTime, true); + + // Apply salt to the wound ("Honk!") (No idea what this comment means) + _audio.PlayPvs(ent.Comp.GunShootFailSound, ent); + _audio.PlayPvs(ent.Comp.ClumsySound, ent); + + _popup.PopupEntity(Loc.GetString("gun-clumsy"), ent, ent); + args.Cancel(); + } + + private void OnBeforeClimbEvent(Entity<ClumsyComponent> ent, ref SelfBeforeClimbEvent args) + { + // This event is called in shared, thats why it has all the extra prediction stuff. + var rand = new System.Random((int)_timing.CurTick.Value); + + // If someone is putting you on the table, always get past the guard. + if (!_cfg.GetCVar(CCVars.GameTableBonk) && args.PuttingOnTable == ent.Owner && !rand.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + HitHeadClumsy(ent, args.BeingClimbedOn); + + _audio.PlayPredicted(ent.Comp.ClumsySound, ent, ent); + + _audio.PlayPredicted(ent.Comp.TableBonkSound, ent, ent); + + var gettingPutOnTableName = Identity.Entity(args.GettingPutOnTable, EntityManager); + var puttingOnTableName = Identity.Entity(args.PuttingOnTable, EntityManager); + + if (args.PuttingOnTable == ent.Owner) + { + // You are slamming yourself onto the table. + _popup.PopupPredicted( + Loc.GetString("bonkable-success-message-user", ("bonkable", args.BeingClimbedOn)), + Loc.GetString("bonkable-success-message-others", ("victim", gettingPutOnTableName), ("bonkable", args.BeingClimbedOn)), + ent, + ent); + } + else + { + // Someone else slamed you onto the table. + // This is only run in server so you need to use popup entity. + _popup.PopupPredicted( + Loc.GetString("forced-bonkable-success-message", + ("bonker", puttingOnTableName), + ("victim", gettingPutOnTableName), + ("bonkable", args.BeingClimbedOn)), + ent, + null); + } + + args.Cancel(); + } + #endregion + + #region Helper functions + /// <summary> + /// "Hits" an entites head against the given table. + /// </summary> + // Oh this fucntion is public le- NO!! This is only public for the one admin command if you use this anywhere else I will cry. + public void HitHeadClumsy(Entity<ClumsyComponent> target, EntityUid table) + { + var stunTime = target.Comp.ClumsyDefaultStunTime; + + if (TryComp<BonkableComponent>(table, out var bonkComp)) + { + stunTime = bonkComp.BonkTime; + if (bonkComp.BonkDamage != null) + _damageable.TryChangeDamage(target, bonkComp.BonkDamage, true); + } + + _stun.TryParalyze(target, stunTime, true); + } + #endregion +} diff --git a/Content.Shared/Construction/Conditions/EntityWhitelistCondition.cs b/Content.Shared/Construction/Conditions/EntityWhitelistCondition.cs index 22d86b54fb7..b7b3c56ba4c 100644 --- a/Content.Shared/Construction/Conditions/EntityWhitelistCondition.cs +++ b/Content.Shared/Construction/Conditions/EntityWhitelistCondition.cs @@ -31,7 +31,8 @@ public sealed partial class EntityWhitelistCondition : IConstructionCondition public bool Condition(EntityUid user, EntityCoordinates location, Direction direction) { - return Whitelist.IsValid(user); + var whitelistSystem = IoCManager.Resolve<IEntityManager>().System<EntityWhitelistSystem>(); + return whitelistSystem.IsWhitelistPass(Whitelist, user); } public ConstructionGuideEntry GenerateGuideEntry() diff --git a/Content.Shared/Construction/Conditions/NoWindowsInTile.cs b/Content.Shared/Construction/Conditions/NoWindowsInTile.cs index be6bc2cfacb..3ae3b593627 100644 --- a/Content.Shared/Construction/Conditions/NoWindowsInTile.cs +++ b/Content.Shared/Construction/Conditions/NoWindowsInTile.cs @@ -12,13 +12,12 @@ public sealed partial class NoWindowsInTile : IConstructionCondition public bool Condition(EntityUid user, EntityCoordinates location, Direction direction) { var entManager = IoCManager.Resolve<IEntityManager>(); - var tagQuery = entManager.GetEntityQuery<TagComponent>(); var sysMan = entManager.EntitySysManager; var tagSystem = sysMan.GetEntitySystem<TagSystem>(); foreach (var entity in location.GetEntitiesInTile(LookupFlags.Static)) { - if (tagSystem.HasTag(entity, "Window", tagQuery)) + if (tagSystem.HasTag(entity, "Window")) return false; } diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs index c041cf1ba06..efb5dfd0248 100644 --- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs +++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs @@ -15,6 +15,7 @@ using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; using Content.Shared.Tag; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem; @@ -32,16 +33,14 @@ public sealed partial class AnchorableSystem : EntitySystem [Dependency] private readonly TagSystem _tagSystem = default!; private EntityQuery<PhysicsComponent> _physicsQuery; - private EntityQuery<TagComponent> _tagQuery; - public const string Unstackable = "Unstackable"; + public readonly ProtoId<TagPrototype> Unstackable = "Unstackable"; public override void Initialize() { base.Initialize(); _physicsQuery = GetEntityQuery<PhysicsComponent>(); - _tagQuery = GetEntityQuery<TagComponent>(); SubscribeLocalEvent<AnchorableComponent, InteractUsingEvent>(OnInteractUsing, before: new[] { typeof(ItemSlotsSystem) }, after: new[] { typeof(SharedConstructionSystem) }); @@ -80,7 +79,7 @@ private void OnInteractUsing(EntityUid uid, AnchorableComponent anchorable, Inte return; // If the used entity doesn't have a tool, return early. - if (!TryComp(args.Used, out ToolComponent? usedTool) || !usedTool.Qualities.Contains(anchorable.Tool)) + if (!TryComp(args.Used, out ToolComponent? usedTool) || !_tool.HasQuality(args.Used, anchorable.Tool, usedTool)) return; args.Handled = true; @@ -312,7 +311,7 @@ public bool AnyUnstackable(EntityUid uid, EntityCoordinates location) DebugTools.Assert(!Transform(uid).Anchored); // If we are unstackable, iterate through any other entities anchored on the current square - return _tagSystem.HasTag(uid, Unstackable, _tagQuery) && AnyUnstackablesAnchoredAt(location); + return _tagSystem.HasTag(uid, Unstackable) && AnyUnstackablesAnchoredAt(location); } public bool AnyUnstackablesAnchoredAt(EntityCoordinates location) @@ -327,10 +326,8 @@ public bool AnyUnstackablesAnchoredAt(EntityCoordinates location) while (enumerator.MoveNext(out var entity)) { // If we find another unstackable here, return true. - if (_tagSystem.HasTag(entity.Value, Unstackable, _tagQuery)) - { + if (_tagSystem.HasTag(entity.Value, Unstackable)) return true; - } } return false; diff --git a/Content.Shared/Construction/MachinePartSystem.cs b/Content.Shared/Construction/MachinePartSystem.cs index 01db7fbade3..603542ee26c 100644 --- a/Content.Shared/Construction/MachinePartSystem.cs +++ b/Content.Shared/Construction/MachinePartSystem.cs @@ -90,9 +90,9 @@ public Dictionary<string, int> GetMachineBoardMaterialCost(Entity<MachineBoardCo var partRecipe = recipes[0]; if (recipes.Count > 1) - partRecipe = recipes.MinBy(p => p.RequiredMaterials.Values.Sum()); + partRecipe = recipes.MinBy(p => p.Materials.Values.Sum()); - foreach (var (mat, matAmount) in partRecipe!.RequiredMaterials) + foreach (var (mat, matAmount) in partRecipe!.Materials) { materials.TryAdd(mat, 0); materials[mat] += matAmount * amount * coefficient; @@ -116,9 +116,9 @@ public Dictionary<string, int> GetMachineBoardMaterialCost(Entity<MachineBoardCo { var partRecipe = recipes[0]; if (recipes.Count > 1) - partRecipe = recipes.MinBy(p => p.RequiredMaterials.Values.Sum()); + partRecipe = recipes.MinBy(p => p.Materials.Values.Sum()); - foreach (var (mat, matAmount) in partRecipe!.RequiredMaterials) + foreach (var (mat, matAmount) in partRecipe!.Materials) { materials.TryAdd(mat, 0); materials[mat] += matAmount * amount * coefficient; @@ -136,9 +136,9 @@ public Dictionary<string, int> GetMachineBoardMaterialCost(Entity<MachineBoardCo { var partRecipe = recipes[0]; if (recipes.Count > 1) - partRecipe = recipes.MinBy(p => p.RequiredMaterials.Values.Sum()); + partRecipe = recipes.MinBy(p => p.Materials.Values.Sum()); - foreach (var (mat, matAmount) in partRecipe!.RequiredMaterials) + foreach (var (mat, matAmount) in partRecipe!.Materials) { materials.TryAdd(mat, 0); materials[mat] += matAmount * amount * coefficient; diff --git a/Content.Shared/Construction/Steps/MultipleTagsConstructionGraphStep.cs b/Content.Shared/Construction/Steps/MultipleTagsConstructionGraphStep.cs index 668952dac23..07ba46946a6 100644 --- a/Content.Shared/Construction/Steps/MultipleTagsConstructionGraphStep.cs +++ b/Content.Shared/Construction/Steps/MultipleTagsConstructionGraphStep.cs @@ -1,14 +1,15 @@ using Content.Shared.Tag; +using Robust.Shared.Prototypes; namespace Content.Shared.Construction.Steps { public sealed partial class MultipleTagsConstructionGraphStep : ArbitraryInsertConstructionGraphStep { [DataField("allTags")] - private List<string>? _allTags; + private List<ProtoId<TagPrototype>>? _allTags; [DataField("anyTags")] - private List<string>? _anyTags; + private List<ProtoId<TagPrototype>>? _anyTags; private static bool IsNullOrEmpty<T>(ICollection<T>? list) { @@ -21,16 +22,12 @@ public override bool EntityValid(EntityUid uid, IEntityManager entityManager, IC if (IsNullOrEmpty(_allTags) && IsNullOrEmpty(_anyTags)) return false; // Step is somehow invalid, we return. - // No tags at all. - if (!entityManager.TryGetComponent(uid, out TagComponent? tags)) - return false; - var tagSystem = entityManager.EntitySysManager.GetEntitySystem<TagSystem>(); - if (_allTags != null && !tagSystem.HasAllTags(tags, _allTags)) + if (_allTags != null && !tagSystem.HasAllTags(uid, _allTags)) return false; // We don't have all the tags needed. - if (_anyTags != null && !tagSystem.HasAnyTag(tags, _anyTags)) + if (_anyTags != null && !tagSystem.HasAnyTag(uid, _anyTags)) return false; // We don't have any of the tags needed. // This entity is valid! diff --git a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsHackerSystem.cs b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsHackerSystem.cs index a626db19e5f..10faf3ebe23 100644 --- a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsHackerSystem.cs +++ b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsHackerSystem.cs @@ -30,8 +30,7 @@ private void OnBeforeInteractHand(Entity<CriminalRecordsHackerComponent> ent, re var doAfterArgs = new DoAfterArgs(EntityManager, ent, ent.Comp.Delay, new CriminalRecordsHackDoAfterEvent(), target: target, used: ent, eventTarget: ent) { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnWeightlessMove = true, MovementThreshold = 0.5f, }; diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index 297fe095f8c..1c8e2ef2b0d 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -511,10 +511,11 @@ public bool TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, Han var doAfterEventArgs = new DoAfterArgs(EntityManager, user, cuffTime, new AddCuffDoAfterEvent(), handcuff, target, handcuff) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, + BreakOnWeightlessMove = false, BreakOnDamage = true, - NeedHand = true + NeedHand = true, + DistanceThreshold = 0.3f }; if (!_doAfter.TryStartDoAfter(doAfterEventArgs)) @@ -606,11 +607,12 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, + BreakOnWeightlessMove = false, BreakOnDamage = true, NeedHand = true, RequireCanInteract = false, // Trust in UncuffAttemptEvent + DistanceThreshold = 0.3f }; if (!_doAfter.TryStartDoAfter(doAfterEventArgs)) diff --git a/Content.Shared/Damage/Components/DamageProtectionBuffComponent.cs b/Content.Shared/Damage/Components/DamageProtectionBuffComponent.cs new file mode 100644 index 00000000000..99b055a6456 --- /dev/null +++ b/Content.Shared/Damage/Components/DamageProtectionBuffComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Damage.Prototypes; +using Robust.Shared.GameStates; + +namespace Content.Shared.Damage.Components; + +/// <summary> +/// Applies the specified DamageModifierSets when the entity takes damage. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class DamageProtectionBuffComponent : Component +{ + /// <summary> + /// The damage modifiers for entities with this component. + /// </summary> + [DataField] + public Dictionary<string, DamageModifierSetPrototype> Modifiers = new(); +} diff --git a/Content.Shared/Damage/Components/DamageableComponent.cs b/Content.Shared/Damage/Components/DamageableComponent.cs index 3c3a27c5fac..ed01bd26473 100644 --- a/Content.Shared/Damage/Components/DamageableComponent.cs +++ b/Content.Shared/Damage/Components/DamageableComponent.cs @@ -74,7 +74,7 @@ public sealed partial class DamageableComponent : Component public List<string> RadiationDamageTypeIDs = new() { "Radiation" }; [DataField] - public Dictionary<MobState, ProtoId<StatusIconPrototype>> HealthIcons = new() + public Dictionary<MobState, ProtoId<HealthIconPrototype>> HealthIcons = new() { { MobState.Alive, "HealthIconFine" }, { MobState.Critical, "HealthIconCritical" }, @@ -82,7 +82,10 @@ public sealed partial class DamageableComponent : Component }; [DataField] - public ProtoId<StatusIconPrototype> RottingIcon = "HealthIconRotting"; + public ProtoId<HealthIconPrototype> RottingIcon = "HealthIconRotting"; + + [DataField] + public FixedPoint2? HealthBarThreshold; } [Serializable, NetSerializable] diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs index 8ab9116a975..7f505b807f7 100644 --- a/Content.Shared/Damage/DamageSpecifier.cs +++ b/Content.Shared/Damage/DamageSpecifier.cs @@ -43,7 +43,7 @@ public sealed partial class DamageSpecifier : IEquatable<DamageSpecifier> /// </summary> /// <remarks> /// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage - /// in another. Consider using <see cref="Any()"/> or <see cref="Empty"/> instead. + /// in another. Consider using <see cref="AnyPositive"/> or <see cref="Empty"/> instead. /// </remarks> public FixedPoint2 GetTotal() { @@ -60,7 +60,7 @@ public FixedPoint2 GetTotal() /// Differs from <see cref="Empty"/> as a damage specifier might contain entries with zeroes. /// This also returns false if the specifier only contains negative values. /// </summary> - public bool Any() + public bool AnyPositive() { foreach (var value in DamageDict.Values) { diff --git a/Content.Shared/Damage/Systems/DamageOnHoldingSystem.cs b/Content.Shared/Damage/Systems/DamageOnHoldingSystem.cs index c13ec0a1b9f..78e86f5ab42 100644 --- a/Content.Shared/Damage/Systems/DamageOnHoldingSystem.cs +++ b/Content.Shared/Damage/Systems/DamageOnHoldingSystem.cs @@ -37,7 +37,7 @@ public override void Update(float frameTime) { if (!component.Enabled || component.NextDamage > _timing.CurTime) continue; - if (_container.TryGetContainingContainer(uid, out var container)) + if (_container.TryGetContainingContainer((uid, null, null), out var container)) { _damageableSystem.TryChangeDamage(container.Owner, component.Damage, origin: uid); } diff --git a/Content.Shared/Damage/Systems/DamageProtectionBuffSystem.cs b/Content.Shared/Damage/Systems/DamageProtectionBuffSystem.cs new file mode 100644 index 00000000000..bbb7bfda323 --- /dev/null +++ b/Content.Shared/Damage/Systems/DamageProtectionBuffSystem.cs @@ -0,0 +1,19 @@ +using Content.Shared.Damage.Components; + +namespace Content.Shared.Damage.Systems; + +public sealed class DamageProtectionBuffSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<DamageProtectionBuffComponent, DamageModifyEvent>(OnDamageModify); + } + + private void OnDamageModify(EntityUid uid, DamageProtectionBuffComponent component, DamageModifyEvent args) + { + foreach (var modifier in component.Modifiers.Values) + args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifier); + } +} diff --git a/Content.Shared/DeltaV/CCVars/DCCVars.cs b/Content.Shared/DeltaV/CCVars/DCCVars.cs index 58c4186ff61..9c5b68268fa 100644 --- a/Content.Shared/DeltaV/CCVars/DCCVars.cs +++ b/Content.Shared/DeltaV/CCVars/DCCVars.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Configuration; +using Robust.Shared.Configuration; namespace Content.Shared.DeltaV.CCVars; @@ -16,6 +16,61 @@ public sealed class DCCVars public static readonly CVarDef<bool> RoundEndPacifist = CVarDef.Create("game.round_end_pacifist", false, CVar.SERVERONLY); + /* + * No EORG + */ + + /// <summary> + /// Whether the no EORG popup is enabled. + /// </summary> + public static readonly CVarDef<bool> RoundEndNoEorgPopup = + CVarDef.Create("game.round_end_eorg_popup_enabled", true, CVar.SERVER | CVar.REPLICATED); + + /// <summary> + /// Skip the no EORG popup. + /// </summary> + public static readonly CVarDef<bool> SkipRoundEndNoEorgPopup = + CVarDef.Create("game.skip_round_end_eorg_popup", false, CVar.CLIENTONLY | CVar.ARCHIVE); + + /// <summary> + /// How long to display the EORG popup for. + /// </summary> + public static readonly CVarDef<float> RoundEndNoEorgPopupTime = + CVarDef.Create("game.round_end_eorg_popup_time", 5f, CVar.SERVER | CVar.REPLICATED); + + /* + * Auto ACO + */ + + /// <summary> + /// How long with no captain before requesting an ACO be elected. + /// </summary> + public static readonly CVarDef<TimeSpan> RequestAcoDelay = + CVarDef.Create("game.request_aco_delay", TimeSpan.FromMinutes(15), CVar.SERVERONLY | CVar.ARCHIVE); + + /// <summary> + /// Determines whether an ACO should be requested when the captain leaves during the round, + /// in addition to cases where there are no captains at round start. + /// </summary> + public static readonly CVarDef<bool> RequestAcoOnCaptainDeparture = + CVarDef.Create("game.request_aco_on_captain_departure", true, CVar.SERVERONLY | CVar.ARCHIVE); + + /// <summary> + /// Determines whether All Access (AA) should be automatically unlocked if no captain is present. + /// </summary> + public static readonly CVarDef<bool> AutoUnlockAllAccessEnabled = + CVarDef.Create("game.auto_unlock_aa_enabled", true, CVar.SERVERONLY | CVar.ARCHIVE); + + /// <summary> + /// How long after an ACO request announcement is made before All Access (AA) should be unlocked. + /// </summary> + public static readonly CVarDef<TimeSpan> AutoUnlockAllAccessDelay = + CVarDef.Create("game.auto_unlock_aa_delay", TimeSpan.FromMinutes(5), CVar.SERVERONLY | CVar.ARCHIVE); + + /* + * Misc. + */ + /// <summary> /// Whether the Shipyard is enabled. /// </summary> diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs new file mode 100644 index 00000000000..8cb2efa900f --- /dev/null +++ b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs @@ -0,0 +1,166 @@ +using Content.Shared.CartridgeLoader; +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class NanoChatUiMessageEvent : CartridgeMessageEvent +{ + /// <summary> + /// The type of UI message being sent. + /// </summary> + public readonly NanoChatUiMessageType Type; + + /// <summary> + /// The recipient's NanoChat number, if applicable. + /// </summary> + public readonly uint? RecipientNumber; + + /// <summary> + /// The content of the message or name for new chats. + /// </summary> + public readonly string? Content; + + /// <summary> + /// The recipient's job title when creating a new chat. + /// </summary> + public readonly string? RecipientJob; + + /// <summary> + /// Creates a new NanoChat UI message event. + /// </summary> + /// <param name="type">The type of message being sent</param> + /// <param name="recipientNumber">Optional recipient number for the message</param> + /// <param name="content">Optional content of the message</param> + /// <param name="recipientJob">Optional job title for new chat creation</param> + public NanoChatUiMessageEvent(NanoChatUiMessageType type, + uint? recipientNumber = null, + string? content = null, + string? recipientJob = null) + { + Type = type; + RecipientNumber = recipientNumber; + Content = content; + RecipientJob = recipientJob; + } +} + +[Serializable, NetSerializable] +public enum NanoChatUiMessageType : byte +{ + NewChat, + SelectChat, + CloseChat, + SendMessage, + DeleteChat, + ToggleMute, +} + +// putting this here because i can +[Serializable, NetSerializable, DataRecord] +public struct NanoChatRecipient +{ + /// <summary> + /// The recipient's unique NanoChat number. + /// </summary> + public uint Number; + + /// <summary> + /// The recipient's display name, typically from their ID card. + /// </summary> + public string Name; + + /// <summary> + /// The recipient's job title, if available. + /// </summary> + public string? JobTitle; + + /// <summary> + /// Whether this recipient has unread messages. + /// </summary> + public bool HasUnread; + + /// <summary> + /// Creates a new NanoChat recipient. + /// </summary> + /// <param name="number">The recipient's NanoChat number</param> + /// <param name="name">The recipient's display name</param> + /// <param name="jobTitle">Optional job title for the recipient</param> + /// <param name="hasUnread">Whether there are unread messages from this recipient</param> + public NanoChatRecipient(uint number, string name, string? jobTitle = null, bool hasUnread = false) + { + Number = number; + Name = name; + JobTitle = jobTitle; + HasUnread = hasUnread; + } +} + +[Serializable, NetSerializable, DataRecord] +public struct NanoChatMessage +{ + /// <summary> + /// When the message was sent. + /// </summary> + public TimeSpan Timestamp; + + /// <summary> + /// The content of the message. + /// </summary> + public string Content; + + /// <summary> + /// The NanoChat number of the sender. + /// </summary> + public uint SenderId; + + /// <summary> + /// Whether the message failed to deliver to the recipient. + /// This can happen if the recipient is out of range or if there's no active telecomms server. + /// </summary> + public bool DeliveryFailed; + + /// <summary> + /// Creates a new NanoChat message. + /// </summary> + /// <param name="timestamp">When the message was sent</param> + /// <param name="content">The content of the message</param> + /// <param name="senderId">The sender's NanoChat number</param> + /// <param name="deliveryFailed">Whether delivery to the recipient failed</param> + public NanoChatMessage(TimeSpan timestamp, string content, uint senderId, bool deliveryFailed = false) + { + Timestamp = timestamp; + Content = content; + SenderId = senderId; + DeliveryFailed = deliveryFailed; + } +} + +/// <summary> +/// NanoChat log data struct +/// </summary> +/// <remarks>Used by the LogProbe</remarks> +[Serializable, NetSerializable, DataRecord] +public readonly struct NanoChatData( + Dictionary<uint, NanoChatRecipient> recipients, + Dictionary<uint, List<NanoChatMessage>> messages, + uint? cardNumber, + NetEntity card) +{ + public Dictionary<uint, NanoChatRecipient> Recipients { get; } = recipients; + public Dictionary<uint, List<NanoChatMessage>> Messages { get; } = messages; + public uint? CardNumber { get; } = cardNumber; + public NetEntity Card { get; } = card; +} + +/// <summary> +/// Raised on the NanoChat card whenever a recipient gets added +/// </summary> +[ByRefEvent] +public readonly record struct NanoChatRecipientUpdatedEvent(EntityUid CardUid); + +/// <summary> +/// Raised on the NanoChat card whenever it receives or tries sending a messsage +/// </summary> +[ByRefEvent] +public readonly record struct NanoChatMessageReceivedEvent(EntityUid CardUid); diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/NanoChatUiState.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/NanoChatUiState.cs new file mode 100644 index 00000000000..47e8986e01a --- /dev/null +++ b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/NanoChatUiState.cs @@ -0,0 +1,30 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class NanoChatUiState : BoundUserInterfaceState +{ + public readonly Dictionary<uint, NanoChatRecipient> Recipients; + public readonly Dictionary<uint, List<NanoChatMessage>> Messages; + public readonly uint? CurrentChat; + public readonly uint OwnNumber; + public readonly int MaxRecipients; + public readonly bool NotificationsMuted; + + public NanoChatUiState( + Dictionary<uint, NanoChatRecipient> recipients, + Dictionary<uint, List<NanoChatMessage>> messages, + uint? currentChat, + uint ownNumber, + int maxRecipients, + bool notificationsMuted) + { + Recipients = recipients; + Messages = messages; + CurrentChat = currentChat; + OwnNumber = ownNumber; + MaxRecipients = maxRecipients; + NotificationsMuted = notificationsMuted; + } +} diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs new file mode 100644 index 00000000000..a80f8c6b8a8 --- /dev/null +++ b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/StockTradingUiMessageEvent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class StockTradingUiMessageEvent(StockTradingUiAction action, int companyIndex, float amount) + : CartridgeMessageEvent +{ + public readonly StockTradingUiAction Action = action; + public readonly int CompanyIndex = companyIndex; + public readonly float Amount = amount; +} + +[Serializable, NetSerializable] +public enum StockTradingUiAction +{ + Buy, + Sell, +} diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/StockTradingUiState.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/StockTradingUiState.cs new file mode 100644 index 00000000000..aea4ba5aa1d --- /dev/null +++ b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/StockTradingUiState.cs @@ -0,0 +1,66 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class StockTradingUiState( + List<StockCompanyStruct> entries, + Dictionary<int, int> ownedStocks, + float balance) + : BoundUserInterfaceState +{ + public readonly List<StockCompanyStruct> Entries = entries; + public readonly Dictionary<int, int> OwnedStocks = ownedStocks; + public readonly float Balance = balance; +} + +// No structure, zero fucks given +[DataDefinition, Serializable] +public partial struct StockCompanyStruct +{ + /// <summary> + /// The displayed name of the company shown in the UI. + /// </summary> + [DataField(required: true)] + public LocId? DisplayName; + + // Used for runtime-added companies that don't have a localization entry + private string? _displayName; + + /// <summary> + /// Gets or sets the display name, using either the localized or direct string value + /// </summary> + [Access(Other = AccessPermissions.ReadWriteExecute)] + public string LocalizedDisplayName + { + get => _displayName ?? Loc.GetString(DisplayName ?? string.Empty); + set => _displayName = value; + } + + /// <summary> + /// The current price of the company's stock + /// </summary> + [DataField(required: true)] + public float CurrentPrice; + + /// <summary> + /// The base price of the company's stock + /// </summary> + [DataField(required: true)] + public float BasePrice; + + /// <summary> + /// The price history of the company's stock + /// </summary> + [DataField] + public List<float>? PriceHistory; + + public StockCompanyStruct(string displayName, float currentPrice, float basePrice, List<float>? priceHistory) + { + DisplayName = displayName; + _displayName = null; + CurrentPrice = currentPrice; + BasePrice = basePrice; + PriceHistory = priceHistory ?? []; + } +} diff --git a/Content.Shared/DeltaV/NanoChat/NanoChatCardComponent.cs b/Content.Shared/DeltaV/NanoChat/NanoChatCardComponent.cs new file mode 100644 index 00000000000..7e40be79832 --- /dev/null +++ b/Content.Shared/DeltaV/NanoChat/NanoChatCardComponent.cs @@ -0,0 +1,52 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.DeltaV.NanoChat; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedNanoChatSystem))] +[AutoGenerateComponentPause, AutoGenerateComponentState] +public sealed partial class NanoChatCardComponent : Component +{ + /// <summary> + /// The number assigned to this card. + /// </summary> + [DataField, AutoNetworkedField] + public uint? Number; + + /// <summary> + /// All chat recipients stored on this card. + /// </summary> + [DataField] + public Dictionary<uint, NanoChatRecipient> Recipients = new(); + + /// <summary> + /// All messages stored on this card, keyed by recipient number. + /// </summary> + [DataField] + public Dictionary<uint, List<NanoChatMessage>> Messages = new(); + + /// <summary> + /// The currently selected chat recipient number. + /// </summary> + [DataField] + public uint? CurrentChat; + + /// <summary> + /// The maximum amount of recipients this card supports. + /// </summary> + [DataField] + public int MaxRecipients = 50; + + /// <summary> + /// Last time a message was sent, for rate limiting. + /// </summary> + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan LastMessageTime; // TODO: actually use this, compare against actor and not the card + + /// <summary> + /// Whether to send notifications. + /// </summary> + [DataField] + public bool NotificationsMuted; +} diff --git a/Content.Shared/DeltaV/NanoChat/SharedNanoChatSystem.cs b/Content.Shared/DeltaV/NanoChat/SharedNanoChatSystem.cs new file mode 100644 index 00000000000..4511ecec207 --- /dev/null +++ b/Content.Shared/DeltaV/NanoChat/SharedNanoChatSystem.cs @@ -0,0 +1,275 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.Examine; +using Content.Shared.PDA; +using Robust.Shared.Timing; + + +namespace Content.Shared.DeltaV.NanoChat; + +/// <summary> +/// Base system for NanoChat functionality shared between client and server. +/// </summary> +public abstract class SharedNanoChatSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<NanoChatCardComponent, ExaminedEvent>(OnExamined); + } + + private void OnExamined(Entity<NanoChatCardComponent> ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (ent.Comp.Number == null) + { + args.PushMarkup(Loc.GetString("nanochat-card-examine-no-number")); + return; + } + + args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}"))); + } + + #region Public API Methods + + /// <summary> + /// Gets the NanoChat number for a card. + /// </summary> + public uint? GetNumber(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return null; + + return card.Comp.Number; + } + + /// <summary> + /// Sets the NanoChat number for a card. + /// </summary> + public void SetNumber(Entity<NanoChatCardComponent?> card, uint number) + { + if (!Resolve(card, ref card.Comp)) + return; + + card.Comp.Number = number; + Dirty(card); + } + + /// <summary> + /// Gets the recipients dictionary from a card. + /// </summary> + public IReadOnlyDictionary<uint, NanoChatRecipient> GetRecipients(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return new Dictionary<uint, NanoChatRecipient>(); + + return card.Comp.Recipients; + } + + /// <summary> + /// Gets the messages dictionary from a card. + /// </summary> + public IReadOnlyDictionary<uint, List<NanoChatMessage>> GetMessages(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return new Dictionary<uint, List<NanoChatMessage>>(); + + return card.Comp.Messages; + } + + /// <summary> + /// Sets a specific recipient in the card. + /// </summary> + public void SetRecipient(Entity<NanoChatCardComponent?> card, uint number, NanoChatRecipient recipient) + { + if (!Resolve(card, ref card.Comp)) + return; + + card.Comp.Recipients[number] = recipient; + Dirty(card); + } + + /// <summary> + /// Gets a specific recipient from the card. + /// </summary> + public NanoChatRecipient? GetRecipient(Entity<NanoChatCardComponent?> card, uint number) + { + if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(number, out var recipient)) + return null; + + return recipient; + } + + /// <summary> + /// Gets all messages for a specific recipient. + /// </summary> + public List<NanoChatMessage>? GetMessagesForRecipient(Entity<NanoChatCardComponent?> card, uint recipientNumber) + { + if (!Resolve(card, ref card.Comp) || !card.Comp.Messages.TryGetValue(recipientNumber, out var messages)) + return null; + + return new List<NanoChatMessage>(messages); + } + + /// <summary> + /// Adds a message to a recipient's conversation. + /// </summary> + public void AddMessage(Entity<NanoChatCardComponent?> card, uint recipientNumber, NanoChatMessage message) + { + if (!Resolve(card, ref card.Comp)) + return; + + if (!card.Comp.Messages.TryGetValue(recipientNumber, out var messages)) + { + messages = new List<NanoChatMessage>(); + card.Comp.Messages[recipientNumber] = messages; + } + + messages.Add(message); + card.Comp.LastMessageTime = _timing.CurTime; + Dirty(card); + } + + /// <summary> + /// Gets the currently selected chat recipient. + /// </summary> + public uint? GetCurrentChat(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return null; + + return card.Comp.CurrentChat; + } + + /// <summary> + /// Sets the currently selected chat recipient. + /// </summary> + public void SetCurrentChat(Entity<NanoChatCardComponent?> card, uint? recipient) + { + if (!Resolve(card, ref card.Comp)) + return; + + card.Comp.CurrentChat = recipient; + Dirty(card); + } + + /// <summary> + /// Gets whether notifications are muted. + /// </summary> + public bool GetNotificationsMuted(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return false; + + return card.Comp.NotificationsMuted; + } + + /// <summary> + /// Sets whether notifications are muted. + /// </summary> + public void SetNotificationsMuted(Entity<NanoChatCardComponent?> card, bool muted) + { + if (!Resolve(card, ref card.Comp)) + return; + + card.Comp.NotificationsMuted = muted; + Dirty(card); + } + + /// <summary> + /// Gets the time of the last message. + /// </summary> + public TimeSpan? GetLastMessageTime(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return null; + + return card.Comp.LastMessageTime; + } + + /// <summary> + /// Gets if there are unread messages from a recipient. + /// </summary> + public bool HasUnreadMessages(Entity<NanoChatCardComponent?> card, uint recipientNumber) + { + if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(recipientNumber, out var recipient)) + return false; + + return recipient.HasUnread; + } + + /// <summary> + /// Clears all messages and recipients from the card. + /// </summary> + public void Clear(Entity<NanoChatCardComponent?> card) + { + if (!Resolve(card, ref card.Comp)) + return; + + card.Comp.Messages.Clear(); + card.Comp.Recipients.Clear(); + card.Comp.CurrentChat = null; + Dirty(card); + } + + /// <summary> + /// Deletes a chat conversation with a recipient from the card. + /// Optionally keeps message history while removing from active chats. + /// </summary> + /// <returns>True if the chat was deleted successfully</returns> + public bool TryDeleteChat(Entity<NanoChatCardComponent?> card, uint recipientNumber, bool keepMessages = false) + { + if (!Resolve(card, ref card.Comp)) + return false; + + // Remove from recipients list + var removed = card.Comp.Recipients.Remove(recipientNumber); + + // Clear messages if requested + if (!keepMessages) + card.Comp.Messages.Remove(recipientNumber); + + // Clear current chat if we just deleted it + if (card.Comp.CurrentChat == recipientNumber) + card.Comp.CurrentChat = null; + + if (removed) + Dirty(card); + + return removed; + } + + /// <summary> + /// Ensures a recipient exists in the card's contacts and message lists. + /// If the recipient doesn't exist, they will be added with the provided info. + /// </summary> + /// <returns>True if the recipient was added or already existed</returns> + public bool EnsureRecipientExists(Entity<NanoChatCardComponent?> card, + uint recipientNumber, + NanoChatRecipient? recipientInfo = null) + { + if (!Resolve(card, ref card.Comp)) + return false; + + if (!card.Comp.Recipients.ContainsKey(recipientNumber)) + { + // Only add if we have recipient info + if (recipientInfo == null) + return false; + + card.Comp.Recipients[recipientNumber] = recipientInfo.Value; + } + + // Ensure message list exists for this recipient + if (!card.Comp.Messages.ContainsKey(recipientNumber)) + card.Comp.Messages[recipientNumber] = new List<NanoChatMessage>(); + + Dirty(card); + return true; + } + + #endregion +} diff --git a/Content.Shared/DeltaV/Salvage/Components/MiningPointsComponent.cs b/Content.Shared/DeltaV/Salvage/Components/MiningPointsComponent.cs new file mode 100644 index 00000000000..33ea80280f8 --- /dev/null +++ b/Content.Shared/DeltaV/Salvage/Components/MiningPointsComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.DeltaV.Salvage.Systems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.DeltaV.Salvage.Components; + +/// <summary> +/// Stores mining points for a holder, such as an ID card or ore processor. +/// Mining points are gained by smelting ore and redeeming them to your ID card. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(MiningPointsSystem))] +[AutoGenerateComponentState] +public sealed partial class MiningPointsComponent : Component +{ + /// <summary> + /// The number of points stored. + /// </summary> + [DataField, AutoNetworkedField] + public uint Points; + + /// <summary> + /// Sound played when successfully transferring points to another holder. + /// </summary> + [DataField] + public SoundSpecifier? TransferSound; +} diff --git a/Content.Shared/DeltaV/Salvage/Components/MiningPointsLatheComponent.cs b/Content.Shared/DeltaV/Salvage/Components/MiningPointsLatheComponent.cs new file mode 100644 index 00000000000..fb7616e5149 --- /dev/null +++ b/Content.Shared/DeltaV/Salvage/Components/MiningPointsLatheComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.DeltaV.Salvage.Components; + +/// <summary> +/// Adds points to <see cref="MiningPointsComponent"/> when making a recipe that has miningPoints set. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class MiningPointsLatheComponent : Component; diff --git a/Content.Shared/DeltaV/Salvage/MiningPointsUI.cs b/Content.Shared/DeltaV/Salvage/MiningPointsUI.cs new file mode 100644 index 00000000000..c101fa0c712 --- /dev/null +++ b/Content.Shared/DeltaV/Salvage/MiningPointsUI.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.Salvage; + +/// <summary> +/// Message for a lathe to transfer its mining points to the user's id card. +/// </summary> +[Serializable, NetSerializable] +public sealed class LatheClaimMiningPointsMessage : BoundUserInterfaceMessage; diff --git a/Content.Shared/DeltaV/Salvage/Systems/MiningPointsSystem.cs b/Content.Shared/DeltaV/Salvage/Systems/MiningPointsSystem.cs new file mode 100644 index 00000000000..33dca3ee019 --- /dev/null +++ b/Content.Shared/DeltaV/Salvage/Systems/MiningPointsSystem.cs @@ -0,0 +1,121 @@ +using Content.Shared.Access.Systems; +using Content.Shared.DeltaV.Salvage.Components; +using Content.Shared.Lathe; +using Robust.Shared.Audio.Systems; + +namespace Content.Shared.DeltaV.Salvage.Systems; + +public sealed class MiningPointsSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedIdCardSystem _idCard = default!; + + private EntityQuery<MiningPointsComponent> _query; + + public override void Initialize() + { + base.Initialize(); + + _query = GetEntityQuery<MiningPointsComponent>(); + + SubscribeLocalEvent<MiningPointsLatheComponent, LatheStartPrintingEvent>(OnStartPrinting); + Subs.BuiEvents<MiningPointsLatheComponent>(LatheUiKey.Key, subs => + { + subs.Event<LatheClaimMiningPointsMessage>(OnClaimMiningPoints); + }); + } + + #region Event Handlers + + private void OnStartPrinting(Entity<MiningPointsLatheComponent> ent, ref LatheStartPrintingEvent args) + { + var points = args.Recipe.MiningPoints; + if (points > 0) + AddPoints(ent.Owner, points); + } + + private void OnClaimMiningPoints(Entity<MiningPointsLatheComponent> ent, ref LatheClaimMiningPointsMessage args) + { + var user = args.Actor; + if (TryFindIdCard(user) is {} dest) + TransferAll(ent.Owner, dest); + } + + #endregion + #region Public API + + /// <summary> + /// Tries to find the user's id card and gets its <see cref="MiningPointsComponent"/>. + /// </summary> + /// <remarks> + /// Component is nullable for easy usage with the API due to Entity<T> not being usable for Entity<T?> arguments. + /// </remarks> + public Entity<MiningPointsComponent?>? TryFindIdCard(EntityUid user) + { + if (!_idCard.TryFindIdCard(user, out var idCard)) + return null; + + if (!_query.TryComp(idCard, out var comp)) + return null; + + return (idCard, comp); + } + + /// <summary> + /// Removes points from a holder, returning true if it succeeded. + /// </summary> + public bool RemovePoints(Entity<MiningPointsComponent?> ent, uint amount) + { + if (!_query.Resolve(ent, ref ent.Comp) || amount > ent.Comp.Points) + return false; + + ent.Comp.Points -= amount; + Dirty(ent); + return true; + } + + /// <summary> + /// Add points to a holder. + /// </summary> + public bool AddPoints(Entity<MiningPointsComponent?> ent, uint amount) + { + if (!_query.Resolve(ent, ref ent.Comp)) + return false; + + ent.Comp.Points += amount; + Dirty(ent); + return true; + } + + /// <summary> + /// Transfer a number of points from source to destination. + /// Returns true if the transfer succeeded. + /// </summary> + public bool Transfer(Entity<MiningPointsComponent?> src, Entity<MiningPointsComponent?> dest, uint amount) + { + // don't make a sound or anything + if (amount == 0) + return true; + + if (!_query.Resolve(src, ref src.Comp) || !_query.Resolve(dest, ref dest.Comp)) + return false; + + if (!RemovePoints(src, amount)) + return false; + + AddPoints(dest, amount); + _audio.PlayPvs(src.Comp.TransferSound, src); + return true; + } + + /// <summary> + /// Transfers all points from source to destination. + /// Returns true if the transfer succeeded. + /// </summary> + public bool TransferAll(Entity<MiningPointsComponent?> src, Entity<MiningPointsComponent?> dest) + { + return _query.Resolve(src, ref src.Comp) && Transfer(src, dest, src.Comp.Points); + } + + #endregion +} diff --git a/Content.Shared/DeltaV/VendingMachines/PointsVendorComponent.cs b/Content.Shared/DeltaV/VendingMachines/PointsVendorComponent.cs new file mode 100644 index 00000000000..d505215a469 --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/PointsVendorComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.DeltaV.VendingMachines; + +/// <summary> +/// Makes a <see cref="ShopVendorComponent"/> use mining points to buy items. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class PointsVendorComponent : Component; diff --git a/Content.Shared/DeltaV/VendingMachines/SharedShopVendorSystem.cs b/Content.Shared/DeltaV/VendingMachines/SharedShopVendorSystem.cs new file mode 100644 index 00000000000..464711c32ab --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/SharedShopVendorSystem.cs @@ -0,0 +1,181 @@ +using Content.Shared.Access.Systems; +using Content.Shared.DeltaV.Salvage.Systems; +using Content.Shared.Destructible; +using Content.Shared.Popups; +using Content.Shared.Power; +using Content.Shared.Power.EntitySystems; +using Content.Shared.UserInterface; +using Content.Shared.VendingMachines; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.DeltaV.VendingMachines; + +public abstract class SharedShopVendorSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _access = default!; + [Dependency] private readonly MiningPointsSystem _points = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPointLightSystem _light = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPowerReceiverSystem _power = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<PointsVendorComponent, ShopVendorBalanceEvent>(OnPointsBalance); + SubscribeLocalEvent<PointsVendorComponent, ShopVendorPurchaseEvent>(OnPointsPurchase); + + SubscribeLocalEvent<ShopVendorComponent, PowerChangedEvent>(OnPowerChanged); + SubscribeLocalEvent<ShopVendorComponent, BreakageEventArgs>(OnBreak); + SubscribeLocalEvent<ShopVendorComponent, ActivatableUIOpenAttemptEvent>(OnOpenAttempt); + Subs.BuiEvents<ShopVendorComponent>(VendingMachineUiKey.Key, subs => + { + subs.Event<ShopVendorPurchaseMessage>(OnPurchase); + }); + } + + #region Public API + + public uint GetBalance(EntityUid uid, EntityUid user) + { + var ev = new ShopVendorBalanceEvent(user); + RaiseLocalEvent(uid, ref ev); + return ev.Balance; + } + + #endregion + + #region Balance adapters + + private void OnPointsBalance(Entity<PointsVendorComponent> ent, ref ShopVendorBalanceEvent args) + { + args.Balance = _points.TryFindIdCard(args.User)?.Comp?.Points ?? 0; + } + + private void OnPointsPurchase(Entity<PointsVendorComponent> ent, ref ShopVendorPurchaseEvent args) + { + if (_points.TryFindIdCard(args.User) is {} idCard && _points.RemovePoints(idCard, args.Cost)) + args.Paid = true; + } + + #endregion + + private void OnPowerChanged(Entity<ShopVendorComponent> ent, ref PowerChangedEvent args) + { + UpdateVisuals(ent); + } + + private void OnBreak(Entity<ShopVendorComponent> ent, ref BreakageEventArgs args) + { + ent.Comp.Broken = true; + UpdateVisuals(ent); + } + + private void OnOpenAttempt(Entity<ShopVendorComponent> ent, ref ActivatableUIOpenAttemptEvent args) + { + if (ent.Comp.Broken) + args.Cancel(); + } + + private void OnPurchase(Entity<ShopVendorComponent> ent, ref ShopVendorPurchaseMessage args) + { + if (ent.Comp.Ejecting != null || ent.Comp.Broken || !_power.IsPowered(ent.Owner)) + return; + + var pack = _proto.Index(ent.Comp.Pack); + if (args.Index < 0 || args.Index >= pack.Listings.Count) + return; + + var user = args.Actor; + if (!_access.IsAllowed(user, ent)) + { + Deny(ent, user); + return; + } + + var listing = pack.Listings[args.Index]; + var ev = new ShopVendorPurchaseEvent(user, listing.Cost); + RaiseLocalEvent(ent, ref ev); + if (!ev.Paid) + { + Deny(ent, user); + return; + } + + ent.Comp.Ejecting = listing.Id; + ent.Comp.NextEject = Timing.CurTime + ent.Comp.EjectDelay; + Dirty(ent); + + _audio.PlayPvs(ent.Comp.PurchaseSound, ent); + UpdateVisuals(ent); + + Log.Debug($"Player {ToPrettyString(user):user} purchased {listing.Id} from {ToPrettyString(ent):vendor}"); + + AfterPurchase(ent); + } + + protected virtual void AfterPurchase(Entity<ShopVendorComponent> ent) + { + } + + private void Deny(Entity<ShopVendorComponent> ent, EntityUid user) + { + _popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), ent, user); + if (ent.Comp.Denying) + return; + + ent.Comp.Denying = true; + ent.Comp.NextDeny = Timing.CurTime + ent.Comp.DenyDelay; + Dirty(ent); + + _audio.PlayPvs(ent.Comp.DenySound, ent); + UpdateVisuals(ent); + } + + protected void UpdateVisuals(Entity<ShopVendorComponent> ent) + { + var state = VendingMachineVisualState.Normal; + var lit = true; + if (ent.Comp.Broken) + { + state = VendingMachineVisualState.Broken; + lit = false; + } + else if (ent.Comp.Ejecting != null) + { + state = VendingMachineVisualState.Eject; + } + else if (ent.Comp.Denying) + { + state = VendingMachineVisualState.Deny; + } + else if (!_power.IsPowered(ent.Owner)) + { + state = VendingMachineVisualState.Off; + lit = true; + } + + _light.SetEnabled(ent, lit); + _appearance.SetData(ent, VendingMachineVisuals.VisualState, state); + } +} + +/// <summary> +/// Raised on a shop vendor to get its current balance. +/// A currency component sets Balance to whatever it is. +/// </summary> +[ByRefEvent] +public record struct ShopVendorBalanceEvent(EntityUid User, uint Balance = 0); + +/// <summary> +/// Raised on a shop vendor when trying to purchase an item. +/// A currency component sets Paid to true if the user successfully paid for it. +/// </summary> +[ByRefEvent] +public record struct ShopVendorPurchaseEvent(EntityUid User, uint Cost, bool Paid = false); diff --git a/Content.Shared/DeltaV/VendingMachines/ShopInventoryPrototype.cs b/Content.Shared/DeltaV/VendingMachines/ShopInventoryPrototype.cs new file mode 100644 index 00000000000..3b04c0d0490 --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/ShopInventoryPrototype.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.VendingMachines; + +/// <summary> +/// Similar to <c>VendingMachineInventoryPrototype</c> but for <see cref="ShopVendorComponent"/>. +/// </summary> +[Prototype] +public sealed class ShopInventoryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// <summary> + /// The item listings for sale. + /// </summary> + [DataField(required: true)] + public List<ShopListing> Listings = new(); +} + +[DataRecord, Serializable] +public record struct ShopListing(EntProtoId Id, uint Cost); diff --git a/Content.Shared/DeltaV/VendingMachines/ShopVendorComponent.cs b/Content.Shared/DeltaV/VendingMachines/ShopVendorComponent.cs new file mode 100644 index 00000000000..1de2c5476ed --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/ShopVendorComponent.cs @@ -0,0 +1,96 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.DeltaV.VendingMachines; + +/// <summary> +/// A vending machine that sells items for a currency controlled by events. +/// Does not need restocking. +/// Another component must handle <see cref="ShopVendorBalanceEvent"/> and <see cref="ShopVendorPurchaseEvent"/> to work. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(SharedShopVendorSystem))] +[AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class ShopVendorComponent : Component +{ + /// <summary> + /// The inventory prototype to sell. + /// </summary> + [DataField(required: true)] + public ProtoId<ShopInventoryPrototype> Pack; + + [DataField, AutoNetworkedField] + public bool Broken; + + [DataField, AutoNetworkedField] + public bool Denying; + + /// <summary> + /// Item being ejected, or null if it isn't. + /// </summary> + [DataField, AutoNetworkedField] + public EntProtoId? Ejecting; + + /// <summary> + /// How long to wait before flashing denied again. + /// </summary> + [DataField] + public TimeSpan DenyDelay = TimeSpan.FromSeconds(2); + + /// <summary> + /// How long to wait before another item can be bought + /// </summary> + [DataField] + public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2); + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextDeny; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextEject; + + [DataField] + public SoundSpecifier PurchaseSound = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg") + { + Params = new AudioParams + { + Volume = -4f, + Variation = 0.15f + } + }; + + [DataField] + public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg") + { + Params = new AudioParams + { + Volume = -2f + } + }; + + #region Visuals + + [DataField] + public bool LoopDenyAnimation = true; + + [DataField] + public string? OffState; + + [DataField] + public string? ScreenState; + + [DataField] + public string? NormalState; + + [DataField] + public string? DenyState; + + [DataField] + public string? EjectState; + + [DataField] + public string? BrokenState; + + #endregion +} diff --git a/Content.Shared/DeltaV/VendingMachines/ShopVendorUI.cs b/Content.Shared/DeltaV/VendingMachines/ShopVendorUI.cs new file mode 100644 index 00000000000..9f288d9d322 --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/ShopVendorUI.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.VendingMachines; + +[Serializable, NetSerializable] +public sealed class ShopVendorPurchaseMessage(int index) : BoundUserInterfaceMessage +{ + public readonly int Index = index; +} diff --git a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs index 83aa7085897..79a32268e80 100644 --- a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs +++ b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs @@ -13,6 +13,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public const string InvokedPort = "link_port"; @@ -529,7 +530,7 @@ private bool CanLink( private bool InRange(EntityUid sourceUid, EntityUid sinkUid, float range) { // TODO: This should be using an existing method and also coordinates inrange instead. - return Transform(sourceUid).MapPosition.InRange(Transform(sinkUid).MapPosition, range); + return _transform.GetMapCoordinates(sourceUid).InRange(_transform.GetMapCoordinates(sinkUid), range); } private void SendNewLinkEvent(EntityUid? user, EntityUid sourceUid, string source, EntityUid sinkUid, string sink) diff --git a/Content.Shared/Devour/SharedDevourSystem.cs b/Content.Shared/Devour/SharedDevourSystem.cs index 124daeffaaa..14047fba7dd 100644 --- a/Content.Shared/Devour/SharedDevourSystem.cs +++ b/Content.Shared/Devour/SharedDevourSystem.cs @@ -59,8 +59,7 @@ protected void OnDevourAction(EntityUid uid, DevourerComponent component, Devour _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, uid, component.DevourTime, new DevourDoAfterEvent(), uid, target: target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, }); break; default: @@ -78,8 +77,7 @@ protected void OnDevourAction(EntityUid uid, DevourerComponent component, Devour _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, uid, component.StructureDevourTime, new DevourDoAfterEvent(), uid, target: target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, }); } } diff --git a/Content.Shared/DoAfter/DoAfter.cs b/Content.Shared/DoAfter/DoAfter.cs index d999b370b3f..f77a6dc272e 100644 --- a/Content.Shared/DoAfter/DoAfter.cs +++ b/Content.Shared/DoAfter/DoAfter.cs @@ -82,12 +82,6 @@ public DoAfter(ushort index, DoAfterArgs args, TimeSpan startTime) { Index = index; - if (args.Target == null) - { - DebugTools.Assert(!args.BreakOnTargetMove); - args.BreakOnTargetMove = false; - } - Args = args; StartTime = startTime; } diff --git a/Content.Shared/DoAfter/DoAfterArgs.cs b/Content.Shared/DoAfter/DoAfterArgs.cs index 73334deddbe..d88f72c965f 100644 --- a/Content.Shared/DoAfter/DoAfterArgs.cs +++ b/Content.Shared/DoAfter/DoAfterArgs.cs @@ -92,29 +92,23 @@ public sealed partial class DoAfterArgs public bool BreakOnHandChange = true; /// <summary> - /// If do_after stops when the user moves + /// If do_after stops when the user or target moves /// </summary> - [DataField("breakOnUserMove")] - public bool BreakOnUserMove; - - /// <summary> - /// If this is true then any movement, even when weightless, will break the doafter. - /// When there is no gravity, BreakOnUserMove is ignored. If it is false to begin with nothing will change. - /// </summary> - [DataField("breakOnWeightlessMove")] - public bool BreakOnWeightlessMove; + [DataField] + public bool BreakOnMove; /// <summary> - /// If do_after stops when the target moves (if there is a target) + /// Whether to break on movement when the user is weightless. + /// This does nothing if <see cref="BreakOnMove"/> is false. /// </summary> - [DataField("breakOnTargetMove")] - public bool BreakOnTargetMove; + [DataField] + public bool BreakOnWeightlessMove = true; /// <summary> /// Threshold for user and target movement /// </summary> [DataField("movementThreshold")] - public float MovementThreshold = 0.1f; + public float MovementThreshold = 0.3f; /// <summary> /// Threshold for distance user from the used OR target entities. @@ -254,9 +248,8 @@ public DoAfterArgs(DoAfterArgs other) Broadcast = other.Broadcast; NeedHand = other.NeedHand; BreakOnHandChange = other.BreakOnHandChange; - BreakOnUserMove = other.BreakOnUserMove; + BreakOnMove = other.BreakOnMove; BreakOnWeightlessMove = other.BreakOnWeightlessMove; - BreakOnTargetMove = other.BreakOnTargetMove; MovementThreshold = other.MovementThreshold; DistanceThreshold = other.DistanceThreshold; BreakOnDamage = other.BreakOnDamage; diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs index 24b38417596..4f77a271b37 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs @@ -1,5 +1,7 @@ using Content.Shared.Gravity; using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Physics; using Robust.Shared.Utility; namespace Content.Shared.DoAfter; @@ -8,6 +10,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem { [Dependency] private readonly IDynamicTypeFactory _factory = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; private DoAfter[] _doAfters = Array.Empty<DoAfter>(); @@ -164,24 +167,53 @@ private bool ShouldCancel(DoAfter doAfter, return true; // TODO: Re-use existing xform query for these calculations. - // when there is no gravity you will be drifting 99% of the time making many doafters impossible - // so this just ignores your movement if you are weightless (unless the doafter sets BreakOnWeightlessMove then moving will still break it) - if (args.BreakOnUserMove - && !userXform.Coordinates.InRange(EntityManager, _transform, doAfter.UserPosition, args.MovementThreshold) - && (args.BreakOnWeightlessMove || !_gravity.IsWeightless(args.User, xform: userXform))) - return true; - - if (args.BreakOnTargetMove) + if (args.BreakOnMove && !(!args.BreakOnWeightlessMove && _gravity.IsWeightless(args.User, xform: userXform))) { - DebugTools.Assert(targetXform != null, "Break on move is true, but no target specified?"); - if (targetXform != null && targetXform.Coordinates.TryDistance(EntityManager, userXform.Coordinates, out var distance)) + // Whether the user has moved too much from their original position. + if (!userXform.Coordinates.InRange(EntityManager, _transform, doAfter.UserPosition, args.MovementThreshold)) + return true; + + // Whether the distance between the user and target(if any) has changed too much. + if (targetXform != null && + targetXform.Coordinates.TryDistance(EntityManager, userXform.Coordinates, out var distance)) { - // once the target moves too far from you the do after breaks if (Math.Abs(distance - doAfter.TargetDistance) > args.MovementThreshold) return true; } } + // Whether the user and the target are too far apart. + if (args.Target != null) + { + if (args.DistanceThreshold != null) + { + if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value, args.DistanceThreshold.Value)) + return true; + } + else + { + if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value)) + return true; + } + } + + // Whether the distance between the tool and the user has grown too much. + if (args.Used != null) + { + if (args.DistanceThreshold != null) + { + if (!_interaction.InRangeUnobstructed(args.User, + args.Used.Value, + args.DistanceThreshold.Value)) + return true; + } + else + { + if (!_interaction.InRangeUnobstructed(args.User,args.Used.Value)) + return true; + } + } + if (args.AttemptFrequency == AttemptFrequency.EveryTick && !TryAttemptEvent(doAfter)) return true; @@ -200,23 +232,6 @@ private bool ShouldCancel(DoAfter doAfter, if (args.RequireCanInteract && !_actionBlocker.CanInteract(args.User, args.Target)) return true; - if (args.DistanceThreshold != null) - { - if (targetXform != null - && !args.User.Equals(args.Target) - && !userXform.Coordinates.InRange(EntityManager, _transform, targetXform.Coordinates, - args.DistanceThreshold.Value)) - { - return true; - } - - if (usedXform != null - && !userXform.Coordinates.InRange(EntityManager, _transform, usedXform.Coordinates, - args.DistanceThreshold.Value)) - { - return true; - } - } return false; } diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index 81c8c4f3823..ed8be1ad657 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -215,12 +215,11 @@ public bool TryStartDoAfter(DoAfterArgs args, [NotNullWhen(true)] out DoAfterId? args.NetUser = GetNetEntity(args.User); args.NetEventTarget = GetNetEntity(args.EventTarget); - if (args.BreakOnUserMove || args.BreakOnTargetMove) + if (args.BreakOnMove) doAfter.UserPosition = Transform(args.User).Coordinates; - if (args.Target != null && args.BreakOnTargetMove) + if (args.Target != null && args.BreakOnMove) { - // Target should never be null if the bool is set. var targetPosition = Transform(args.Target.Value).Coordinates; doAfter.UserPosition.TryDistance(EntityManager, targetPosition, out doAfter.TargetDistance); } diff --git a/Content.Shared/Drowsiness/DrowsinessComponent.cs b/Content.Shared/Drowsiness/DrowsinessComponent.cs new file mode 100644 index 00000000000..7e170ed232e --- /dev/null +++ b/Content.Shared/Drowsiness/DrowsinessComponent.cs @@ -0,0 +1,28 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Drowsiness; + +/// <summary> +/// Exists for use as a status effect. Adds a shader to the client that scales with the effect duration. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] +public sealed partial class DrowsinessComponent : Component +{ + /// <summary> + /// The random time between sleeping incidents, (min, max). + /// </summary> + [DataField(required: true)] + public Vector2 TimeBetweenIncidents = new Vector2(5f, 60f); + + /// <summary> + /// The duration of sleeping incidents, (min, max). + /// </summary> + [DataField(required: true)] + public Vector2 DurationOfIncident = new Vector2(2, 5); + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextIncidentTime = TimeSpan.Zero; +} diff --git a/Content.Shared/Drowsiness/DrowsinessSystem.cs b/Content.Shared/Drowsiness/DrowsinessSystem.cs new file mode 100644 index 00000000000..97d7c0952dc --- /dev/null +++ b/Content.Shared/Drowsiness/DrowsinessSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.StatusEffect; + +namespace Content.Shared.Drowsiness; + +public abstract class SharedDrowsinessSystem : EntitySystem +{ + [ValidatePrototypeId<StatusEffectPrototype>] + public const string DrowsinessKey = "Drowsiness"; +} diff --git a/Content.Shared/EntityEffects/EntityEffect.cs b/Content.Shared/EntityEffects/EntityEffect.cs new file mode 100644 index 00000000000..21e79f224de --- /dev/null +++ b/Content.Shared/EntityEffects/EntityEffect.cs @@ -0,0 +1,132 @@ +using System.Linq; +using System.Text.Json.Serialization; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Content.Shared.Localizations; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Shared.EntityEffects; + +/// <summary> +/// Entity effects describe behavior that occurs on different kinds of triggers, e.g. when a reagent is ingested and metabolized by some +/// organ. They only trigger when all of <see cref="Conditions"/> are satisfied. +/// </summary> +[ImplicitDataDefinitionForInheritors] +[MeansImplicitUse] +public abstract partial class EntityEffect +{ + private protected string _id => this.GetType().Name; + /// <summary> + /// The list of conditions required for the effect to activate. Not required. + /// </summary> + [DataField("conditions")] + public EntityEffectCondition[]? Conditions; + + public virtual string ReagentEffectFormat => "guidebook-reagent-effect-description"; + + protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys); + + /// <summary> + /// What's the chance, from 0 to 1, that this effect will occur? + /// </summary> + [DataField("probability")] + public float Probability = 1.0f; + + public virtual LogImpact LogImpact { get; private set; } = LogImpact.Low; + + /// <summary> + /// Should this entity effect log at all? + /// </summary> + public virtual bool ShouldLog { get; private set; } = false; + + public abstract void Effect(EntityEffectBaseArgs args); + + /// <summary> + /// Produces a localized, bbcode'd guidebook description for this effect. + /// </summary> + /// <returns></returns> + public string? GuidebookEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var effect = ReagentEffectGuidebookText(prototype, entSys); + if (effect is null) + return null; + + return Loc.GetString(ReagentEffectFormat, ("effect", effect), ("chance", Probability), + ("conditionCount", Conditions?.Length ?? 0), + ("conditions", + ContentLocalizationManager.FormatList(Conditions?.Select(x => x.GuidebookExplanation(prototype)).ToList() ?? + new List<string>()))); + } +} + +public static class EntityEffectExt +{ + public static bool ShouldApply(this EntityEffect effect, EntityEffectBaseArgs args, + IRobustRandom? random = null) + { + if (random == null) + random = IoCManager.Resolve<IRobustRandom>(); + + if (effect.Probability < 1.0f && !random.Prob(effect.Probability)) + return false; + + if (effect.Conditions != null) + { + foreach (var cond in effect.Conditions) + { + if (!cond.Condition(args)) + return false; + } + } + + return true; + } +} + +/// <summary> +/// EntityEffectBaseArgs only contains the target of an effect. +/// If a trigger wants to include more info (e.g. the quantity of the chemical triggering the effect), it can be extended (see EntityEffectReagentArgs). +/// </summary> +public record class EntityEffectBaseArgs +{ + public EntityUid TargetEntity; + + public IEntityManager EntityManager = default!; + + public EntityEffectBaseArgs(EntityUid targetEntity, IEntityManager entityManager) + { + TargetEntity = targetEntity; + EntityManager = entityManager; + } +} + +public record class EntityEffectReagentArgs : EntityEffectBaseArgs +{ + public EntityUid? OrganEntity; + + public Solution? Source; + + public FixedPoint2 Quantity; + + public ReagentPrototype? Reagent; + + public ReactionMethod? Method; + + public FixedPoint2 Scale; + + public EntityEffectReagentArgs(EntityUid targetEntity, IEntityManager entityManager, EntityUid? organEntity, Solution? source, FixedPoint2 quantity, ReagentPrototype? reagent, ReactionMethod? method, FixedPoint2 scale) : base(targetEntity, entityManager) + { + OrganEntity = organEntity; + Source = source; + Quantity = quantity; + Reagent = reagent; + Method = method; + Scale = scale; + } +} diff --git a/Content.Shared/EntityEffects/EntityEffectCondition.cs b/Content.Shared/EntityEffects/EntityEffectCondition.cs new file mode 100644 index 00000000000..d6028b9f2c4 --- /dev/null +++ b/Content.Shared/EntityEffects/EntityEffectCondition.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects; + +[ImplicitDataDefinitionForInheritors] +[MeansImplicitUse] +public abstract partial class EntityEffectCondition +{ + [JsonPropertyName("id")] private protected string _id => this.GetType().Name; + + public abstract bool Condition(EntityEffectBaseArgs args); + + /// <summary> + /// Effect explanations are of the form "[chance to] [action] when [condition] and [condition]" + /// </summary> + /// <param name="prototype"></param> + /// <returns></returns> + public abstract string GuidebookExplanation(IPrototypeManager prototype); +} + diff --git a/Content.Shared/Execution/DoAfterEvent.cs b/Content.Shared/Execution/DoAfterEvent.cs new file mode 100644 index 00000000000..78549745276 --- /dev/null +++ b/Content.Shared/Execution/DoAfterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Execution; + +[Serializable, NetSerializable] +public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Execution/ExecutionComponent.cs b/Content.Shared/Execution/ExecutionComponent.cs new file mode 100644 index 00000000000..31477ead697 --- /dev/null +++ b/Content.Shared/Execution/ExecutionComponent.cs @@ -0,0 +1,77 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Execution; + +/// <summary> +/// Added to entities that can be used to execute another target. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ExecutionComponent : Component +{ + /// <summary> + /// How long the execution duration lasts. + /// </summary> + [DataField, AutoNetworkedField] + public float DoAfterDuration = 5f; + + /// <summary> + /// Arbitrarily chosen number to multiply damage by, used to deal reasonable amounts of damage to a victim of an execution. + /// /// </summary> + [DataField, AutoNetworkedField] + public float DamageMultiplier = 9f; + + /// <summary> + /// Shown to the person performing the melee execution (attacker) upon starting a melee execution. + /// </summary> + [DataField] + public LocId InternalMeleeExecutionMessage = "execution-popup-melee-initial-internal"; + + /// <summary> + /// Shown to bystanders and the victim of a melee execution when a melee execution is started. + /// </summary> + [DataField] + public LocId ExternalMeleeExecutionMessage = "execution-popup-melee-initial-external"; + + /// <summary> + /// Shown to the attacker upon completion of a melee execution. + /// </summary> + [DataField] + public LocId CompleteInternalMeleeExecutionMessage = "execution-popup-melee-complete-internal"; + + /// <summary> + /// Shown to bystanders and the victim of a melee execution when a melee execution is completed. + /// </summary> + [DataField] + public LocId CompleteExternalMeleeExecutionMessage = "execution-popup-melee-complete-external"; + + /// <summary> + /// Shown to the person performing the self execution when starting one. + /// </summary> + [DataField] + public LocId InternalSelfExecutionMessage = "execution-popup-self-initial-internal"; + + /// <summary> + /// Shown to bystanders near a self execution when one is started. + /// </summary> + [DataField] + public LocId ExternalSelfExecutionMessage = "execution-popup-self-initial-external"; + + /// <summary> + /// Shown to the person performing a self execution upon completion of a do-after or on use of /suicide with a weapon that has the Execution component. + /// </summary> + [DataField] + public LocId CompleteInternalSelfExecutionMessage = "execution-popup-self-complete-internal"; + + /// <summary> + /// Shown to bystanders when a self execution is completed or a suicide via execution weapon happens nearby. + /// </summary> + [DataField] + public LocId CompleteExternalSelfExecutionMessage = "execution-popup-self-complete-external"; + + // Not networked because this is transient inside of a tick. + /// <summary> + /// True if it is currently executing for handlers. + /// </summary> + [DataField] + public bool Executing = false; +} diff --git a/Content.Shared/Execution/SharedExecutionSystem.cs b/Content.Shared/Execution/SharedExecutionSystem.cs new file mode 100644 index 00000000000..616b43b1a8f --- /dev/null +++ b/Content.Shared/Execution/SharedExecutionSystem.cs @@ -0,0 +1,235 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.Chat; +using Content.Shared.CombatMode; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Interaction.Events; +using Content.Shared.Mind; +using Content.Shared.Tag; +using Content.Shared.Weapons.Ranged.Components; +using Robust.Shared.Player; +using Robust.Shared.Audio.Systems; + +namespace Content.Shared.Execution; + +/// <summary> +/// Verb for violently murdering cuffed creatures. +/// </summary> +public sealed class SharedExecutionSystem : EntitySystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedSuicideSystem _suicide = default!; + [Dependency] private readonly SharedCombatModeSystem _combat = default!; + [Dependency] private readonly SharedExecutionSystem _execution = default!; + [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + + /// <inheritdoc/> + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ExecutionComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionsVerbs); + SubscribeLocalEvent<ExecutionComponent, GetMeleeDamageEvent>(OnGetMeleeDamage); + SubscribeLocalEvent<ExecutionComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment); + SubscribeLocalEvent<ExecutionComponent, ExecutionDoAfterEvent>(OnExecutionDoAfter); + } + + private void OnGetInteractionsVerbs(EntityUid uid, ExecutionComponent comp, GetVerbsEvent<UtilityVerb> args) + { + if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + return; + + var attacker = args.User; + var weapon = args.Using.Value; + var victim = args.Target; + + if (!CanBeExecuted(victim, attacker, weapon)) + return; + + UtilityVerb verb = new() + { + Act = () => TryStartExecutionDoAfter(weapon, victim, attacker, comp), + Impact = LogImpact.High, + Text = Loc.GetString("execution-verb-name"), + Message = Loc.GetString("execution-verb-message"), + }; + + args.Verbs.Add(verb); + } + + private void TryStartExecutionDoAfter(EntityUid weapon, EntityUid victim, EntityUid attacker, ExecutionComponent comp) + { + if (!CanBeExecuted(victim, attacker, weapon)) + return; + + if (attacker == victim) + { + ShowExecutionInternalPopup(comp.InternalSelfExecutionMessage, attacker, victim, weapon); + ShowExecutionExternalPopup(comp.ExternalSelfExecutionMessage, attacker, victim, weapon); + } + else + { + ShowExecutionInternalPopup(comp.InternalMeleeExecutionMessage, attacker, victim, weapon); + ShowExecutionExternalPopup(comp.ExternalMeleeExecutionMessage, attacker, victim, weapon); + } + + var doAfter = + new DoAfterArgs(EntityManager, attacker, comp.DoAfterDuration, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + _doAfter.TryStartDoAfter(doAfter); + + } + + public bool CanBeExecuted(EntityUid victim, EntityUid attacker, EntityUid weapon) + { + // No point executing someone if they can't take damage + if (!HasComp<DamageableComponent>(victim)) + return false; + + // You can't execute something that cannot die + if (!TryComp<MobStateComponent>(victim, out var mobState)) + return false; + + // You're not allowed to execute dead people (no fun allowed) + if (_mobState.IsDead(victim, mobState)) + return false; + + // You must be able to attack people to execute + if (!_actionBlocker.CanAttack(attacker, victim)) + return false; + + // The victim must be incapacitated to be executed + if (victim != attacker && _actionBlocker.CanInteract(victim, null)) + return false; + + // All checks passed + return true; + } + + private void OnGetMeleeDamage(Entity<ExecutionComponent> entity, ref GetMeleeDamageEvent args) + { + if (!TryComp<MeleeWeaponComponent>(entity, out var melee) || !entity.Comp.Executing) + return; + + var bonus = melee.Damage * entity.Comp.DamageMultiplier - melee.Damage; + args.Damage += bonus; + args.ResistanceBypass = true; + } + + private void OnSuicideByEnvironment(Entity<ExecutionComponent> entity, ref SuicideByEnvironmentEvent args) + { + if (!TryComp<MeleeWeaponComponent>(entity, out var melee)) + return; + + string? internalMsg = entity.Comp.CompleteInternalSelfExecutionMessage; + string? externalMsg = entity.Comp.CompleteExternalSelfExecutionMessage; + + if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent)) + return; + + ShowExecutionInternalPopup(internalMsg, args.Victim, args.Victim, entity, false); + ShowExecutionExternalPopup(externalMsg, args.Victim, args.Victim, entity); + _audio.PlayPredicted(melee.SoundHit, args.Victim, args.Victim); + _suicide.ApplyLethalDamage((args.Victim, damageableComponent), melee.Damage); + args.Handled = true; + } + + private void ShowExecutionInternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon, bool predict = true) + { + if (predict) + { + _popup.PopupClient( + Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, + attacker, + PopupType.MediumCaution + ); + } + else + { + _popup.PopupEntity( + Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, + attacker, + PopupType.MediumCaution + ); + } + } + + private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon) + { + _popup.PopupEntity( + Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, + Filter.PvsExcept(attacker), + true, + PopupType.MediumCaution + ); + } + + private void OnExecutionDoAfter(Entity<ExecutionComponent> entity, ref ExecutionDoAfterEvent args) + { + if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) + return; + + if (!TryComp<MeleeWeaponComponent>(entity, out var meleeWeaponComp)) + return; + + var attacker = args.User; + var victim = args.Target.Value; + var weapon = args.Used.Value; + + if (!_execution.CanBeExecuted(victim, attacker, weapon)) + return; + + // This is needed so the melee system does not stop it. + var prev = _combat.IsInCombatMode(attacker); + _combat.SetInCombatMode(attacker, true); + entity.Comp.Executing = true; + + var internalMsg = entity.Comp.CompleteInternalMeleeExecutionMessage; + var externalMsg = entity.Comp.CompleteExternalMeleeExecutionMessage; + + if (attacker == victim) + { + var suicideEvent = new SuicideEvent(victim); + RaiseLocalEvent(victim, suicideEvent); + + var suicideGhostEvent = new SuicideGhostEvent(victim); + RaiseLocalEvent(victim, suicideGhostEvent); + } + else + _melee.AttemptLightAttack(attacker, weapon, meleeWeaponComp, victim); + + _combat.SetInCombatMode(attacker, prev); + entity.Comp.Executing = false; + args.Handled = true; + + if (attacker != victim) + { + _execution.ShowExecutionInternalPopup(internalMsg, attacker, victim, entity); + _execution.ShowExecutionExternalPopup(externalMsg, attacker, victim, entity); + } + } +} diff --git a/Content.Shared/Explosion/ExplosionPrototype.cs b/Content.Shared/Explosion/ExplosionPrototype.cs index b2aa83c40dd..df2fb18360e 100644 --- a/Content.Shared/Explosion/ExplosionPrototype.cs +++ b/Content.Shared/Explosion/ExplosionPrototype.cs @@ -25,6 +25,12 @@ public sealed partial class ExplosionPrototype : IPrototype [DataField("damagePerIntensity", required: true)] public DamageSpecifier DamagePerIntensity = default!; + /// <summary> + /// Amount of firestacks to apply in addition to igniting. + /// </summary> + [DataField] + public float? FireStacks; + /// <summary> /// This set of points, together with <see cref="_tileBreakIntensity"/> define a function that maps the /// explosion intensity to a tile break chance via linear interpolation. diff --git a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs index 767d30389ac..2ce008da262 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs @@ -75,7 +75,7 @@ private void AddSpillVerb(Entity<SpillableComponent> entity, ref GetVerbsEvent<V _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, entity.Comp.SpillDelay ?? 0, new SpillDoAfterEvent(), entity.Owner, target: entity.Owner) { BreakOnDamage = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true, }); }; diff --git a/Content.Shared/Friends/Components/PettableFriendComponent.cs b/Content.Shared/Friends/Components/PettableFriendComponent.cs new file mode 100644 index 00000000000..d05e1769aec --- /dev/null +++ b/Content.Shared/Friends/Components/PettableFriendComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Friends.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Friends.Components; + +/// <summary> +/// Pet something to become friends with it (use in hand, press Z) +/// Requires this entity to have FactionExceptionComponent to work. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(PettableFriendSystem))] +public sealed partial class PettableFriendComponent : Component +{ + /// <summary> + /// Localized popup sent when petting for the first time + /// </summary> + [DataField(required: true)] + public LocId SuccessString = string.Empty; + + /// <summary> + /// Localized popup sent when petting multiple times + /// </summary> + [DataField(required: true)] + public LocId FailureString = string.Empty; +} diff --git a/Content.Shared/Friends/Systems/PettableFriendSystem.cs b/Content.Shared/Friends/Systems/PettableFriendSystem.cs new file mode 100644 index 00000000000..00a4ddd155c --- /dev/null +++ b/Content.Shared/Friends/Systems/PettableFriendSystem.cs @@ -0,0 +1,62 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Friends.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; +using Content.Shared.Popups; +using Content.Shared.Timing; + +namespace Content.Shared.Friends.Systems; + +public sealed class PettableFriendSystem : EntitySystem +{ + [Dependency] private readonly NpcFactionSystem _factionException = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + private EntityQuery<FactionExceptionComponent> _exceptionQuery; + private EntityQuery<UseDelayComponent> _useDelayQuery; + + public override void Initialize() + { + base.Initialize(); + + _exceptionQuery = GetEntityQuery<FactionExceptionComponent>(); + _useDelayQuery = GetEntityQuery<UseDelayComponent>(); + + SubscribeLocalEvent<PettableFriendComponent, UseInHandEvent>(OnUseInHand); + SubscribeLocalEvent<PettableFriendComponent, GotRehydratedEvent>(OnRehydrated); + } + + private void OnUseInHand(Entity<PettableFriendComponent> ent, ref UseInHandEvent args) + { + var (uid, comp) = ent; + var user = args.User; + if (args.Handled || !_exceptionQuery.TryGetComponent(uid, out var exceptionComp)) + return; + + if (_useDelayQuery.TryGetComponent(uid, out var useDelay) && !_useDelay.TryResetDelay((uid, useDelay), true)) + return; + + var exception = (uid, exceptionComp); + if (_factionException.IsIgnored(exception, user)) + { + _popup.PopupClient(Loc.GetString(comp.FailureString, ("target", uid)), user, user); + return; + } + + // you have made a new friend :) + _popup.PopupClient(Loc.GetString(comp.SuccessString, ("target", uid)), user, user); + _factionException.IgnoreEntity(exception, user); + args.Handled = true; + } + + private void OnRehydrated(Entity<PettableFriendComponent> ent, ref GotRehydratedEvent args) + { + // can only pet before hydrating, after that the fish cannot be negotiated with + if (!TryComp<FactionExceptionComponent>(ent, out var comp)) + return; + + _factionException.IgnoreEntities(args.Target, comp.Ignored); + } +} diff --git a/Content.Shared/GPS/Components/HandheldGPSComponent.cs b/Content.Shared/GPS/Components/HandheldGPSComponent.cs new file mode 100644 index 00000000000..a0af0909867 --- /dev/null +++ b/Content.Shared/GPS/Components/HandheldGPSComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.GPS.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class HandheldGPSComponent : Component +{ + [DataField] + public float UpdateRate = 1.5f; +} diff --git a/Content.Shared/Glue/GluedComponent.cs b/Content.Shared/Glue/GluedComponent.cs index fd7a52fdb13..4b46f0aa5b0 100644 --- a/Content.Shared/Glue/GluedComponent.cs +++ b/Content.Shared/Glue/GluedComponent.cs @@ -6,11 +6,6 @@ namespace Content.Shared.Glue; [Access(typeof(SharedGlueSystem))] public sealed partial class GluedComponent : Component { - /// <summary> - /// Reverts name to before prefix event (essentially removes prefix). - /// </summary> - [DataField("beforeGluedEntityName"), ViewVariables(VVAccess.ReadOnly)] - public string BeforeGluedEntityName = string.Empty; [DataField("until", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] public TimeSpan Until; diff --git a/Content.Shared/Gravity/SharedGravityGeneratorComponent.cs b/Content.Shared/Gravity/SharedGravityGeneratorComponent.cs index 1f78e333f15..75b636b2fa8 100644 --- a/Content.Shared/Gravity/SharedGravityGeneratorComponent.cs +++ b/Content.Shared/Gravity/SharedGravityGeneratorComponent.cs @@ -1,118 +1,44 @@ +using Content.Shared.Power; using Robust.Shared.GameStates; -using Robust.Shared.Serialization; -namespace Content.Shared.Gravity -{ - [NetworkedComponent()] - [Virtual] - public partial class SharedGravityGeneratorComponent : Component - { - /// <summary> - /// A map of the sprites used by the gravity generator given its status. - /// </summary> - [DataField("spriteMap")] - [Access(typeof(SharedGravitySystem))] - public Dictionary<GravityGeneratorStatus, string> SpriteMap = new(); - - /// <summary> - /// The sprite used by the core of the gravity generator when the gravity generator is starting up. - /// </summary> - [DataField("coreStartupState")] - [ViewVariables(VVAccess.ReadWrite)] - public string CoreStartupState = "startup"; - - /// <summary> - /// The sprite used by the core of the gravity generator when the gravity generator is idle. - /// </summary> - [DataField("coreIdleState")] - [ViewVariables(VVAccess.ReadWrite)] - public string CoreIdleState = "idle"; - - /// <summary> - /// The sprite used by the core of the gravity generator when the gravity generator is activating. - /// </summary> - [DataField("coreActivatingState")] - [ViewVariables(VVAccess.ReadWrite)] - public string CoreActivatingState = "activating"; - - /// <summary> - /// The sprite used by the core of the gravity generator when the gravity generator is active. - /// </summary> - [DataField("coreActivatedState")] - [ViewVariables(VVAccess.ReadWrite)] - public string CoreActivatedState = "activated"; - - /// <summary> - /// Sent to the server to set whether the generator should be on or off - /// </summary> - [Serializable, NetSerializable] - public sealed class SwitchGeneratorMessage : BoundUserInterfaceMessage - { - public bool On; - - public SwitchGeneratorMessage(bool on) - { - On = on; - } - } +namespace Content.Shared.Gravity; - [Serializable, NetSerializable] - public sealed class GeneratorState : BoundUserInterfaceState - { - public bool On; - // 0 -> 255 - public byte Charge; - public GravityGeneratorPowerStatus PowerStatus; - public short PowerDraw; - public short PowerDrawMax; - public short EtaSeconds; - - public GeneratorState( - bool on, - byte charge, - GravityGeneratorPowerStatus powerStatus, - short powerDraw, - short powerDrawMax, - short etaSeconds) - { - On = on; - Charge = charge; - PowerStatus = powerStatus; - PowerDraw = powerDraw; - PowerDrawMax = powerDrawMax; - EtaSeconds = etaSeconds; - } - } - - [Serializable, NetSerializable] - public enum GravityGeneratorUiKey - { - Key - } - } - - [Serializable, NetSerializable] - public enum GravityGeneratorVisuals - { - State, - Charge - } - - [Serializable, NetSerializable] - public enum GravityGeneratorStatus - { - Broken, - Unpowered, - Off, - On - } - - [Serializable, NetSerializable] - public enum GravityGeneratorPowerStatus : byte - { - Off, - Discharging, - Charging, - FullyCharged - } +[NetworkedComponent()] +[Virtual] +public partial class SharedGravityGeneratorComponent : Component +{ + /// <summary> + /// A map of the sprites used by the gravity generator given its status. + /// </summary> + [DataField("spriteMap")] + [Access(typeof(SharedGravitySystem))] + public Dictionary<PowerChargeStatus, string> SpriteMap = new(); + + /// <summary> + /// The sprite used by the core of the gravity generator when the gravity generator is starting up. + /// </summary> + [DataField("coreStartupState")] + [ViewVariables(VVAccess.ReadWrite)] + public string CoreStartupState = "startup"; + + /// <summary> + /// The sprite used by the core of the gravity generator when the gravity generator is idle. + /// </summary> + [DataField("coreIdleState")] + [ViewVariables(VVAccess.ReadWrite)] + public string CoreIdleState = "idle"; + + /// <summary> + /// The sprite used by the core of the gravity generator when the gravity generator is activating. + /// </summary> + [DataField("coreActivatingState")] + [ViewVariables(VVAccess.ReadWrite)] + public string CoreActivatingState = "activating"; + + /// <summary> + /// The sprite used by the core of the gravity generator when the gravity generator is active. + /// </summary> + [DataField("coreActivatedState")] + [ViewVariables(VVAccess.ReadWrite)] + public string CoreActivatedState = "activated"; } diff --git a/Content.Shared/Gravity/SharedGravitySystem.cs b/Content.Shared/Gravity/SharedGravitySystem.cs index 59d75e453af..f9c65e9477b 100644 --- a/Content.Shared/Gravity/SharedGravitySystem.cs +++ b/Content.Shared/Gravity/SharedGravitySystem.cs @@ -1,9 +1,7 @@ using Content.Shared.Alert; -using Content.Shared.Clothing; using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Robust.Shared.GameStates; -using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Serialization; @@ -16,7 +14,6 @@ public abstract partial class SharedGravitySystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly AlertsSystem _alerts = default!; - [Dependency] private readonly InventorySystem _inventory = default!; [ValidatePrototypeId<AlertPrototype>] public const string WeightlessAlert = "Weightless"; @@ -34,6 +31,11 @@ public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, Transform if (TryComp<MovementIgnoreGravityComponent>(uid, out var ignoreGravityComponent)) return ignoreGravityComponent.Weightless; + var ev = new IsWeightlessEvent(uid); + RaiseLocalEvent(uid, ref ev); + if (ev.Handled) + return ev.IsWeightless; + if (!Resolve(uid, ref xform)) return true; @@ -44,18 +46,6 @@ public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, Transform return false; } - var hasGrav = gravity != null || mapGravity != null; - - // Check for something holding us down - // If the planet has gravity component and no gravity it will still give gravity - // If there's no gravity comp at all (i.e. space) then they don't work. - if (hasGrav && _inventory.TryGetSlotEntity(uid, "shoes", out var ent)) - { - // TODO this should just be a event that gets relayed instead of a specific slot & component check. - if (TryComp<MagbootsComponent>(ent, out var boots) && boots.On) - return false; - } - return true; } @@ -78,9 +68,11 @@ public override void Update(float frameTime) private void OnHandleState(EntityUid uid, GravityComponent component, ref ComponentHandleState args) { - if (args.Current is not GravityComponentState state) return; + if (args.Current is not GravityComponentState state) + return; - if (component.EnabledVV == state.Enabled) return; + if (component.EnabledVV == state.Enabled) + return; component.EnabledVV = state.Enabled; var ev = new GravityChangedEvent(uid, component.EnabledVV); RaiseLocalEvent(uid, ref ev, true); @@ -94,9 +86,10 @@ private void OnGetState(EntityUid uid, GravityComponent component, ref Component private void OnGravityChange(ref GravityChangedEvent ev) { var alerts = AllEntityQuery<AlertsComponent, TransformComponent>(); - while(alerts.MoveNext(out var uid, out var comp, out var xform)) + while(alerts.MoveNext(out var uid, out _, out var xform)) { - if (xform.GridUid != ev.ChangedGridIndex) continue; + if (xform.GridUid != ev.ChangedGridIndex) + continue; if (!ev.HasGravity) { @@ -149,4 +142,10 @@ public GravityComponentState(bool enabled) } } } -} \ No newline at end of file + + [ByRefEvent] + public record struct IsWeightlessEvent(EntityUid Entity, bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent + { + SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET; + } +} diff --git a/Content.Shared/Hands/Components/HandsComponent.cs b/Content.Shared/Hands/Components/HandsComponent.cs index a7464e5bac7..f61e0600436 100644 --- a/Content.Shared/Hands/Components/HandsComponent.cs +++ b/Content.Shared/Hands/Components/HandsComponent.cs @@ -39,11 +39,11 @@ public sealed partial class HandsComponent : Component public bool DisableExplosionRecursion = false; /// <summary> - /// The amount of throw impulse per distance the player is from the throw target. + /// Modifies the speed at which items are thrown. /// </summary> - [DataField("throwForceMultiplier")] + [DataField] [ViewVariables(VVAccess.ReadWrite)] - public float ThrowForceMultiplier { get; set; } = 10f; //should be tuned so that a thrown item lands about under the player's cursor + public float BaseThrowspeed { get; set; } = 10f; /// <summary> /// Distance after which longer throw targets stop increasing throw impulse. diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs index 20e08b2767d..d1f41738e9d 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs @@ -108,10 +108,10 @@ public bool TryPickup( var xform = Transform(uid); var coordinateEntity = xform.ParentUid.IsValid() ? xform.ParentUid : uid; var itemXform = Transform(entity); - var itemPos = itemXform.MapPosition; + var itemPos = TransformSystem.GetMapCoordinates(entity, xform: itemXform); if (itemPos.MapId == xform.MapID - && (itemPos.Position - xform.MapPosition.Position).Length() <= MaxAnimationRange + && (itemPos.Position - TransformSystem.GetMapCoordinates(uid, xform: xform).Position).Length() <= MaxAnimationRange && MetaData(entity).VisibilityMask == MetaData(uid).VisibilityMask) // Don't animate aghost pickups. { var initialPosition = EntityCoordinates.FromMap(coordinateEntity, itemPos, TransformSystem, EntityManager); diff --git a/Content.Shared/IdentityManagement/TryGetIdentityShortInfoEvent.cs b/Content.Shared/IdentityManagement/TryGetIdentityShortInfoEvent.cs new file mode 100644 index 00000000000..1303896e7b8 --- /dev/null +++ b/Content.Shared/IdentityManagement/TryGetIdentityShortInfoEvent.cs @@ -0,0 +1,32 @@ +namespace Content.Shared.IdentityManagement; + +/// <summary> +/// Event of attempt to collect actor full title - full name + job from id card for employee or entity name for borgs. +/// </summary> +public sealed class TryGetIdentityShortInfoEvent(EntityUid? whileInteractingWith, EntityUid forActor, bool forLogging = false) : HandledEntityEventArgs +{ + /// <summary> + /// Full name of <see cref="ForActor"/>, with JobTitle. + /// Can be null if no system could find actor name / job. + /// </summary> + public string? Title; + + /// <summary> + /// Entity for interacting with which title should be collected. + /// Could be used to black-out name of people when announcing actions + /// on e-magged devices. + /// </summary> + public readonly EntityUid? WhileInteractingWith = whileInteractingWith; + + /// <summary> + /// Actor for whom title should be collected. + /// </summary> + public readonly EntityUid ForActor = forActor; + + /// <summary> + /// Marker that title info was requested for access logging. + /// Is required as event handlers can determine, if they don't need + /// to place title info due to access logging restrictions. + /// </summary> + public readonly bool RequestForAccessLogging = forLogging; +} diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index a43d4fca723..eee7a9cf958 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -30,7 +30,7 @@ public override void Initialize() SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent); SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent); SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent); - SubscribeLocalEvent<ImplantedComponent, TransformSpeakerSpeechEvent>(RelayToImplantEvent); + SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent); } private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args) @@ -96,20 +96,36 @@ private void OnRemove(EntityUid uid, SubdermalImplantComponent component, EntGot /// </summary> public void AddImplants(EntityUid uid, IEnumerable<String> implants) { - var coords = Transform(uid).Coordinates; foreach (var id in implants) { - var ent = Spawn(id, coords); - if (TryComp<SubdermalImplantComponent>(ent, out var implant)) - { - ForceImplant(uid, ent, implant); - } - else - { - Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}"); - Del(ent); - } + AddImplant(uid, id); + } + } + + /// <summary> + /// Adds a single implant to a person, and returns the implant. + /// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>. + /// </summary> + /// <returns> + /// The implant, if it was successfully created. Otherwise, null. + /// </returns>> + public EntityUid? AddImplant(EntityUid uid, String implantId) + { + var coords = Transform(uid).Coordinates; + var ent = Spawn(implantId, coords); + + if (TryComp<SubdermalImplantComponent>(ent, out var implant)) + { + ForceImplant(uid, ent, implant); + } + else + { + Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}"); + Del(ent); + return null; } + + return ent; } /// <summary> diff --git a/Content.Shared/Info/RulesMessages.cs b/Content.Shared/Info/RulesMessages.cs index 9cb73c9aa86..ac7400238f0 100644 --- a/Content.Shared/Info/RulesMessages.cs +++ b/Content.Shared/Info/RulesMessages.cs @@ -23,3 +23,19 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer buffer.Write(PopupTime); } } + +/// <summary> +/// Sent by the client when it has accepted the rules. +/// </summary> +public sealed class RulesAcceptedMessage : NetMessage +{ + public override MsgGroups MsgGroup => MsgGroups.Command; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + } +} diff --git a/Content.Shared/Interaction/Components/ClumsyComponent.cs b/Content.Shared/Interaction/Components/ClumsyComponent.cs deleted file mode 100644 index 824696c8385..00000000000 --- a/Content.Shared/Interaction/Components/ClumsyComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Audio; -using Robust.Shared.GameStates; - -namespace Content.Shared.Interaction.Components; - -/// <summary> -/// A simple clumsy tag-component. -/// </summary> -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class ClumsyComponent : Component -{ - /// <summary> - /// Damage dealt to a clumsy character when they try to fire a gun. - /// </summary> - [DataField(required: true), AutoNetworkedField] - public DamageSpecifier ClumsyDamage = default!; - - /// <summary> - /// Sound to play when clumsy interactions fail. - /// </summary> - [DataField] - public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); -} diff --git a/Content.Shared/Interaction/Events/SuicideEvent.cs b/Content.Shared/Interaction/Events/SuicideEvent.cs index 7b9c1efe0dd..bcd5df67abf 100644 --- a/Content.Shared/Interaction/Events/SuicideEvent.cs +++ b/Content.Shared/Interaction/Events/SuicideEvent.cs @@ -1,48 +1,41 @@ -namespace Content.Shared.Interaction.Events +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Interaction.Events; + +/// <summary> +/// Raised Directed at an entity to check whether they will handle the suicide. +/// </summary> +public sealed class SuicideEvent : HandledEntityEventArgs { - /// <summary> - /// Raised Directed at an entity to check whether they will handle the suicide. - /// </summary> - public sealed class SuicideEvent : EntityEventArgs + public SuicideEvent(EntityUid victim) { - public SuicideEvent(EntityUid victim) - { - Victim = victim; - } - public void SetHandled(SuicideKind kind) - { - if (Handled) - throw new InvalidOperationException("Suicide was already handled"); - - Kind = kind; - } + Victim = victim; + } - public void BlockSuicideAttempt(bool suicideAttempt) - { - if (suicideAttempt) - AttemptBlocked = suicideAttempt; - } + public DamageSpecifier? DamageSpecifier; + public ProtoId<DamageTypePrototype>? DamageType; + public EntityUid Victim { get; private set; } +} - public SuicideKind? Kind { get; private set; } - public EntityUid Victim { get; private set; } - public bool AttemptBlocked { get; private set; } - public bool Handled => Kind != null; +public sealed class SuicideByEnvironmentEvent : HandledEntityEventArgs +{ + public SuicideByEnvironmentEvent(EntityUid victim) + { + Victim = victim; } - public enum SuicideKind - { - Special, //Doesn't damage the mob, used for "weird" suicides like gibbing + public EntityUid Victim { get; set; } +} - //Damage type suicides - Blunt, - Slash, - Piercing, - Heat, - Shock, - Cold, - Poison, - Radiation, - Asphyxiation, - Bloodloss +public sealed class SuicideGhostEvent : HandledEntityEventArgs +{ + public SuicideGhostEvent(EntityUid victim) + { + Victim = victim; } + + public EntityUid Victim { get; set; } + public bool CanReturnToBody; } diff --git a/Content.Shared/Interaction/RotateToFaceSystem.cs b/Content.Shared/Interaction/RotateToFaceSystem.cs index fa213011ef1..6fe5582bb17 100644 --- a/Content.Shared/Interaction/RotateToFaceSystem.cs +++ b/Content.Shared/Interaction/RotateToFaceSystem.cs @@ -69,7 +69,7 @@ public bool TryFaceCoordinates(EntityUid user, Vector2 coordinates, TransformCom if (!Resolve(user, ref xform)) return false; - var diff = coordinates - xform.MapPosition.Position; + var diff = coordinates - _transform.GetMapCoordinates(user, xform: xform).Position; if (diff.LengthSquared() <= 0.01f) return true; diff --git a/Content.Shared/Interaction/SharedInteractionSystem.Clumsy.cs b/Content.Shared/Interaction/SharedInteractionSystem.Clumsy.cs deleted file mode 100644 index 9e45847e078..00000000000 --- a/Content.Shared/Interaction/SharedInteractionSystem.Clumsy.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Interaction.Components; -using Robust.Shared.Random; - -namespace Content.Shared.Interaction -{ - public partial class SharedInteractionSystem - { - public bool RollClumsy(ClumsyComponent component, float chance) - { - return component.Running && _random.Prob(chance); - } - - /// <summary> - /// Rolls a probability chance for a "bad action" if the target entity is clumsy. - /// </summary> - /// <param name="entity">The entity that the clumsy check is happening for.</param> - /// <param name="chance"> - /// The chance that a "bad action" happens if the user is clumsy, between 0 and 1 inclusive. - /// </param> - /// <returns>True if a "bad action" happened, false if the normal action should happen.</returns> - public bool TryRollClumsy(EntityUid entity, float chance, ClumsyComponent? component = null) - { - return Resolve(entity, ref component, false) && RollClumsy(component, chance); - } - } -} diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index cc548fd4b16..24bb1b2446d 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -21,6 +21,7 @@ using Content.Shared.Players.RateLimiting; using Content.Shared.Popups; using Content.Shared.Storage; +using Content.Shared.Strip; using Content.Shared.Tag; using Content.Shared.Timing; using Content.Shared.UserInterface; @@ -42,8 +43,6 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; -#pragma warning disable 618 - namespace Content.Shared.Interaction { /// <summary> @@ -69,6 +68,7 @@ public abstract partial class SharedInteractionSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + [Dependency] private readonly SharedStrippableSystem _strippable = default!; [Dependency] private readonly SharedPlayerRateLimitManager _rateLimit = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly ISharedChatManager _chat = default!; @@ -191,10 +191,7 @@ private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev return; } - if (!uiComp.RequireHands) - return; - - if (!_handsQuery.TryComp(ev.Actor, out var hands) || hands.Hands.Count == 0) + if (uiComp.RequireHands && !_handsQuery.HasComp(ev.Actor)) ev.Cancel(); } @@ -556,11 +553,11 @@ public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? targe protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates) { // Verify user is on the same map as the entity they clicked on - if (coordinates.GetMapId(EntityManager) != Transform(user).MapID) + if (_transform.GetMapId(coordinates) != Transform(user).MapID) return false; if (!HasComp<NoRotateOnInteractComponent>(user)) - _rotateToFaceSystem.TryFaceCoordinates(user, coordinates.ToMapPos(EntityManager, _transform)); + _rotateToFaceSystem.TryFaceCoordinates(user, _transform.ToMapCoordinates(coordinates).Position); return true; } @@ -781,8 +778,8 @@ public bool InRangeUnobstructed( // No fixtures, e.g. wallmounts. else { - originPos = _transform.GetMapCoordinates(origin, origin); - var otherParent = (other.Comp ?? Transform(other)).ParentUid; + originPos = _transform.GetMapCoordinates(origin, xform: origin.Comp); + var otherParent = Transform(other).ParentUid; targetRot = otherParent.IsValid() ? Transform(otherParent).LocalRotation + otherAngle : otherAngle; } @@ -893,7 +890,7 @@ public bool InRangeUnobstructed( Ignored? predicate = null, bool popup = false) { - return InRangeUnobstructed(origin, other.ToMap(EntityManager, _transform), range, collisionMask, predicate, popup); + return InRangeUnobstructed(origin, _transform.ToMapCoordinates(other), range, collisionMask, predicate, popup); } /// <summary> @@ -929,7 +926,7 @@ public bool InRangeUnobstructed( bool popup = false) { Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false); - var originPosition = Transform(origin).MapPosition; + var originPosition = _transform.GetMapCoordinates(origin); var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, combinedPredicate, ShouldCheckAccess(origin)); if (!inRange && popup && _gameTiming.IsFirstTimePredicted) @@ -961,11 +958,21 @@ public bool RangedInteractDoBefore( } /// <summary> - /// Uses a item/object on an entity + /// Uses an item/object on an entity /// Finds components with the InteractUsing interface and calls their function /// NOTE: Does not have an InRangeUnobstructed check /// </summary> - public void InteractUsing( + /// <param name="user">User doing the interaction.</param> + /// <param name="used">Item being used on the <paramref name="target"/>.</param> + /// <param name="target">Entity getting interacted with by the <paramref name="user"/> using the + /// <paramref name="used"/> entity.</param> + /// <param name="clickLocation">The location that the <paramref name="user"/> clicked.</param> + /// <param name="checkCanInteract">Whether to check that the <paramref name="user"/> can interact with the + /// <paramref name="target"/>.</param> + /// <param name="checkCanUse">Whether to check that the <paramref name="user"/> can use the + /// <paramref name="used"/> entity.</param> + /// <returns>True if the interaction was handled. Otherwise, false.</returns> + public bool InteractUsing( EntityUid user, EntityUid used, EntityUid target, @@ -974,58 +981,76 @@ public void InteractUsing( bool checkCanUse = true) { if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)) - return; + return false; if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target)) - return; + return false; + + if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target)) + return false; if (checkCanUse && !_actionBlockerSystem.CanUseHeldEntity(user, used)) - return; + return false; + + _adminLogger.Add( + LogType.InteractUsing, + LogImpact.Low, + $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}"); if (RangedInteractDoBefore(user, used, target, clickLocation, true)) - return; + return true; // all interactions should only happen when in range / unobstructed, so no range check is needed var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); RaiseLocalEvent(target, interactUsingEvent, true); DoContactInteraction(user, used, interactUsingEvent); DoContactInteraction(user, target, interactUsingEvent); - + // Contact interactions are currently only used for forensics, so we don't raise used -> target if (interactUsingEvent.Handled) - return; + return true; - InteractDoAfter(user, used, target, clickLocation, canReach: true); + if (InteractDoAfter(user, used, target, clickLocation, canReach: true)) + return true; + return false; } /// <summary> /// Used when clicking on an entity resulted in no other interaction. Used for low-priority interactions. /// </summary> - public void InteractDoAfter( + /// <param name="user"><inheritdoc cref="InteractUsing"/></param> + /// <param name="used"><inheritdoc cref="InteractUsing"/></param> + /// <param name="target"><inheritdoc cref="InteractUsing"/></param> + /// <param name="clickLocation"><inheritdoc cref="InteractUsing"/></param> + /// <param name="canReach">Whether the <paramref name="user"/> is in range of the <paramref name="target"/>.</param> + /// <param name="checkDeletion">Whether we should check if any entities were deleted.</param> + /// <returns>True if the interaction was handled. Otherwise, false.</returns> + public bool InteractDoAfter( EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach, - bool checkDeletion = false - ) + bool checkDeletion = false) { - if (target is {Valid: false}) + if (target is { Valid: false }) target = null; if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))) - return; + return false; var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach); RaiseLocalEvent(used, afterInteractEvent); DoContactInteraction(user, used, afterInteractEvent); + if (canReach) DoContactInteraction(user, target, afterInteractEvent); + // Contact interactions are currently only used for forensics, so we don't raise used -> target if (afterInteractEvent.Handled) - return; + return true; if (target == null) - return; + return false; var afterInteractUsingEvent = new AfterInteractUsingEvent(user, used, target, clickLocation, canReach); RaiseLocalEvent(target.Value, afterInteractUsingEvent); @@ -1033,6 +1058,11 @@ public void InteractDoAfter( DoContactInteraction(user, used, afterInteractUsingEvent); if (canReach) DoContactInteraction(user, target, afterInteractUsingEvent); + // Contact interactions are currently only used for forensics, so we don't raise used -> target + + if (afterInteractUsingEvent.Handled) + return true; + return false; } #region ActivateItemInWorld @@ -1227,7 +1257,7 @@ public bool IsAccessible(Entity<TransformComponent?> user, Entity<TransformCompo /// </summary> public bool CanAccessViaStorage(EntityUid user, EntityUid target) { - if (!_containerSystem.TryGetContainingContainer(target, out var container)) + if (!_containerSystem.TryGetContainingContainer((target, null, null), out var container)) return false; return CanAccessViaStorage(user, target, container); @@ -1253,7 +1283,7 @@ public bool CanAccessEquipment(EntityUid user, EntityUid target) if (Deleted(target)) return false; - if (!_containerSystem.TryGetContainingContainer(target, out var container)) + if (!_containerSystem.TryGetContainingContainer((target, null, null), out var container)) return false; var wearer = container.Owner; @@ -1263,7 +1293,7 @@ public bool CanAccessEquipment(EntityUid user, EntityUid target) if (wearer == user) return true; - if (slotDef.StripHidden) + if (_strippable.IsStripHidden(slotDef, user)) return false; return InRangeUnobstructed(user, wearer) && _containerSystem.IsInSameOrParentContainer(user, wearer); diff --git a/Content.Shared/InteractionVerbs/InteractionAction.cs b/Content.Shared/InteractionVerbs/InteractionAction.cs index 6c74b304c9f..0ffbc42b2b0 100644 --- a/Content.Shared/InteractionVerbs/InteractionAction.cs +++ b/Content.Shared/InteractionVerbs/InteractionAction.cs @@ -1,3 +1,4 @@ +using Content.Shared.Whitelist; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization; @@ -55,5 +56,6 @@ public sealed class VerbDependencies [Dependency] public readonly IRobustRandom Random = default!; [Dependency] public readonly IGameTiming Timing = default!; [Dependency] public readonly ISerializationManager Serialization = default!; + [Dependency] public readonly EntityWhitelistSystem WhitelistSystem = default!; } } diff --git a/Content.Shared/InteractionVerbs/InteractionVerbPrototype.cs b/Content.Shared/InteractionVerbs/InteractionVerbPrototype.cs index 671e8e47184..b656428f405 100644 --- a/Content.Shared/InteractionVerbs/InteractionVerbPrototype.cs +++ b/Content.Shared/InteractionVerbs/InteractionVerbPrototype.cs @@ -108,13 +108,12 @@ public sealed partial class InteractionVerbPrototype : IPrototype, IInheritingPr /// The user, target, needHand, event, and other required parameters are set up automatically when the do-after is created. /// </summary> [DataField] - public DoAfterArgs DoAfter = new DoAfterArgs() + public DoAfterArgs DoAfter = new() { User = EntityUid.Invalid, NetUser = NetEntity.Invalid, BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnWeightlessMove = true, RequireCanInteract = false, // Never used, but must be present because the field is non-nullable and will error during serialization if not set. diff --git a/Content.Shared/InteractionVerbs/Requirements/AssortedRequirements.cs b/Content.Shared/InteractionVerbs/Requirements/AssortedRequirements.cs index 9a4dd323360..1e7a09d2716 100644 --- a/Content.Shared/InteractionVerbs/Requirements/AssortedRequirements.cs +++ b/Content.Shared/InteractionVerbs/Requirements/AssortedRequirements.cs @@ -15,11 +15,9 @@ public sealed partial class EntityWhitelistRequirement : InteractionRequirement { [DataField] public EntityWhitelist? Whitelist, Blacklist; - public override bool IsMet(InteractionArgs args, InteractionVerbPrototype proto, InteractionAction.VerbDependencies deps) - { - return Whitelist?.IsValid(args.Target, deps.EntMan) != false - && Blacklist?.IsValid(args.Target, deps.EntMan) != true; - } + public override bool IsMet(InteractionArgs args, InteractionVerbPrototype proto, InteractionAction.VerbDependencies deps) => + !deps.WhitelistSystem.IsWhitelistFail(Whitelist, args.Target) + && !deps.WhitelistSystem.IsBlacklistPass(Blacklist, args.Target); } /// <summary> diff --git a/Content.Shared/InteractionVerbs/SharedInteractionVerbsSystem.cs b/Content.Shared/InteractionVerbs/SharedInteractionVerbsSystem.cs index f8100e71c39..76106dc92a7 100644 --- a/Content.Shared/InteractionVerbs/SharedInteractionVerbsSystem.cs +++ b/Content.Shared/InteractionVerbs/SharedInteractionVerbsSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.InteractionVerbs.Events; using Content.Shared.Popups; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Network; @@ -36,6 +37,7 @@ public abstract class SharedInteractionVerbsSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popups = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { diff --git a/Content.Shared/Inventory/Events/UnequipAttemptEvent.cs b/Content.Shared/Inventory/Events/UnequipAttemptEvent.cs index d8d0a2a23b2..b647ee8e92b 100644 --- a/Content.Shared/Inventory/Events/UnequipAttemptEvent.cs +++ b/Content.Shared/Inventory/Events/UnequipAttemptEvent.cs @@ -17,6 +17,11 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs /// </summary> public readonly EntityUid Equipment; + /// <summary> + /// The slotFlags of the slot this item is being removed from. + /// </summary> + public readonly SlotFlags SlotFlags; + /// <summary> /// The slot the entity is being unequipped from. /// </summary> @@ -33,6 +38,7 @@ public UnequipAttemptEventBase(EntityUid unequipee, EntityUid unEquipTarget, Ent UnEquipTarget = unEquipTarget; Equipment = equipment; Unequipee = unequipee; + SlotFlags = slotDefinition.SlotFlags; Slot = slotDefinition.Name; } } diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 12435eba890..f4567eeae18 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -10,7 +10,9 @@ using Content.Shared.Item; using Content.Shared.Movement.Systems; using Content.Shared.Popups; +using Content.Shared.Strip; using Content.Shared.Strip.Components; +using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Timing; @@ -30,6 +32,8 @@ public abstract partial class InventorySystem [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly SharedStrippableSystem _strippable = default!; [ValidatePrototypeId<ItemSizePrototype>] private const string PocketableItemSize = "Small"; @@ -169,8 +173,7 @@ public bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, strin { BlockDuplicate = true, BreakOnHandChange = true, - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, CancelDuplicate = true, RequireCanInteract = true, NeedHand = true @@ -268,13 +271,8 @@ public bool CanEquip(EntityUid actor, EntityUid target, EntityUid itemUid, strin return false; } - if (slotDefinition.Whitelist != null && !slotDefinition.Whitelist.IsValid(itemUid)) - { - reason = "inventory-component-can-equip-does-not-fit"; - return false; - } - - if (slotDefinition.Blacklist != null && slotDefinition.Blacklist.IsValid(itemUid)) + if (_whitelistSystem.IsWhitelistFail(slotDefinition.Whitelist, itemUid) || + _whitelistSystem.IsBlacklistPass(slotDefinition.Blacklist, itemUid)) { reason = "inventory-component-can-equip-does-not-fit"; return false; @@ -418,8 +416,7 @@ public bool TryUnequip( { BlockDuplicate = true, BreakOnHandChange = true, - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, CancelDuplicate = true, RequireCanInteract = true, NeedHand = true diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 44892a617e0..9d263a60989 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -1,18 +1,24 @@ +using Content.Shared.Chat; using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Climbing.Events; using Content.Shared.Damage; using Content.Shared.Damage.Events; using Content.Shared.Electrocution; using Content.Shared.Explosion; using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Gravity; using Content.Shared.IdentityManagement.Components; using Content.Shared.Inventory.Events; using Content.Shared.Movement.Systems; +using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Overlays; using Content.Shared.Radio; using Content.Shared.Slippery; using Content.Shared.Strip.Components; using Content.Shared.Temperature; using Content.Shared.Verbs; +using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Chat; namespace Content.Shared.Inventory; @@ -30,10 +36,16 @@ public void InitializeRelay() SubscribeLocalEvent<InventoryComponent, SeeIdentityAttemptEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent); - SubscribeLocalEvent<InventoryComponent, TransformSpeakerSpeechEvent>(RelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, SelfBeforeHyposprayInjectsEvent>(RelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, TargetBeforeHyposprayInjectsEvent>(RelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, SelfBeforeGunShotEvent>(RelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, SelfBeforeClimbEvent>(RelayInventoryEvent); // by-ref events SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent); + SubscribeLocalEvent<InventoryComponent, IsWeightlessEvent>(RefRelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, ModifySlowOnDamageSpeedEvent>(RefRelayInventoryEvent); // Eye/vision events @@ -102,7 +114,7 @@ private void OnGetEquipmentVerbs(EntityUid uid, InventoryComponent component, Ge var enumerator = new InventorySlotEnumerator(component); while (enumerator.NextItem(out var item, out var slotDef)) { - if (!slotDef.StripHidden || args.User == uid) + if (!_strippable.IsStripHidden(slotDef, args.User) || args.User == uid) RaiseLocalEvent(item, ev); } } diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs index a4d77767e37..91accec8c93 100644 --- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs +++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Shared.Strip; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; @@ -39,6 +40,10 @@ public sealed partial class SlotDefinition [DataField("displayName", required: true)] public string DisplayName { get; private set; } = string.Empty; + /// <summary> + /// Whether or not this slot will have its item hidden in the strip menu, and block interactions. + /// <seealso cref="SharedStrippableSystem.IsStripHidden"/> + /// </summary> [DataField("stripHidden")] public bool StripHidden { get; private set; } /// <summary> diff --git a/Content.Shared/Inventory/SelfEquipOnlyComponent.cs b/Content.Shared/Inventory/SelfEquipOnlyComponent.cs new file mode 100644 index 00000000000..ee1980ef8ad --- /dev/null +++ b/Content.Shared/Inventory/SelfEquipOnlyComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Inventory; + +/// <summary> +/// This is used for an item that can only be equipped/unequipped by the user. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(SelfEquipOnlySystem))] +public sealed partial class SelfEquipOnlyComponent : Component +{ + /// <summary> + /// Whether or not the self-equip only condition requires the person to be conscious. + /// </summary> + [DataField] + public bool UnequipRequireConscious = true; +} diff --git a/Content.Shared/Inventory/SelfEquipOnlySystem.cs b/Content.Shared/Inventory/SelfEquipOnlySystem.cs new file mode 100644 index 00000000000..2bd113e22b1 --- /dev/null +++ b/Content.Shared/Inventory/SelfEquipOnlySystem.cs @@ -0,0 +1,45 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory.Events; + +namespace Content.Shared.Inventory; + +public sealed class SelfEquipOnlySystem : EntitySystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + + /// <inheritdoc/> + public override void Initialize() + { + SubscribeLocalEvent<SelfEquipOnlyComponent, BeingEquippedAttemptEvent>(OnBeingEquipped); + SubscribeLocalEvent<SelfEquipOnlyComponent, BeingUnequippedAttemptEvent>(OnBeingUnequipped); + } + + private void OnBeingEquipped(Entity<SelfEquipOnlyComponent> ent, ref BeingEquippedAttemptEvent args) + { + if (args.Cancelled) + return; + + if (TryComp<ClothingComponent>(ent, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE) + return; + + if (args.Equipee != args.EquipTarget) + args.Cancel(); + } + + private void OnBeingUnequipped(Entity<SelfEquipOnlyComponent> ent, ref BeingUnequippedAttemptEvent args) + { + if (args.Cancelled) + return; + + if (args.Unequipee == args.UnEquipTarget) + return; + + if (TryComp<ClothingComponent>(ent, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE) + return; + + if (ent.Comp.UnequipRequireConscious && !_actionBlocker.CanConsciouslyPerformAction(args.UnEquipTarget)) + return; + args.Cancel(); + } +} diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs index c20cdbc111e..873401eef66 100644 --- a/Content.Shared/IoC/SharedContentIoC.cs +++ b/Content.Shared/IoC/SharedContentIoC.cs @@ -1,14 +1,17 @@ using Content.Shared.Humanoid.Markings; using Content.Shared.Localizations; +using Content.Shared.Tag; +using Content.Shared.Whitelist; -namespace Content.Shared.IoC +namespace Content.Shared.IoC; + +public static class SharedContentIoC { - public static class SharedContentIoC + public static void Register() { - public static void Register() - { - IoCManager.Register<MarkingManager, MarkingManager>(); - IoCManager.Register<ContentLocalizationManager, ContentLocalizationManager>(); - } + IoCManager.Register<MarkingManager, MarkingManager>(); + IoCManager.Register<ContentLocalizationManager, ContentLocalizationManager>(); + IoCManager.Register<TagSystem>(); + IoCManager.Register<EntityWhitelistSystem>(); } } diff --git a/Content.Shared/Item/ItemToggle/ComponentTogglerSystem.cs b/Content.Shared/Item/ItemToggle/ComponentTogglerSystem.cs new file mode 100644 index 00000000000..760cefe27d4 --- /dev/null +++ b/Content.Shared/Item/ItemToggle/ComponentTogglerSystem.cs @@ -0,0 +1,26 @@ +using Content.Shared.Item.ItemToggle.Components; + +namespace Content.Shared.Item.ItemToggle; + +/// <summary> +/// Handles <see cref="ComponentTogglerComponent"/> component manipulation. +/// </summary> +public sealed class ComponentTogglerSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ComponentTogglerComponent, ItemToggledEvent>(OnToggled); + } + + private void OnToggled(Entity<ComponentTogglerComponent> ent, ref ItemToggledEvent args) + { + var target = ent.Comp.Parent ? Transform(ent).ParentUid : ent.Owner; + + if (args.Activated) + EntityManager.AddComponents(target, ent.Comp.Components); + else + EntityManager.RemoveComponents(target, ent.Comp.RemoveComponents ?? ent.Comp.Components); + } +} diff --git a/Content.Shared/Item/ItemToggle/Components/ComponentTogglerComponent.cs b/Content.Shared/Item/ItemToggle/Components/ComponentTogglerComponent.cs new file mode 100644 index 00000000000..20ef0a02315 --- /dev/null +++ b/Content.Shared/Item/ItemToggle/Components/ComponentTogglerComponent.cs @@ -0,0 +1,32 @@ +using Content.Shared.Item.ItemToggle; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Item.ItemToggle.Components; + +/// <summary> +/// Adds or removes components when toggled. +/// Requires <see cref="ItemToggleComponent"/>. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(ComponentTogglerSystem))] +public sealed partial class ComponentTogglerComponent : Component +{ + /// <summary> + /// The components to add when activated. + /// </summary> + [DataField(required: true)] + public ComponentRegistry Components = new(); + + /// <summary> + /// The components to remove when deactivated. + /// If this is null <see cref="Components"/> is reused. + /// </summary> + [DataField] + public ComponentRegistry? RemoveComponents; + + /// <summary> + /// If true, adds components on the entity's parent instead of the entity itself. + /// </summary> + [DataField] + public bool Parent; +} diff --git a/Content.Shared/Item/ItemToggle/Components/ItemToggleActiveSoundComponent.cs b/Content.Shared/Item/ItemToggle/Components/ItemToggleActiveSoundComponent.cs index 6d534713578..cdac49ae6d6 100644 --- a/Content.Shared/Item/ItemToggle/Components/ItemToggleActiveSoundComponent.cs +++ b/Content.Shared/Item/ItemToggle/Components/ItemToggleActiveSoundComponent.cs @@ -12,12 +12,12 @@ public sealed partial class ItemToggleActiveSoundComponent : Component /// <summary> /// The continuous noise this item makes when it's activated (like an e-sword's hum). /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] + [DataField(required: true), AutoNetworkedField] public SoundSpecifier? ActiveSound; /// <summary> /// Used when the item emits sound while active. /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField] + [DataField] public EntityUid? PlayingStream; } diff --git a/Content.Shared/Item/ItemToggle/Components/ItemToggleComponent.cs b/Content.Shared/Item/ItemToggle/Components/ItemToggleComponent.cs index 620ddfd1942..46249fdd0de 100644 --- a/Content.Shared/Item/ItemToggle/Components/ItemToggleComponent.cs +++ b/Content.Shared/Item/ItemToggle/Components/ItemToggleComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared.Item.ItemToggle.Components; /// </summary> /// <remarks> /// If you need extended functionality (e.g. requiring power) then add a new component and use events: -/// ItemToggleActivateAttemptEvent, ItemToggleDeactivateAttemptEvent or ItemToggleForceToggleEvent. +/// ItemToggleActivateAttemptEvent, ItemToggleDeactivateAttemptEvent, ItemToggledEvent. /// </remarks> [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ItemToggleComponent : Component @@ -19,6 +19,13 @@ public sealed partial class ItemToggleComponent : Component [DataField, AutoNetworkedField] public bool Activated = false; + /// <summary> + /// If this is set to false then the item can't be toggled by pressing Z. + /// Use another system to do it then. + /// </summary> + [DataField] + public bool OnUse = true; + /// <summary> /// Whether the item's toggle can be predicted by the client. /// </summary> diff --git a/Content.Shared/Item/ItemToggle/Components/ToggleVerbComponent.cs b/Content.Shared/Item/ItemToggle/Components/ToggleVerbComponent.cs new file mode 100644 index 00000000000..b673c55e0f1 --- /dev/null +++ b/Content.Shared/Item/ItemToggle/Components/ToggleVerbComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Item.ItemToggle; +using Robust.Shared.GameStates; + +namespace Content.Shared.Item.ItemToggle.Components; + +/// <summary> +/// Adds a verb for toggling something, requires <see cref="ItemToggleComponent"/>. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(ToggleVerbSystem))] +public sealed partial class ToggleVerbComponent : Component +{ + /// <summary> + /// Text the verb will have. + /// Gets passed "entity" as the entity's identity string. + /// </summary> + [DataField(required: true)] + public LocId Text = string.Empty; +} diff --git a/Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs b/Content.Shared/Item/ItemToggle/ItemToggleSystem.cs similarity index 67% rename from Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs rename to Content.Shared/Item/ItemToggle/ItemToggleSystem.cs index d07fd5a735e..c4e4150659b 100644 --- a/Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs +++ b/Content.Shared/Item/ItemToggle/ItemToggleSystem.cs @@ -18,12 +18,12 @@ namespace Content.Shared.Item.ItemToggle; /// <remarks> /// If you need extended functionality (e.g. requiring power) then add a new component and use events. /// </remarks> -public abstract class SharedItemToggleSystem : EntitySystem +public sealed class ItemToggleSystem : EntitySystem { - [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPointLightSystem _light = default!; - [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; public override void Initialize() @@ -31,8 +31,9 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<ItemToggleComponent, ComponentStartup>(OnStartup); - SubscribeLocalEvent<ItemToggleComponent, ItemUnwieldedEvent>(TurnOffonUnwielded); - SubscribeLocalEvent<ItemToggleComponent, ItemWieldedEvent>(TurnOnonWielded); + SubscribeLocalEvent<ItemToggleComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<ItemToggleComponent, ItemUnwieldedEvent>(TurnOffOnUnwielded); + SubscribeLocalEvent<ItemToggleComponent, ItemWieldedEvent>(TurnOnOnWielded); SubscribeLocalEvent<ItemToggleComponent, UseInHandEvent>(OnUseInHand); SubscribeLocalEvent<ItemToggleHotComponent, IsHotEvent>(OnIsHotEvent); @@ -47,57 +48,76 @@ private void OnStartup(Entity<ItemToggleComponent> ent, ref ComponentStartup arg UpdateVisuals(ent); } - private void OnUseInHand(EntityUid uid, ItemToggleComponent itemToggle, UseInHandEvent args) + private void OnMapInit(Entity<ItemToggleComponent> ent, ref MapInitEvent args) + { + if (!ent.Comp.Activated) + return; + + var ev = new ItemToggledEvent(Predicted: ent.Comp.Predictable, Activated: ent.Comp.Activated, User: null); + RaiseLocalEvent(ent, ref ev); + } + + private void OnUseInHand(Entity<ItemToggleComponent> ent, ref UseInHandEvent args) { - if (args.Handled) + if (args.Handled || !ent.Comp.OnUse) return; args.Handled = true; - Toggle(uid, args.User, predicted: itemToggle.Predictable, itemToggle: itemToggle); + Toggle((ent, ent.Comp), args.User, predicted: ent.Comp.Predictable); } /// <summary> /// Used when an item is attempted to be toggled. + /// Sets its state to the opposite of what it is. /// </summary> - public void Toggle(EntityUid uid, EntityUid? user = null, bool predicted = true, ItemToggleComponent? itemToggle = null) + /// <returns>Same as <see cref="TrySetActive"/></returns> + public bool Toggle(Entity<ItemToggleComponent?> ent, EntityUid? user = null, bool predicted = true) { - if (!Resolve(uid, ref itemToggle)) - return; + if (!Resolve(ent, ref ent.Comp)) + return false; - if (itemToggle.Activated) - { - TryDeactivate(uid, user, itemToggle: itemToggle, predicted: predicted); - } + return TrySetActive(ent, !ent.Comp.Activated, user, predicted); + } + + /// <summary> + /// Tries to set the activated bool from a value. + /// </summary> + /// <returns>false if the attempt fails for any reason</returns> + public bool TrySetActive(Entity<ItemToggleComponent?> ent, bool active, EntityUid? user = null, bool predicted = true) + { + if (active) + return TryActivate(ent, user, predicted: predicted); else - { - TryActivate(uid, user, itemToggle: itemToggle, predicted: predicted); - } + return TryDeactivate(ent, user, predicted: predicted); } /// <summary> /// Used when an item is attempting to be activated. It returns false if the attempt fails any reason, interrupting the activation. /// </summary> - public bool TryActivate(EntityUid uid, EntityUid? user = null, bool predicted = true, ItemToggleComponent? itemToggle = null) + public bool TryActivate(Entity<ItemToggleComponent?> ent, EntityUid? user = null, bool predicted = true) { - if (!Resolve(uid, ref itemToggle)) + if (!Resolve(ent, ref ent.Comp)) return false; - if (itemToggle.Activated) + var uid = ent.Owner; + var comp = ent.Comp; + if (comp.Activated) return true; - if (!itemToggle.Predictable && _netManager.IsClient) + if (!comp.Predictable && _netManager.IsClient) return true; var attempt = new ItemToggleActivateAttemptEvent(user); RaiseLocalEvent(uid, ref attempt); + if (!comp.Predictable) predicted = false; if (attempt.Cancelled) { if (predicted) - _audio.PlayPredicted(itemToggle.SoundFailToActivate, uid, user); + _audio.PlayPredicted(comp.SoundFailToActivate, uid, user); else - _audio.PlayPvs(itemToggle.SoundFailToActivate, uid); + _audio.PlayPvs(comp.SoundFailToActivate, uid); if (attempt.Popup != null && user != null) { @@ -110,7 +130,7 @@ public bool TryActivate(EntityUid uid, EntityUid? user = null, bool predicted = return false; } - Activate(uid, itemToggle, predicted, user); + Activate((uid, comp), predicted, user); return true; } @@ -118,75 +138,65 @@ public bool TryActivate(EntityUid uid, EntityUid? user = null, bool predicted = /// <summary> /// Used when an item is attempting to be deactivated. It returns false if the attempt fails any reason, interrupting the deactivation. /// </summary> - public bool TryDeactivate(EntityUid uid, EntityUid? user = null, bool predicted = true, ItemToggleComponent? itemToggle = null) + public bool TryDeactivate(Entity<ItemToggleComponent?> ent, EntityUid? user = null, bool predicted = true) { - if (!Resolve(uid, ref itemToggle)) + if (!Resolve(ent, ref ent.Comp)) return false; - if (!itemToggle.Predictable && _netManager.IsClient) + var uid = ent.Owner; + var comp = ent.Comp; + if (!comp.Activated) return true; - if (!itemToggle.Activated) + if (!comp.Predictable && _netManager.IsClient) return true; var attempt = new ItemToggleDeactivateAttemptEvent(user); RaiseLocalEvent(uid, ref attempt); if (attempt.Cancelled) - { return false; - } - Deactivate(uid, itemToggle, predicted, user); + if (!comp.Predictable) predicted = false; + Deactivate((uid, comp), predicted, user); return true; } - private void Activate(EntityUid uid, ItemToggleComponent itemToggle, bool predicted, EntityUid? user = null) + private void Activate(Entity<ItemToggleComponent> ent, bool predicted, EntityUid? user = null) { - // TODO: Fix this hardcoding - TryComp(uid, out AppearanceComponent? appearance); - _appearance.SetData(uid, ToggleableLightVisuals.Enabled, true, appearance); - _appearance.SetData(uid, ToggleVisuals.Toggled, true, appearance); - - if (_light.TryGetLight(uid, out var light)) - { - _light.SetEnabled(uid, true, light); - } - - var soundToPlay = itemToggle.SoundActivate; + var (uid, comp) = ent; + var soundToPlay = comp.SoundActivate; if (predicted) _audio.PlayPredicted(soundToPlay, uid, user); else _audio.PlayPvs(soundToPlay, uid); - // END FIX HARDCODING + comp.Activated = true; + UpdateVisuals((uid, comp)); + Dirty(uid, comp); var toggleUsed = new ItemToggledEvent(predicted, Activated: true, user); RaiseLocalEvent(uid, ref toggleUsed); - - itemToggle.Activated = true; - UpdateVisuals((uid, itemToggle)); - Dirty(uid, itemToggle); } /// <summary> /// Used to make the actual changes to the item's components on deactivation. /// </summary> - private void Deactivate(EntityUid uid, ItemToggleComponent itemToggle, bool predicted, EntityUid? user = null) + private void Deactivate(Entity<ItemToggleComponent> ent, bool predicted, EntityUid? user = null) { - var soundToPlay = itemToggle.SoundDeactivate; + var (uid, comp) = ent; + var soundToPlay = comp.SoundDeactivate; if (predicted) _audio.PlayPredicted(soundToPlay, uid, user); else _audio.PlayPvs(soundToPlay, uid); - // END FIX HARDCODING + + comp.Activated = false; + UpdateVisuals((uid, comp)); + Dirty(uid, comp); var toggleUsed = new ItemToggledEvent(predicted, Activated: false, user); RaiseLocalEvent(uid, ref toggleUsed); - - itemToggle.Activated = false; - UpdateVisuals((uid, itemToggle)); - Dirty(uid, itemToggle); } private void UpdateVisuals(Entity<ItemToggleComponent> ent) @@ -209,68 +219,56 @@ private void UpdateVisuals(Entity<ItemToggleComponent> ent) /// <summary> /// Used for items that require to be wielded in both hands to activate. For instance the dual energy sword will turn off if not wielded. /// </summary> - private void TurnOffonUnwielded(EntityUid uid, ItemToggleComponent itemToggle, ItemUnwieldedEvent args) + private void TurnOffOnUnwielded(Entity<ItemToggleComponent> ent, ref ItemUnwieldedEvent args) { - if (itemToggle.Activated) - TryDeactivate(uid, args.User, itemToggle: itemToggle); + TryDeactivate((ent, ent.Comp), args.User); } /// <summary> /// Wieldable items will automatically turn on when wielded. /// </summary> - private void TurnOnonWielded(EntityUid uid, ItemToggleComponent itemToggle, ref ItemWieldedEvent args) + private void TurnOnOnWielded(Entity<ItemToggleComponent> ent, ref ItemWieldedEvent args) { - if (!itemToggle.Activated) - TryActivate(uid, itemToggle: itemToggle); + // FIXME: for some reason both client and server play sound + TryActivate((ent, ent.Comp)); } - public bool IsActivated(EntityUid uid, ItemToggleComponent? comp = null) + public bool IsActivated(Entity<ItemToggleComponent?> ent) { - if (!Resolve(uid, ref comp, false)) + if (!Resolve(ent, ref ent.Comp, false)) return true; // assume always activated if no component - return comp.Activated; + return ent.Comp.Activated; } /// <summary> /// Used to make the item hot when activated. /// </summary> - private void OnIsHotEvent(EntityUid uid, ItemToggleHotComponent itemToggleHot, IsHotEvent args) + private void OnIsHotEvent(Entity<ItemToggleHotComponent> ent, ref IsHotEvent args) { - args.IsHot |= IsActivated(uid); + args.IsHot |= IsActivated(ent.Owner); } /// <summary> /// Used to update the looping active sound linked to the entity. /// </summary> - private void UpdateActiveSound(EntityUid uid, ItemToggleActiveSoundComponent activeSound, ref ItemToggledEvent args) + private void UpdateActiveSound(Entity<ItemToggleActiveSoundComponent> ent, ref ItemToggledEvent args) { - if (args.Activated) + var (uid, comp) = ent; + if (!args.Activated) { - if (activeSound.ActiveSound != null && activeSound.PlayingStream == null) - { - if (args.Predicted) - { - var playingStream = _audio.PlayPredicted(activeSound.ActiveSound, uid, args.User, AudioParams.Default.WithLoop(true)); - - if (playingStream == null) - return; - - activeSound.PlayingStream = playingStream!.Value.Entity; - } else - { - var playingStream = _audio.PlayPvs(activeSound.ActiveSound, uid, AudioParams.Default.WithLoop(true)); - - if (playingStream == null) - return; - - activeSound.PlayingStream = playingStream!.Value.Entity; - } - } + comp.PlayingStream = _audio.Stop(comp.PlayingStream); + return; } - else + + if (comp.ActiveSound != null && comp.PlayingStream == null) { - activeSound.PlayingStream = _audio.Stop(activeSound.PlayingStream); + var loop = AudioParams.Default.WithLoop(true); + var stream = args.Predicted + ? _audio.PlayPredicted(comp.ActiveSound, uid, args.User, loop) + : _audio.PlayPvs(comp.ActiveSound, uid, loop); + if (stream?.Entity is {} entity) + comp.PlayingStream = entity; } } diff --git a/Content.Shared/Item/ItemToggle/ToggleVerbSystem.cs b/Content.Shared/Item/ItemToggle/ToggleVerbSystem.cs new file mode 100644 index 00000000000..858cd9bc111 --- /dev/null +++ b/Content.Shared/Item/ItemToggle/ToggleVerbSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.IdentityManagement; +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Verbs; + +namespace Content.Shared.Item.ItemToggle; + +/// <summary> +/// Adds a verb for toggling something with <see cref="ToggleVerbComponent"/>. +/// </summary> +public sealed class ToggleVerbSystem : EntitySystem +{ + [Dependency] private readonly ItemToggleSystem _toggle = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ToggleVerbComponent, GetVerbsEvent<ActivationVerb>>(OnGetVerbs); + } + + private void OnGetVerbs(Entity<ToggleVerbComponent> ent, ref GetVerbsEvent<ActivationVerb> args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + var name = Identity.Entity(ent, EntityManager); + var user = args.User; + args.Verbs.Add(new ActivationVerb() + { + Text = Loc.GetString(ent.Comp.Text, ("entity", name)), + Act = () => _toggle.Toggle(ent.Owner, user) + }); + } +} diff --git a/Content.Shared/Item/SharedItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs index 5eaa25f4847..a058cb86588 100644 --- a/Content.Shared/Item/SharedItemSystem.cs +++ b/Content.Shared/Item/SharedItemSystem.cs @@ -110,8 +110,8 @@ private void AddPickupVerb(EntityUid uid, ItemComponent component, GetVerbsEvent // if the item already in a container (that is not the same as the user's), then change the text. // this occurs when the item is in their inventory or in an open backpack - Container.TryGetContainingContainer(args.User, out var userContainer); - if (Container.TryGetContainingContainer(args.Target, out var container) && container != userContainer) + Container.TryGetContainingContainer((args.User, null, null), out var userContainer); + if (Container.TryGetContainingContainer((args.Target, null, null), out var container) && container != userContainer) verb.Text = Loc.GetString("pick-up-verb-get-data-text-inventory"); else verb.Text = Loc.GetString("pick-up-verb-get-data-text"); diff --git a/Content.Shared/Kitchen/Components/RecipeProviderComponent.cs b/Content.Shared/Kitchen/Components/RecipeProviderComponent.cs new file mode 100644 index 00000000000..d677e426743 --- /dev/null +++ b/Content.Shared/Kitchen/Components/RecipeProviderComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Kitchen.Components; + +[RegisterComponent] +public sealed partial class FoodRecipeProviderComponent : Component +{ + /// <summary> + /// These are additional recipes that the entity is capable of cooking. + /// </summary> + [DataField, ViewVariables] + public List<ProtoId<FoodRecipePrototype>> ProvidedRecipes = new(); +} diff --git a/Content.Shared/Kitchen/GetSecretRecipesEvent.cs b/Content.Shared/Kitchen/GetSecretRecipesEvent.cs new file mode 100644 index 00000000000..cf5416dfaa4 --- /dev/null +++ b/Content.Shared/Kitchen/GetSecretRecipesEvent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Kitchen; + +/// <summary> +/// This returns a list of recipes not found in the main list of available recipes. +/// </summary> +[ByRefEvent] +public struct GetSecretRecipesEvent() +{ + public List<FoodRecipePrototype> Recipes = new(); +} diff --git a/Content.Shared/Kitchen/MicrowaveMealRecipePrototype.cs b/Content.Shared/Kitchen/MicrowaveMealRecipePrototype.cs index b0991b54605..65a7b9ed045 100644 --- a/Content.Shared/Kitchen/MicrowaveMealRecipePrototype.cs +++ b/Content.Shared/Kitchen/MicrowaveMealRecipePrototype.cs @@ -37,6 +37,12 @@ public sealed partial class FoodRecipePrototype : IPrototype public IReadOnlyDictionary<string, FixedPoint2> IngredientsReagents => _ingsReagents; public IReadOnlyDictionary<string, FixedPoint2> IngredientsSolids => _ingsSolids; + /// <summary> + /// Is this recipe unavailable in normal circumstances? + /// </summary> + [DataField] + public bool SecretRecipe = false; + /// <summary> /// Count the number of ingredients in a recipe for sorting the recipe list. /// This makes sure that where ingredient lists overlap, the more complex diff --git a/Content.Shared/Kitchen/RecipeManager.cs b/Content.Shared/Kitchen/RecipeManager.cs index 0b120db26da..79c169b754b 100644 --- a/Content.Shared/Kitchen/RecipeManager.cs +++ b/Content.Shared/Kitchen/RecipeManager.cs @@ -14,7 +14,8 @@ public void Initialize() Recipes = new List<FoodRecipePrototype>(); foreach (var item in _prototypeManager.EnumeratePrototypes<FoodRecipePrototype>()) { - Recipes.Add(item); + if (!item.SecretRecipe) + Recipes.Add(item); } Recipes.Sort(new RecipeComparer()); diff --git a/Content.Shared/Labels/Components/LabelComponent.cs b/Content.Shared/Labels/Components/LabelComponent.cs index c0dccd34815..d57023c8ab1 100644 --- a/Content.Shared/Labels/Components/LabelComponent.cs +++ b/Content.Shared/Labels/Components/LabelComponent.cs @@ -14,11 +14,4 @@ public sealed partial class LabelComponent : Component /// </summary> [DataField, AutoNetworkedField] public string? CurrentLabel { get; set; } - - /// <summary> - /// The original name of the entity - /// Used for reverting the modified entity name when the label is removed - /// </summary> - [DataField, AutoNetworkedField] - public string? OriginalName { get; set; } } diff --git a/Content.Shared/Labels/EntitySystems/SharedHandLabelerSystem.cs b/Content.Shared/Labels/EntitySystems/SharedHandLabelerSystem.cs index 7dbeee3e770..0763bb101ca 100644 --- a/Content.Shared/Labels/EntitySystems/SharedHandLabelerSystem.cs +++ b/Content.Shared/Labels/EntitySystems/SharedHandLabelerSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Labels.Components; using Content.Shared.Popups; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Robust.Shared.GameStates; using Robust.Shared.Network; @@ -16,6 +17,7 @@ public abstract class SharedHandLabelerSystem : EntitySystem [Dependency] private readonly SharedLabelSystem _labelSystem = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -77,7 +79,7 @@ private void AddLabelTo(EntityUid uid, HandLabelerComponent? handLabeler, Entity private void OnUtilityVerb(EntityUid uid, HandLabelerComponent handLabeler, GetVerbsEvent<UtilityVerb> args) { - if (args.Target is not { Valid: true } target || !handLabeler.Whitelist.IsValid(target) || !args.CanAccess) + if (args.Target is not { Valid: true } target || _whitelistSystem.IsWhitelistFail(handLabeler.Whitelist, target) || !args.CanAccess) return; var labelerText = handLabeler.AssignedLabel == string.Empty ? Loc.GetString("hand-labeler-remove-label-text") : Loc.GetString("hand-labeler-add-label-text"); @@ -96,7 +98,7 @@ private void OnUtilityVerb(EntityUid uid, HandLabelerComponent handLabeler, GetV private void AfterInteractOn(EntityUid uid, HandLabelerComponent handLabeler, AfterInteractEvent args) { - if (args.Target is not { Valid: true } target || !handLabeler.Whitelist.IsValid(target) || !args.CanReach) + if (args.Target is not { Valid: true } target || _whitelistSystem.IsWhitelistFail(handLabeler.Whitelist, target) || !args.CanReach) return; Labeling(uid, target, args.User, handLabeler); diff --git a/Content.Shared/Labels/EntitySystems/SharedLabelSystem.cs b/Content.Shared/Labels/EntitySystems/SharedLabelSystem.cs index 1189bb46d04..f1998e524d9 100644 --- a/Content.Shared/Labels/EntitySystems/SharedLabelSystem.cs +++ b/Content.Shared/Labels/EntitySystems/SharedLabelSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Examine; using Content.Shared.Labels.Components; +using Content.Shared.NameModifier.EntitySystems; using Robust.Shared.Utility; namespace Content.Shared.Labels.EntitySystems; @@ -11,6 +12,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<LabelComponent, ExaminedEvent>(OnExamine); + SubscribeLocalEvent<LabelComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } public virtual void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null){} @@ -27,4 +29,10 @@ private void OnExamine(EntityUid uid, LabelComponent? label, ExaminedEvent args) message.AddText(Loc.GetString("hand-labeler-has-label", ("label", label.CurrentLabel))); args.PushMessage(message); } + + private void OnRefreshNameModifiers(Entity<LabelComponent> entity, ref RefreshNameModifiersEvent args) + { + if (!string.IsNullOrEmpty(entity.Comp.CurrentLabel)) + args.AddModifier("comp-label-format", extraArgs: ("label", entity.Comp.CurrentLabel)); + } } diff --git a/Content.Shared/Lathe/LatheComponent.cs b/Content.Shared/Lathe/LatheComponent.cs index d4bf18b0507..ab3d59b6423 100644 --- a/Content.Shared/Lathe/LatheComponent.cs +++ b/Content.Shared/Lathe/LatheComponent.cs @@ -33,6 +33,15 @@ public sealed partial class LatheComponent : Component [DataField] public SoundSpecifier? ProducingSound; + [DataField] + public string? ReagentOutputSlotId; + + /// <summary> + /// The default amount that's displayed in the UI for selecting the print amount. + /// </summary> + [DataField, AutoNetworkedField] + public int DefaultProductionAmount = 1; + #region Visualizer info [DataField] public string? IdleState; diff --git a/Content.Shared/Lathe/SharedLatheSystem.cs b/Content.Shared/Lathe/SharedLatheSystem.cs index b41a91f9c34..e240571f315 100644 --- a/Content.Shared/Lathe/SharedLatheSystem.cs +++ b/Content.Shared/Lathe/SharedLatheSystem.cs @@ -1,5 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Shared.Emag.Systems; +using Content.Shared.Examine; +using Content.Shared.Localizations; using Content.Shared.Materials; using Content.Shared.Research.Prototypes; using JetBrains.Annotations; @@ -23,10 +26,21 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<EmagLatheRecipesComponent, GotEmaggedEvent>(OnEmagged); + SubscribeLocalEvent<LatheComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded); + BuildInverseRecipeDictionary(); } + private void OnExamined(Entity<LatheComponent> ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (ent.Comp.ReagentOutputSlotId != null) + args.PushMarkup(Loc.GetString("lathe-menu-reagent-slot-examine")); + } + [PublicAPI] public bool CanProduce(EntityUid uid, string recipe, int amount = 1, LatheComponent? component = null) { @@ -40,7 +54,7 @@ public bool CanProduce(EntityUid uid, LatheRecipePrototype recipe, int amount = if (!HasRecipe(uid, recipe, component)) return false; - foreach (var (material, needed) in recipe.RequiredMaterials) + foreach (var (material, needed) in recipe.Materials) { var adjustedAmount = AdjustMaterial(needed, recipe.ApplyMaterialDiscount, component.MaterialUseMultiplier); @@ -72,6 +86,9 @@ private void BuildInverseRecipeDictionary() _inverseRecipeDictionary.Clear(); foreach (var latheRecipe in _proto.EnumeratePrototypes<LatheRecipePrototype>()) { + if (latheRecipe.Result == null) + continue; + _inverseRecipeDictionary.GetOrNew(latheRecipe.Result).Add(latheRecipe); } } @@ -83,4 +100,55 @@ public bool TryGetRecipesFromEntity(string prototype, [NotNullWhen(true)] out Li recipes.AddRange(r); return recipes.Count != 0; } + + public string GetRecipeName(ProtoId<LatheRecipePrototype> proto) + { + return GetRecipeName(_proto.Index(proto)); + } + + public string GetRecipeName(LatheRecipePrototype proto) + { + if (!string.IsNullOrWhiteSpace(proto.Name)) + return Loc.GetString(proto.Name); + + if (proto.Result is { } result) + { + return _proto.Index(result).Name; + } + + if (proto.ResultReagents is { } resultReagents) + { + return ContentLocalizationManager.FormatList(resultReagents + .Select(p => Loc.GetString("lathe-menu-result-reagent-display", ("reagent", _proto.Index(p.Key).LocalizedName), ("amount", p.Value))) + .ToList()); + } + + return string.Empty; + } + + [PublicAPI] + public string GetRecipeDescription(ProtoId<LatheRecipePrototype> proto) + { + return GetRecipeDescription(_proto.Index(proto)); + } + + public string GetRecipeDescription(LatheRecipePrototype proto) + { + if (!string.IsNullOrWhiteSpace(proto.Description)) + return Loc.GetString(proto.Description); + + if (proto.Result is { } result) + { + return _proto.Index(result).Description; + } + + if (proto.ResultReagents is { } resultReagents) + { + // We only use the first one for the description since these descriptions don't combine very well. + var reagent = resultReagents.First().Key; + return _proto.Index(reagent).LocalizedDescription; + } + + return string.Empty; + } } diff --git a/Content.Shared/Light/Components/SlimPoweredLightComponent.cs b/Content.Shared/Light/Components/SlimPoweredLightComponent.cs new file mode 100644 index 00000000000..bf6ae0e5251 --- /dev/null +++ b/Content.Shared/Light/Components/SlimPoweredLightComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Light.Components; + +// All content light code is terrible and everything is baked-in. Power code got predicted before light code did. +/// <summary> +/// Handles turning a pointlight on / off based on power. Nothing else +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SlimPoweredLightComponent : Component +{ + /// <summary> + /// Used to make this as being lit. If unpowered then the light will still be off. + /// </summary> + [DataField, AutoNetworkedField] + public bool Enabled = true; +} diff --git a/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs b/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs new file mode 100644 index 00000000000..6d984ed19a3 --- /dev/null +++ b/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs @@ -0,0 +1,67 @@ +using Content.Shared.Light.Components; +using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; + +namespace Content.Shared.Light.EntitySystems; + +public sealed class SlimPoweredLightSystem : EntitySystem +{ + [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!; + [Dependency] private readonly SharedPointLightSystem _lights = default!; + + private bool _setting; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<SlimPoweredLightComponent, AttemptPointLightToggleEvent>(OnLightAttempt); + SubscribeLocalEvent<SlimPoweredLightComponent, PowerChangedEvent>(OnLightPowerChanged); + } + + private void OnLightAttempt(Entity<SlimPoweredLightComponent> ent, ref AttemptPointLightToggleEvent args) + { + // Early-out to avoid having to trycomp stuff if we're the caller setting it + if (_setting) + return; + + if (args.Enabled && !_receiver.IsPowered(ent.Owner)) + args.Cancelled = true; + } + + private void OnLightPowerChanged(Entity<SlimPoweredLightComponent> ent, ref PowerChangedEvent args) + { + // Early out if we don't need to trycomp. + if (args.Powered) + { + if (!ent.Comp.Enabled) + return; + } + else + { + if (!ent.Comp.Enabled) + return; + } + + if (!_lights.TryGetLight(ent.Owner, out var light)) + return; + + var enabled = ent.Comp.Enabled && args.Powered; + _setting = true; + _lights.SetEnabled(ent.Owner, enabled, light); + _setting = false; + } + + public void SetEnabled(Entity<SlimPoweredLightComponent?> entity, bool enabled) + { + if (!Resolve(entity.Owner, ref entity.Comp, false)) + return; + + if (entity.Comp.Enabled == enabled) + return; + + entity.Comp.Enabled = enabled; + Dirty(entity); + _lights.SetEnabled(entity.Owner, enabled); + } +} diff --git a/Content.Shared/Light/EntitySystems/UnpoweredFlashlightSystem.cs b/Content.Shared/Light/EntitySystems/UnpoweredFlashlightSystem.cs index 42e55bea55d..8754de50583 100644 --- a/Content.Shared/Light/EntitySystems/UnpoweredFlashlightSystem.cs +++ b/Content.Shared/Light/EntitySystems/UnpoweredFlashlightSystem.cs @@ -13,6 +13,8 @@ namespace Content.Shared.Light.EntitySystems; public sealed class UnpoweredFlashlightSystem : EntitySystem { + // TODO: Split some of this to ItemTogglePointLight + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; diff --git a/Content.Shared/Localizations/ContentLocalizationManager.cs b/Content.Shared/Localizations/ContentLocalizationManager.cs index 7d40182f6cc..ad8890ae0fd 100644 --- a/Content.Shared/Localizations/ContentLocalizationManager.cs +++ b/Content.Shared/Localizations/ContentLocalizationManager.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using Robust.Shared.Utility; @@ -119,6 +119,20 @@ public static string FormatList(List<string> list) }; } + /// <summary> + /// Formats a list as per english grammar rules, but uses or instead of and. + /// </summary> + public static string FormatListToOr(List<string> list) + { + return list.Count switch + { + <= 0 => string.Empty, + 1 => list[0], + 2 => $"{list[0]} or {list[1]}", + _ => $"{string.Join(" or ", list)}" + }; + } + /// <summary> /// Formats a direction struct as a human-readable string. /// </summary> diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index bfdfc77bc21..9c1d1a1c789 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -121,7 +121,9 @@ public bool TryLock(EntityUid uid, EntityUid user, LockComponent? lockComp = nul return _doAfter.TryStartDoAfter( new DoAfterArgs(EntityManager, user, lockComp.LockTime, new LockDoAfter(), uid, uid) { - BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, RequireCanInteract = true, + BreakOnDamage = true, + BreakOnMove = true, + RequireCanInteract = true, NeedHand = true }); } @@ -197,7 +199,9 @@ public bool TryUnlock(EntityUid uid, EntityUid user, LockComponent? lockComp = n return _doAfter.TryStartDoAfter( new DoAfterArgs(EntityManager, user, lockComp.LockTime, new UnlockDoAfter(), uid, uid) { - BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, RequireCanInteract = true, + BreakOnDamage = true, + BreakOnMove = true, + RequireCanInteract = true, NeedHand = true }); } diff --git a/Content.Shared/Lube/LubedComponent.cs b/Content.Shared/Lube/LubedComponent.cs index fe1946ddb15..9d032a077ec 100644 --- a/Content.Shared/Lube/LubedComponent.cs +++ b/Content.Shared/Lube/LubedComponent.cs @@ -3,12 +3,6 @@ namespace Content.Shared.Lube; [RegisterComponent] public sealed partial class LubedComponent : Component { - /// <summary> - /// Reverts name to before prefix event (essentially removes prefix). - /// </summary> - [DataField("beforeLubedEntityName")] - public string BeforeLubedEntityName = string.Empty; - [DataField("slipsLeft"), ViewVariables(VVAccess.ReadWrite)] public int SlipsLeft; diff --git a/Content.Shared/Magic/Events/ChargeSpellEvent.cs b/Content.Shared/Magic/Events/ChargeSpellEvent.cs index 57e1eb880dd..47a5a5bd623 100644 --- a/Content.Shared/Magic/Events/ChargeSpellEvent.cs +++ b/Content.Shared/Magic/Events/ChargeSpellEvent.cs @@ -1,6 +1,7 @@ using Content.Shared.Actions; using Content.Shared.Chat; + namespace Content.Shared.Magic.Events; /// <summary> diff --git a/Content.Shared/Magic/Events/KnockSpellEvent.cs b/Content.Shared/Magic/Events/KnockSpellEvent.cs index 6775a679ab8..cb161b77eb0 100644 --- a/Content.Shared/Magic/Events/KnockSpellEvent.cs +++ b/Content.Shared/Magic/Events/KnockSpellEvent.cs @@ -1,6 +1,7 @@ using Content.Shared.Actions; using Content.Shared.Chat; + namespace Content.Shared.Magic.Events; public sealed partial class KnockSpellEvent : InstantActionEvent, ISpeakSpell diff --git a/Content.Shared/Magic/Events/ProjectileSpellEvent.cs b/Content.Shared/Magic/Events/ProjectileSpellEvent.cs index 439b09e7afb..cf338a6bb43 100644 --- a/Content.Shared/Magic/Events/ProjectileSpellEvent.cs +++ b/Content.Shared/Magic/Events/ProjectileSpellEvent.cs @@ -12,9 +12,6 @@ public sealed partial class ProjectileSpellEvent : WorldTargetActionEvent, ISpea [DataField(required: true)] public EntProtoId Prototype; - [DataField] - public float ProjectileSpeed = 20; - [DataField] public string? Speech { get; private set; } diff --git a/Content.Shared/Magic/Events/SpeakSpellEvent.cs b/Content.Shared/Magic/Events/SpeakSpellEvent.cs index d04daf139d8..023fda0efc4 100644 --- a/Content.Shared/Magic/Events/SpeakSpellEvent.cs +++ b/Content.Shared/Magic/Events/SpeakSpellEvent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chat; +using Content.Shared.Chat; namespace Content.Shared.Magic.Events; diff --git a/Content.Shared/Magic/Events/TeleportSpellEvent.cs b/Content.Shared/Magic/Events/TeleportSpellEvent.cs index 2f07cab2016..ff29b213346 100644 --- a/Content.Shared/Magic/Events/TeleportSpellEvent.cs +++ b/Content.Shared/Magic/Events/TeleportSpellEvent.cs @@ -1,6 +1,7 @@ using Content.Shared.Actions; using Content.Shared.Chat; + namespace Content.Shared.Magic.Events; // TODO: Can probably just be an entity or something @@ -9,8 +10,6 @@ public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakS [DataField] public string? Speech { get; private set; } - public InGameICChatType ChatType { get; } = InGameICChatType.Speak; - // TODO: Move to magic component // TODO: Maybe not since sound specifier is a thing // Keep here to remind what the volume was set as @@ -19,4 +18,6 @@ public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakS /// </summary> [DataField] public float BlinkVolume = 5f; + + public InGameICChatType ChatType { get; } = InGameICChatType.Speak; } diff --git a/Content.Shared/Magic/SharedMagicSystem.cs b/Content.Shared/Magic/SharedMagicSystem.cs index cae581298a6..b0a9fef75d0 100644 --- a/Content.Shared/Magic/SharedMagicSystem.cs +++ b/Content.Shared/Magic/SharedMagicSystem.cs @@ -344,7 +344,7 @@ private void OnProjectileSpell(ProjectileSpellEvent ev) var ent = Spawn(ev.Prototype, spawnCoords); var direction = toCoords.ToMapPos(EntityManager, _transform) - spawnCoords.ToMapPos(EntityManager, _transform); - _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer, ev.ProjectileSpeed); + _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer); } // End Projectile Spells #endregion diff --git a/Content.Shared/Magic/SpellbookSystem.cs b/Content.Shared/Magic/SpellbookSystem.cs index a7c82746249..84b2b232980 100644 --- a/Content.Shared/Magic/SpellbookSystem.cs +++ b/Content.Shared/Magic/SpellbookSystem.cs @@ -86,11 +86,9 @@ private void AttemptLearn(Entity<SpellbookComponent> ent, UseInHandEvent args) { var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, ent.Comp.LearnTime, new SpellbookDoAfterEvent(), ent, target: ent) { - BreakOnTargetMove = true, - BreakOnUserMove = true, - BreakOnWeightlessMove = true, + BreakOnMove = true, BreakOnDamage = true, - NeedHand = true, // What, are you going to read with your eyes only?? + NeedHand = true //What, are you going to read with your eyes only?? }; _doAfter.TryStartDoAfter(doAfterEventArgs); diff --git a/Content.Shared/Materials/SharedMaterialReclaimerSystem.cs b/Content.Shared/Materials/SharedMaterialReclaimerSystem.cs index cd4ae2e7676..b518bc875fd 100644 --- a/Content.Shared/Materials/SharedMaterialReclaimerSystem.cs +++ b/Content.Shared/Materials/SharedMaterialReclaimerSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Examine; using Content.Shared.Mobs.Components; using Content.Shared.Stacks; +using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; @@ -32,6 +33,7 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] protected readonly SharedContainerSystem Container = default!; [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public const string ActiveReclaimerContainerId = "active-material-reclaimer-container"; @@ -94,13 +96,11 @@ public bool TryStartProcessItem(EntityUid uid, EntityUid item, MaterialReclaimer if (HasComp<MobStateComponent>(item) && !CanGib(uid, item, component)) // whitelist? We be gibbing, boy! return false; - if (component.Whitelist is {} whitelist && !whitelist.IsValid(item)) + if (_whitelistSystem.IsWhitelistFail(component.Whitelist, item) || + _whitelistSystem.IsBlacklistPass(component.Blacklist, item)) return false; - if (component.Blacklist is {} blacklist && blacklist.IsValid(item)) - return false; - - if (Container.TryGetContainingContainer(item, out _) && !Container.TryRemoveFromContainer(item)) + if (Container.TryGetContainingContainer((item, null, null), out _) && !Container.TryRemoveFromContainer(item)) return false; if (user != null) diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs index 04926c34562..2ec48085c47 100644 --- a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs +++ b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Weapons.Melee; +using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -37,6 +38,7 @@ public abstract class SharedMechSystem : EntitySystem [Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; /// <inheritdoc/> public override void Initialize() @@ -216,7 +218,7 @@ public void InsertEquipment(EntityUid uid, EntityUid toInsert, MechComponent? co if (component.EquipmentContainer.ContainedEntities.Count >= component.MaxEquipmentAmount) return; - if (component.EquipmentWhitelist != null && !component.EquipmentWhitelist.IsValid(toInsert)) + if (_whitelistSystem.IsWhitelistFail(component.EquipmentWhitelist, toInsert)) return; equipmentComponent.EquipmentOwner = uid; @@ -434,7 +436,7 @@ private void OnDragDrop(EntityUid uid, MechComponent component, ref DragDropTarg var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Dragged, component.EntryDelay, new MechEntryEvent(), uid, target: uid) { - BreakOnUserMove = true, + BreakOnMove = true, }; _doAfter.TryStartDoAfter(doAfterEventArgs); diff --git a/Content.Shared/Medical/CPR/Systems/CPRSystem.cs b/Content.Shared/Medical/CPR/Systems/CPRSystem.cs index e050f1b4e1d..d82a7ee202b 100644 --- a/Content.Shared/Medical/CPR/Systems/CPRSystem.cs +++ b/Content.Shared/Medical/CPR/Systems/CPRSystem.cs @@ -95,8 +95,7 @@ private void StartCPR(EntityUid performer, EntityUid target, CPRTrainingComponen _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, performer, cprComponent.DoAfterDuration, new CPRDoAfterEvent(), performer, target, performer) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true, BlockDuplicate = true }); diff --git a/Content.Shared/Medical/DefibrillatorComponent.cs b/Content.Shared/Medical/DefibrillatorComponent.cs index 2da52852854..e4cd8077d26 100644 --- a/Content.Shared/Medical/DefibrillatorComponent.cs +++ b/Content.Shared/Medical/DefibrillatorComponent.cs @@ -10,16 +10,11 @@ namespace Content.Shared.Medical; /// <summary> /// This is used for defibrillators; a machine that shocks a dead /// person back into the world of the living. +/// Uses <c>ItemToggleComponent</c> /// </summary> [RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] public sealed partial class DefibrillatorComponent : Component { - /// <summary> - /// Whether or not it's turned on and able to be used. - /// </summary> - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] - public bool Enabled; - /// <summary> /// The time at which the zap cooldown will be completed /// </summary> @@ -60,21 +55,18 @@ public sealed partial class DefibrillatorComponent : Component [DataField("doAfterDuration"), ViewVariables(VVAccess.ReadWrite)] public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3); + [DataField] + public bool AllowDoAfterMovement = true; + + [DataField] + public bool CanDefibCrit = true; + /// <summary> /// The sound when someone is zapped. /// </summary> [ViewVariables(VVAccess.ReadWrite), DataField("zapSound")] public SoundSpecifier? ZapSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_zap.ogg"); - /// <summary> - /// The sound when the defib is powered on. - /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("powerOnSound")] - public SoundSpecifier? PowerOnSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_safety_on.ogg"); - - [ViewVariables(VVAccess.ReadWrite), DataField("powerOffSound")] - public SoundSpecifier? PowerOffSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_safety_off.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("chargeSound")] public SoundSpecifier? ChargeSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_charge.ogg"); diff --git a/Content.Shared/Medical/DefibrillatorEvents.cs b/Content.Shared/Medical/DefibrillatorEvents.cs new file mode 100644 index 00000000000..12956d924f7 --- /dev/null +++ b/Content.Shared/Medical/DefibrillatorEvents.cs @@ -0,0 +1,36 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Medical; + +public abstract class BeforeDefibrillatorZapsEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public EntityUid EntityUsingDefib; + public readonly EntityUid Defib; + public EntityUid DefibTarget; + + public BeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibTarget) + { + EntityUsingDefib = entityUsingDefib; + Defib = defib; + DefibTarget = defibTarget; + } +} + +/// <summary> +/// This event is raised on the user using the defibrillator before is actually zaps someone. +/// The event is triggered on the user and all their clothing. +/// </summary> +public sealed class SelfBeforeDefibrillatorZapsEvent : BeforeDefibrillatorZapsEvent +{ + public SelfBeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibtarget) : base(entityUsingDefib, defib, defibtarget) { } +} + +/// <summary> +/// This event is raised on the target before it gets zapped with the defibrillator. +/// The event is triggered on the target itself and all its clothing. +/// </summary> +public sealed class TargetBeforeDefibrillatorZapsEvent : BeforeDefibrillatorZapsEvent +{ + public TargetBeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibtarget) : base(entityUsingDefib, defib, defibtarget) { } +} diff --git a/Content.Shared/Medical/TargetDefibrillatedEvent.cs b/Content.Shared/Medical/TargetDefibrillatedEvent.cs new file mode 100644 index 00000000000..60d1a215845 --- /dev/null +++ b/Content.Shared/Medical/TargetDefibrillatedEvent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared.Medical; + +[ByRefEvent] +public readonly record struct TargetDefibrillatedEvent(EntityUid User, Entity<DefibrillatorComponent> Defibrillator); diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index bf1065c1b1d..2b83f051904 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -168,15 +168,17 @@ private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, Exa args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]"); } + /// <summary> + /// Checks to see if the user's mind prevents them from suicide + /// Handles the suicide event without killing the user if true + /// </summary> private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args) { if (args.Handled) return; if (TryComp(component.Mind, out MindComponent? mind) && mind.PreventSuicide) - { - args.BlockSuicideAttempt(true); - } + args.Handled = true; } public EntityUid? GetMind(EntityUid uid, MindContainerComponent? mind = null) diff --git a/Content.Shared/Mindshield/Components/MindShieldComponent.cs b/Content.Shared/Mindshield/Components/MindShieldComponent.cs index 235862ed02d..77f5497973f 100644 --- a/Content.Shared/Mindshield/Components/MindShieldComponent.cs +++ b/Content.Shared/Mindshield/Components/MindShieldComponent.cs @@ -12,5 +12,5 @@ namespace Content.Shared.Mindshield.Components; public sealed partial class MindShieldComponent : Component { [DataField, ViewVariables(VVAccess.ReadWrite)] - public ProtoId<StatusIconPrototype> MindShieldStatusIcon = "MindShieldIcon"; + public ProtoId<SecurityIconPrototype> MindShieldStatusIcon = "MindShieldIcon"; } diff --git a/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs b/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs index 5e631719864..9419daf3481 100644 --- a/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs +++ b/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Actions; +using Content.Shared.Actions; using Content.Shared.Mobs.Components; namespace Content.Shared.Mobs.Systems; @@ -14,9 +14,26 @@ public sealed class MobStateActionsSystem : EntitySystem public override void Initialize() { SubscribeLocalEvent<MobStateActionsComponent, MobStateChangedEvent>(OnMobStateChanged); + SubscribeLocalEvent<MobStateComponent, ComponentInit>(OnMobStateComponentInit); } private void OnMobStateChanged(EntityUid uid, MobStateActionsComponent component, MobStateChangedEvent args) + { + ComposeActions(uid, component, args.NewMobState); + } + + private void OnMobStateComponentInit(EntityUid uid, MobStateComponent component, ComponentInit args) + { + if (!TryComp<MobStateActionsComponent>(uid, out var mobStateActionsComp)) + return; + + ComposeActions(uid, mobStateActionsComp, component.CurrentState); + } + + /// <summary> + /// Adds or removes actions from a mob based on mobstate. + /// </summary> + private void ComposeActions(EntityUid uid, MobStateActionsComponent component, MobState newMobState) { if (!TryComp<ActionsComponent>(uid, out var action)) return; @@ -27,7 +44,7 @@ private void OnMobStateChanged(EntityUid uid, MobStateActionsComponent component } component.GrantedActions.Clear(); - if (!component.Actions.TryGetValue(args.NewMobState, out var toGrant)) + if (!component.Actions.TryGetValue(newMobState, out var toGrant)) return; foreach (var id in toGrant) diff --git a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs index b11de9eac56..a5307c4ba57 100644 --- a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs +++ b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs @@ -28,10 +28,10 @@ public override void Initialize() private void OnGetState(EntityUid uid, MobThresholdsComponent component, ref ComponentGetState args) { var thresholds = new Dictionary<FixedPoint2, MobState>(); + foreach (var (key, value) in component.Thresholds) - { thresholds.Add(key, value); - } + args.State = new MobThresholdsComponentState(thresholds, component.TriggersAlerts, component.CurrentThresholdState, diff --git a/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs b/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs index 67a238cf60f..d866d83d78d 100644 --- a/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs +++ b/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs @@ -88,5 +88,10 @@ public RefreshMovementSpeedModifiersEvent(bool isImmune = false) { IsImmune = isImmune; } + + public void ModifySpeed(float mod) + { + ModifySpeed(mod, mod); + } } } diff --git a/Content.Shared/Movement/Systems/SharedJetpackSystem.cs b/Content.Shared/Movement/Systems/SharedJetpackSystem.cs index 8d358f8db39..8eb943d7153 100644 --- a/Content.Shared/Movement/Systems/SharedJetpackSystem.cs +++ b/Content.Shared/Movement/Systems/SharedJetpackSystem.cs @@ -15,12 +15,12 @@ namespace Content.Shared.Movement.Systems; public abstract class SharedJetpackSystem : EntitySystem { - [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; - [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedContainerSystem Container = default!; - [Dependency] private readonly SharedMoverController _mover = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly IConfigurationManager _config = default!; @@ -164,7 +164,7 @@ public void SetEnabled(EntityUid uid, JetpackComponent component, bool enabled, if (user == null) { - Container.TryGetContainingContainer(uid, out var container); + Container.TryGetContainingContainer((uid, null, null), out var container); user = container?.Owner; } diff --git a/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs b/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs index 400a675cd25..6e1b3a29aec 100644 --- a/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs +++ b/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Movement.Components; +using Content.Shared.Whitelist; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; @@ -9,6 +10,7 @@ public sealed class SpeedModifierContactsSystem : EntitySystem { [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly MovementSpeedModifierSystem _speedModifierSystem = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; // TODO full-game-save // Either these need to be processed before a map is saved, or slowed/slowing entities need to update on init. @@ -86,7 +88,7 @@ private void MovementSpeedCheck(EntityUid uid, SpeedModifiedByContactComponent c if (!TryComp<SpeedModifierContactsComponent>(ent, out var slowContactsComponent)) continue; - if (slowContactsComponent.IgnoreWhitelist != null && slowContactsComponent.IgnoreWhitelist.IsValid(uid)) + if (_whitelistSystem.IsWhitelistPass(slowContactsComponent.IgnoreWhitelist, uid)) continue; walkSpeed += slowContactsComponent.WalkSpeedModifier; diff --git a/Content.Server/NPC/Components/FactionExceptionComponent.cs b/Content.Shared/NPC/Components/FactionExceptionComponent.cs similarity index 72% rename from Content.Server/NPC/Components/FactionExceptionComponent.cs rename to Content.Shared/NPC/Components/FactionExceptionComponent.cs index 6abd503537c..54de0404c2f 100644 --- a/Content.Server/NPC/Components/FactionExceptionComponent.cs +++ b/Content.Shared/NPC/Components/FactionExceptionComponent.cs @@ -1,23 +1,24 @@ -using Content.Server.NPC.Systems; +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; -namespace Content.Server.NPC.Components; +namespace Content.Shared.NPC.Components; /// <summary> /// Prevents an NPC from attacking ignored entities from enemy factions. /// Can be added to if pettable, see PettableFriendComponent. /// </summary> -[RegisterComponent, Access(typeof(NpcFactionSystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSystem))] public sealed partial class FactionExceptionComponent : Component { /// <summary> /// Collection of entities that this NPC will refuse to attack /// </summary> - [DataField("ignored")] + [DataField] public HashSet<EntityUid> Ignored = new(); /// <summary> /// Collection of entities that this NPC will attack, regardless of faction. /// </summary> - [DataField("hostiles")] + [DataField] public HashSet<EntityUid> Hostiles = new(); } diff --git a/Content.Shared/NPC/Components/FactionExceptionTrackerComponent.cs b/Content.Shared/NPC/Components/FactionExceptionTrackerComponent.cs new file mode 100644 index 00000000000..f6eded7371b --- /dev/null +++ b/Content.Shared/NPC/Components/FactionExceptionTrackerComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.NPC.Components; + +/// <summary> +/// This is used for tracking entities stored in <see cref="FactionExceptionComponent"/>. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSystem))] +public sealed partial class FactionExceptionTrackerComponent : Component +{ + /// <summary> + /// Entities with <see cref="FactionExceptionComponent"/> that are tracking this entity. + /// </summary> + [DataField] + public HashSet<EntityUid> Entities = new(); +} diff --git a/Content.Shared/NPC/Components/NpcFactionMemberComponent.cs b/Content.Shared/NPC/Components/NpcFactionMemberComponent.cs new file mode 100644 index 00000000000..188ece81ed7 --- /dev/null +++ b/Content.Shared/NPC/Components/NpcFactionMemberComponent.cs @@ -0,0 +1,36 @@ +using Content.Shared.NPC.Prototypes; +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.NPC.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSystem))] +public sealed partial class NpcFactionMemberComponent : Component +{ + /// <summary> + /// Factions this entity is a part of. + /// </summary> + [DataField] + public HashSet<ProtoId<NpcFactionPrototype>> Factions = new(); + + /// <summary> + /// Cached friendly factions. + /// </summary> + [ViewVariables] + public readonly HashSet<ProtoId<NpcFactionPrototype>> FriendlyFactions = new(); + + /// <summary> + /// Cached hostile factions. + /// </summary> + [ViewVariables] + public readonly HashSet<ProtoId<NpcFactionPrototype>> HostileFactions = new(); + + // Nyano - Summary - Begin modified code block: support for specific entities to be friendly. + /// <summary> + /// Permanently friendly specific entities. Our summoner, etc. + /// Would like to separate. Could I do that by extending this method, maybe? + /// </summary> + public HashSet<EntityUid> ExceptionalFriendlies = new(); + // Nyano - End modified code block. +} diff --git a/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs b/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs new file mode 100644 index 00000000000..1dcdd751c88 --- /dev/null +++ b/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs @@ -0,0 +1,32 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.NPC.Prototypes; + +/// <summary> +/// Contains data about this faction's relations with other factions. +/// </summary> +[Prototype("npcFaction")] +public sealed partial class NpcFactionPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public List<ProtoId<NpcFactionPrototype>> Friendly = new(); + + [DataField] + public List<ProtoId<NpcFactionPrototype>> Hostile = new(); +} + +/// <summary> +/// Cached data for the faction prototype. Is modified at runtime, whereas the prototype is not. +/// </summary> +public record struct FactionData +{ + [ViewVariables] + public HashSet<ProtoId<NpcFactionPrototype>> Friendly; + + [ViewVariables] + public HashSet<ProtoId<NpcFactionPrototype>> Hostile; +} diff --git a/Content.Shared/NPC/SharedPathfindingSystem.Line.cs b/Content.Shared/NPC/SharedPathfindingSystem.Line.cs new file mode 100644 index 00000000000..7e7bdd082b1 --- /dev/null +++ b/Content.Shared/NPC/SharedPathfindingSystem.Line.cs @@ -0,0 +1,74 @@ +namespace Content.Shared.NPC; + +public abstract partial class SharedPathfindingSystem +{ + public static void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback) + { + // https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566 + // declare all locals at the top so it's obvious how big the footprint is + int dx, dy, xinc, yinc, side, i, error; + + // starting cell is always returned + if (!callback(start)) + return; + + xinc = (end.X < start.X) ? -1 : 1; + yinc = (end.Y < start.Y) ? -1 : 1; + dx = xinc * (end.X - start.X); + dy = yinc * (end.Y - start.Y); + var ax = start.X; + var ay = start.Y; + + if (dx == dy) // Handle perfect diagonals + { + // I include this "optimization" for more aesthetic reasons, actually. + // While Bresenham's Line can handle perfect diagonals just fine, it adds + // additional cells to the line that make it not a perfect diagonal + // anymore. So, while this branch is ~twice as fast as the next branch, + // the real reason it is here is for style. + + // Also, there *is* the reason of performance. If used for cell-based + // raycasts, for example, then perfect diagonals will check half as many + // cells. + + while (dx --> 0) + { + ax += xinc; + ay += yinc; + if (!callback(new Vector2i(ax, ay))) + return; + } + + return; + } + + // Handle all other lines + + side = -1 * ((dx == 0 ? yinc : xinc) - 1); + + i = dx + dy; + error = dx - dy; + + dx *= 2; + dy *= 2; + + while (i --> 0) + { + if (error > 0 || error == side) + { + ax += xinc; + error -= dy; + } + else + { + ay += yinc; + error += dx; + } + + if (!callback(new Vector2i(ax, ay))) + return; + } + } + + public delegate bool Vector2iCallback(Vector2i index); +} \ No newline at end of file diff --git a/Content.Shared/NPC/SharedPathfindingSystem.cs b/Content.Shared/NPC/SharedPathfindingSystem.cs index 8831acc1ddb..f59e6d3c462 100644 --- a/Content.Shared/NPC/SharedPathfindingSystem.cs +++ b/Content.Shared/NPC/SharedPathfindingSystem.cs @@ -2,7 +2,7 @@ namespace Content.Shared.NPC; -public abstract class SharedPathfindingSystem : EntitySystem +public abstract partial class SharedPathfindingSystem : EntitySystem { /// <summary> /// This is equivalent to agent radii for navmeshes. In our case it's preferable that things are cleanly @@ -37,4 +37,31 @@ public static float OctileDistance(Vector2i start, Vector2i end) var ab = Vector2.Abs(diff); return ab.X + ab.Y + (1.41f - 2) * Math.Min(ab.X, ab.Y); } -} + + public static IEnumerable<Vector2i> GetTileOutline(Vector2i center, float radius) + { + // https://www.redblobgames.com/grids/circle-drawing/ + var vecCircle = center + Vector2.One / 2f; + + for (var r = 0; r <= Math.Floor(radius * MathF.Sqrt(0.5f)); r++) + { + var d = MathF.Floor(MathF.Sqrt(radius * radius - r * r)); + + yield return new Vector2(vecCircle.X - d, vecCircle.Y + r).Floored(); + + yield return new Vector2(vecCircle.X + d, vecCircle.Y + r).Floored(); + + yield return new Vector2(vecCircle.X - d, vecCircle.Y - r).Floored(); + + yield return new Vector2(vecCircle.X + d, vecCircle.Y - r).Floored(); + + yield return new Vector2(vecCircle.X + r, vecCircle.Y - d).Floored(); + + yield return new Vector2(vecCircle.X + r, vecCircle.Y + d).Floored(); + + yield return new Vector2(vecCircle.X - r, vecCircle.Y - d).Floored(); + + yield return new Vector2(vecCircle.X - r, vecCircle.Y + d).Floored(); + } + } +} \ No newline at end of file diff --git a/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs b/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs new file mode 100644 index 00000000000..e69f0c2f7ad --- /dev/null +++ b/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs @@ -0,0 +1,135 @@ +using Content.Shared.NPC.Components; +using System.Linq; + +namespace Content.Shared.NPC.Systems; + +/// <summary> +/// Prevents an NPC from attacking some entities from an enemy faction. +/// Also makes it attack some entities even if they are in neutral factions (retaliation). +/// </summary> +public sealed partial class NpcFactionSystem +{ + private EntityQuery<FactionExceptionComponent> _exceptionQuery; + private EntityQuery<FactionExceptionTrackerComponent> _trackerQuery; + + public void InitializeException() + { + _exceptionQuery = GetEntityQuery<FactionExceptionComponent>(); + _trackerQuery = GetEntityQuery<FactionExceptionTrackerComponent>(); + + SubscribeLocalEvent<FactionExceptionComponent, ComponentShutdown>(OnShutdown); + SubscribeLocalEvent<FactionExceptionTrackerComponent, ComponentShutdown>(OnTrackerShutdown); + } + + private void OnShutdown(Entity<FactionExceptionComponent> ent, ref ComponentShutdown args) + { + foreach (var uid in ent.Comp.Hostiles) + { + if (_trackerQuery.TryGetComponent(uid, out var tracker)) + tracker.Entities.Remove(ent); + } + + foreach (var uid in ent.Comp.Ignored) + { + if (_trackerQuery.TryGetComponent(uid, out var tracker)) + tracker.Entities.Remove(ent); + } + } + + private void OnTrackerShutdown(Entity<FactionExceptionTrackerComponent> ent, ref ComponentShutdown args) + { + foreach (var uid in ent.Comp.Entities) + { + if (!_exceptionQuery.TryGetComponent(uid, out var exception)) + continue; + + exception.Ignored.Remove(ent); + exception.Hostiles.Remove(ent); + } + } + + /// <summary> + /// Returns whether the entity from an enemy faction won't be attacked + /// </summary> + public bool IsIgnored(Entity<FactionExceptionComponent?> ent, EntityUid target) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return ent.Comp.Ignored.Contains(target); + } + + /// <summary> + /// Returns the specific hostile entities for a given entity. + /// </summary> + public IEnumerable<EntityUid> GetHostiles(Entity<FactionExceptionComponent?> ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return Array.Empty<EntityUid>(); + + // evil c# + return ent.Comp!.Hostiles; + } + + /// <summary> + /// Prevents an entity from an enemy faction from being attacked + /// </summary> + public void IgnoreEntity(Entity<FactionExceptionComponent?> ent, Entity<FactionExceptionTrackerComponent?> target) + { + ent.Comp ??= EnsureComp<FactionExceptionComponent>(ent); + ent.Comp.Ignored.Add(target); + target.Comp ??= EnsureComp<FactionExceptionTrackerComponent>(target); + target.Comp.Entities.Add(ent); + } + + /// <summary> + /// Prevents a list of entities from an enemy faction from being attacked + /// </summary> + public void IgnoreEntities(Entity<FactionExceptionComponent?> ent, IEnumerable<EntityUid> ignored) + { + ent.Comp ??= EnsureComp<FactionExceptionComponent>(ent); + foreach (var ignore in ignored) + { + IgnoreEntity(ent, ignore); + } + } + + /// <summary> + /// Makes an entity always be considered hostile. + /// </summary> + public void AggroEntity(Entity<FactionExceptionComponent?> ent, Entity<FactionExceptionTrackerComponent?> target) + { + ent.Comp ??= EnsureComp<FactionExceptionComponent>(ent); + ent.Comp.Hostiles.Add(target); + target.Comp ??= EnsureComp<FactionExceptionTrackerComponent>(target); + target.Comp.Entities.Add(ent); + } + + /// <summary> + /// Makes an entity no longer be considered hostile, if it was. + /// Doesn't apply to regular faction hostilities. + /// </summary> + public void DeAggroEntity(Entity<FactionExceptionComponent?> ent, EntityUid target) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + if (!ent.Comp.Hostiles.Remove(target) || !_trackerQuery.TryGetComponent(target, out var tracker)) + return; + + tracker.Entities.Remove(ent); + } + + /// <summary> + /// Makes a list of entities no longer be considered hostile, if it was. + /// Doesn't apply to regular faction hostilities. + /// </summary> + public void AggroEntities(Entity<FactionExceptionComponent?> ent, IEnumerable<EntityUid> entities) + { + ent.Comp ??= EnsureComp<FactionExceptionComponent>(ent); + foreach (var uid in entities) + { + AggroEntity(ent, uid); + } + } +} diff --git a/Content.Shared/NPC/Systems/NpcFactionSystem.cs b/Content.Shared/NPC/Systems/NpcFactionSystem.cs new file mode 100644 index 00000000000..448c7eeaec8 --- /dev/null +++ b/Content.Shared/NPC/Systems/NpcFactionSystem.cs @@ -0,0 +1,318 @@ +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Prototypes; +using Robust.Shared.Prototypes; +using System.Collections.Frozen; +using System.Linq; + +namespace Content.Shared.NPC.Systems; + +/// <summary> +/// Outlines faction relationships with each other. +/// part of psionics rework was making this a partial class. Should've already been handled upstream, based on the linter. +/// </summary> +public sealed partial class NpcFactionSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + + /// <summary> + /// To avoid prototype mutability we store an intermediary data class that gets used instead. + /// </summary> + private FrozenDictionary<string, FactionData> _factions = FrozenDictionary<string, FactionData>.Empty; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<NpcFactionMemberComponent, ComponentStartup>(OnFactionStartup); + SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnProtoReload); + + InitializeException(); + RefreshFactions(); + } + + private void OnProtoReload(PrototypesReloadedEventArgs obj) + { + if (obj.WasModified<NpcFactionPrototype>()) + RefreshFactions(); + } + + private void OnFactionStartup(Entity<NpcFactionMemberComponent> ent, ref ComponentStartup args) + { + RefreshFactions(ent); + } + + /// <summary> + /// Refreshes the cached factions for this component. + /// </summary> + private void RefreshFactions(Entity<NpcFactionMemberComponent> ent) + { + ent.Comp.FriendlyFactions.Clear(); + ent.Comp.HostileFactions.Clear(); + + foreach (var faction in ent.Comp.Factions) + { + // YAML Linter already yells about this, don't need to log an error here + if (!_factions.TryGetValue(faction, out var factionData)) + continue; + + ent.Comp.FriendlyFactions.UnionWith(factionData.Friendly); + ent.Comp.HostileFactions.UnionWith(factionData.Hostile); + } + } + + /// <summary> + /// Returns whether an entity is a member of a faction. + /// </summary> + public bool IsMember(Entity<NpcFactionMemberComponent?> ent, string faction) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return ent.Comp.Factions.Contains(faction); + } + + /// <summary> + /// Returns whether an entity is a member of any listed faction. + /// If the list is empty this returns false. + /// </summary> + public bool IsMemberOfAny(Entity<NpcFactionMemberComponent?> ent, IEnumerable<ProtoId<NpcFactionPrototype>> factions) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + foreach (var faction in factions) + { + if (ent.Comp.Factions.Contains(faction)) + return true; + } + + return false; + } + + /// <summary> + /// Adds this entity to the particular faction. + /// </summary> + public void AddFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true) + { + if (!_proto.HasIndex<NpcFactionPrototype>(faction)) + { + Log.Error($"Unable to find faction {faction}"); + return; + } + + ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent); + if (!ent.Comp.Factions.Add(faction)) + return; + + if (dirty) + RefreshFactions((ent, ent.Comp)); + } + + /// <summary> + /// Adds this entity to the particular factions. + /// </summary> + public void AddFactions(Entity<NpcFactionMemberComponent?> ent, HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true) + { + ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent); + + foreach (var faction in factions) + { + if (!_proto.HasIndex(faction)) + { + Log.Error($"Unable to find faction {faction}"); + continue; + } + + ent.Comp.Factions.Add(faction); + } + + if (dirty) + RefreshFactions((ent, ent.Comp)); + } + + /// <summary> + /// Removes this entity from the particular faction. + /// </summary> + public void RemoveFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true) + { + if (!_proto.HasIndex<NpcFactionPrototype>(faction)) + { + Log.Error($"Unable to find faction {faction}"); + return; + } + + if (!Resolve(ent, ref ent.Comp, false)) + return; + + if (!ent.Comp.Factions.Remove(faction)) + return; + + if (dirty) + RefreshFactions((ent, ent.Comp)); + } + + /// <summary> + /// Remove this entity from all factions. + /// </summary> + public void ClearFactions(Entity<NpcFactionMemberComponent?> ent, bool dirty = true) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.Factions.Clear(); + + if (dirty) + RefreshFactions((ent, ent.Comp)); + } + + public IEnumerable<EntityUid> GetNearbyHostiles(Entity<NpcFactionMemberComponent?, FactionExceptionComponent?> ent, float range) + { + if (!Resolve(ent, ref ent.Comp1, false)) + return Array.Empty<EntityUid>(); + + var hostiles = GetNearbyFactions(ent, range, ent.Comp1.HostileFactions) + // ignore mobs that have both hostile faction and the same faction, + // otherwise having multiple factions is strictly negative + .Where(target => !IsEntityFriendly((ent, ent.Comp1), target)); + if (!Resolve(ent, ref ent.Comp2, false)) + return hostiles; + + // ignore anything from enemy faction that we are explicitly friendly towards + var faction = (ent.Owner, ent.Comp2); + return hostiles + .Union(GetHostiles(faction)) + .Where(target => !IsIgnored(faction, target)); + } + + public IEnumerable<EntityUid> GetNearbyFriendlies(Entity<NpcFactionMemberComponent?> ent, float range) + { + if (!Resolve(ent, ref ent.Comp, false)) + return Array.Empty<EntityUid>(); + + return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions); + } + + private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, HashSet<ProtoId<NpcFactionPrototype>> factions) + { + var xform = Transform(entity); + foreach (var ent in _lookup.GetEntitiesInRange<NpcFactionMemberComponent>(_xform.GetMapCoordinates((entity, xform)), range)) + { + if (ent.Owner == entity) + continue; + + if (!factions.Overlaps(ent.Comp.Factions)) + continue; + + yield return ent.Owner; + } + } + + /// <remarks> + /// 1-way and purely faction based, ignores faction exception. + /// </remarks> + public bool IsEntityFriendly(Entity<NpcFactionMemberComponent?> ent, Entity<NpcFactionMemberComponent?> other) + { + if (!Resolve(ent, ref ent.Comp, false) || !Resolve(other, ref other.Comp, false)) + return false; + + return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions); + } + + public bool IsFactionFriendly(string target, string with) + { + return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target); + } + + public bool IsFactionFriendly(string target, Entity<NpcFactionMemberComponent?> with) + { + if (!Resolve(with, ref with.Comp, false)) + return false; + + return with.Comp.Factions.All(x => IsFactionFriendly(target, x)) || + with.Comp.FriendlyFactions.Contains(target); + } + + public bool IsFactionHostile(string target, string with) + { + return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target); + } + + public bool IsFactionHostile(string target, Entity<NpcFactionMemberComponent?> with) + { + if (!Resolve(with, ref with.Comp, false)) + return false; + + return with.Comp.Factions.All(x => IsFactionHostile(target, x)) || + with.Comp.HostileFactions.Contains(target); + } + + public bool IsFactionNeutral(string target, string with) + { + return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with); + } + + /// <summary> + /// Makes the source faction friendly to the target faction, 1-way. + /// </summary> + public void MakeFriendly(string source, string target) + { + if (!_factions.TryGetValue(source, out var sourceFaction)) + { + Log.Error($"Unable to find faction {source}"); + return; + } + + if (!_factions.ContainsKey(target)) + { + Log.Error($"Unable to find faction {target}"); + return; + } + + sourceFaction.Friendly.Add(target); + sourceFaction.Hostile.Remove(target); + RefreshFactions(); + } + + /// <summary> + /// Makes the source faction hostile to the target faction, 1-way. + /// </summary> + public void MakeHostile(string source, string target) + { + if (!_factions.TryGetValue(source, out var sourceFaction)) + { + Log.Error($"Unable to find faction {source}"); + return; + } + + if (!_factions.ContainsKey(target)) + { + Log.Error($"Unable to find faction {target}"); + return; + } + + sourceFaction.Friendly.Remove(target); + sourceFaction.Hostile.Add(target); + RefreshFactions(); + } + + private void RefreshFactions() + { + _factions = _proto.EnumeratePrototypes<NpcFactionPrototype>().ToFrozenDictionary( + faction => faction.ID, + faction => new FactionData + { + Friendly = faction.Friendly.ToHashSet(), + Hostile = faction.Hostile.ToHashSet() + }); + + var query = AllEntityQuery<NpcFactionMemberComponent>(); + while (query.MoveNext(out var uid, out var comp)) + { + comp.FriendlyFactions.Clear(); + comp.HostileFactions.Clear(); + RefreshFactions((uid, comp)); + } + } +} diff --git a/Content.Shared/NameModifier/Components/ModifyWearerNameComponent.cs b/Content.Shared/NameModifier/Components/ModifyWearerNameComponent.cs new file mode 100644 index 00000000000..781ed3daae1 --- /dev/null +++ b/Content.Shared/NameModifier/Components/ModifyWearerNameComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.NameModifier.Components; + +/// <summary> +/// Adds a modifier to the wearer's name when this item is equipped, +/// and removes it when it is unequipped. +/// </summary> +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class ModifyWearerNameComponent : Component +{ + /// <summary> + /// The localization ID of the text to be used as the modifier. + /// The base name will be passed in as <c>$baseName</c> + /// </summary> + [DataField, AutoNetworkedField] + public LocId LocId = string.Empty; + + /// <summary> + /// Priority of the modifier. See <see cref="EntitySystems.RefreshNameModifiersEvent"/> for more information. + /// </summary> + [DataField, AutoNetworkedField] + public int Priority; +} diff --git a/Content.Shared/NameModifier/Components/NameModifierComponent.cs b/Content.Shared/NameModifier/Components/NameModifierComponent.cs new file mode 100644 index 00000000000..3a9dd9712b6 --- /dev/null +++ b/Content.Shared/NameModifier/Components/NameModifierComponent.cs @@ -0,0 +1,20 @@ +using Content.Shared.NameModifier.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.NameModifier.Components; + +/// <summary> +/// Used to manage modifiers on an entity's name and handle renaming in a way +/// that survives being renamed by multiple systems. +/// </summary> +[RegisterComponent] +[NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(NameModifierSystem))] +public sealed partial class NameModifierComponent : Component +{ + /// <summary> + /// The entity's name without any modifiers applied. + /// </summary> + [DataField, AutoNetworkedField] + public string BaseName = string.Empty; +} diff --git a/Content.Shared/NameModifier/EntitySystems/ModifyWearerNameSystem.cs b/Content.Shared/NameModifier/EntitySystems/ModifyWearerNameSystem.cs new file mode 100644 index 00000000000..e728e6cdb51 --- /dev/null +++ b/Content.Shared/NameModifier/EntitySystems/ModifyWearerNameSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Clothing; +using Content.Shared.Inventory; +using Content.Shared.NameModifier.Components; + +namespace Content.Shared.NameModifier.EntitySystems; + +public sealed partial class ModifyWearerNameSystem : EntitySystem +{ + [Dependency] private readonly NameModifierSystem _nameMod = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ModifyWearerNameComponent, InventoryRelayedEvent<RefreshNameModifiersEvent>>(OnRefreshNameModifiers); + SubscribeLocalEvent<ModifyWearerNameComponent, ClothingGotEquippedEvent>(OnGotEquipped); + SubscribeLocalEvent<ModifyWearerNameComponent, ClothingGotUnequippedEvent>(OnGotUnequipped); + } + + private void OnGotEquipped(Entity<ModifyWearerNameComponent> entity, ref ClothingGotEquippedEvent args) + { + _nameMod.RefreshNameModifiers(args.Wearer); + } + + private void OnGotUnequipped(Entity<ModifyWearerNameComponent> entity, ref ClothingGotUnequippedEvent args) + { + _nameMod.RefreshNameModifiers(args.Wearer); + } + + private void OnRefreshNameModifiers(Entity<ModifyWearerNameComponent> entity, ref InventoryRelayedEvent<RefreshNameModifiersEvent> args) + { + args.Args.AddModifier(entity.Comp.LocId, entity.Comp.Priority); + } +} diff --git a/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs b/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs new file mode 100644 index 00000000000..4dffb51805c --- /dev/null +++ b/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs @@ -0,0 +1,143 @@ +using System.Linq; +using Content.Shared.Inventory; +using Content.Shared.NameModifier.Components; + +namespace Content.Shared.NameModifier.EntitySystems; + +/// <inheritdoc cref="NameModifierComponent"/> +public sealed partial class NameModifierSystem : EntitySystem +{ + [Dependency] private readonly MetaDataSystem _metaData = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<NameModifierComponent, EntityRenamedEvent>(OnEntityRenamed); + } + + private void OnEntityRenamed(Entity<NameModifierComponent> entity, ref EntityRenamedEvent args) + { + SetBaseName((entity, entity.Comp), args.NewName); + RefreshNameModifiers((entity, entity.Comp)); + } + + private void SetBaseName(Entity<NameModifierComponent> entity, string name) + { + if (name == entity.Comp.BaseName) + return; + + // Set the base name to the new name + entity.Comp.BaseName = name; + Dirty(entity); + } + + /// <summary> + /// Raises a <see cref="RefreshNameModifiersEvent"/> to gather modifiers and + /// updates the entity's name to its base name with modifiers applied. + /// This will add a <see cref="NameModifierComponent"/> if any modifiers are added. + /// </summary> + /// <remarks> + /// Call this to update the entity's name when adding or removing a modifier. + /// </remarks> + public void RefreshNameModifiers(Entity<NameModifierComponent?> entity) + { + var meta = MetaData(entity); + var baseName = meta.EntityName; + if (Resolve(entity, ref entity.Comp, logMissing: false)) + baseName = entity.Comp.BaseName; + + // Raise an event to get any modifiers + // If the entity already has the component, use its BaseName, otherwise use the entity's name from metadata + var modifierEvent = new RefreshNameModifiersEvent(baseName); + RaiseLocalEvent(entity, ref modifierEvent); + + // Nothing added a modifier, so we can just use the base name + if (modifierEvent.ModifierCount == 0) + { + // If the entity doesn't have the component, we're done + if (entity.Comp == null) + return; + + // Restore the base name + _metaData.SetEntityName(entity, entity.Comp.BaseName, meta, raiseEvents: false); + // The component isn't doing anything anymore, so remove it + RemComp<NameModifierComponent>(entity); + return; + } + // We have at least one modifier, so we need to apply it to the entity. + + // Get the final name with modifiers applied + var modifiedName = modifierEvent.GetModifiedName(); + + // Add the component if needed, and initialize it with the base name + if (!EnsureComp<NameModifierComponent>(entity, out var comp)) + SetBaseName((entity, comp), meta.EntityName); + + // Set the entity's name with modifiers applied + _metaData.SetEntityName(entity, modifiedName, meta, raiseEvents: false); + } +} + +/// <summary> +/// Raised on an entity when <see cref="NameModifierSystem.RefreshNameModifiers"/> is called. +/// Subscribe to this event and use its methods to add modifiers to the entity's name. +/// </summary> +[ByRefEvent] +public sealed class RefreshNameModifiersEvent : IInventoryRelayEvent +{ + /// <summary> + /// The entity's name without any modifiers applied. + /// If you want to base a modifier on the entity's name, use + /// this so you don't include other modifiers. + /// </summary> + public readonly string BaseName; + + private readonly List<(LocId LocId, int Priority, (string, object)[] ExtraArgs)> _modifiers = []; + + /// <inheritdoc/> + public SlotFlags TargetSlots => ~SlotFlags.POCKET; + + /// <summary> + /// How many modifiers have been added to this event. + /// </summary> + public int ModifierCount => _modifiers.Count; + + public RefreshNameModifiersEvent(string baseName) + { + BaseName = baseName; + } + + /// <summary> + /// Adds a modifier to the entity's name. + /// The original name will be passed to Fluent as <c>$baseName</c> along with any <paramref name="extraArgs"/>. + /// Modifiers with a higher <paramref name="priority"/> will be applied later. + /// </summary> + public void AddModifier(LocId locId, int priority = 0, params (string, object)[] extraArgs) + { + _modifiers.Add((locId, priority, extraArgs)); + } + + /// <summary> + /// Returns the final name with all modifiers applied. + /// </summary> + public string GetModifiedName() + { + // Start out with the entity's name name + var name = BaseName; + + // Iterate through all the modifiers in priority order + foreach (var modifier in _modifiers.OrderBy(n => n.Priority)) + { + // Grab any extra args needed by the Loc string + var args = modifier.ExtraArgs; + // Add the current version of the entity name as an arg + Array.Resize(ref args, args.Length + 1); + args[^1] = ("baseName", name); + // Resolve the Loc string and use the result as the base in the next iteration. + name = Loc.GetString(modifier.LocId, args); + } + + return name; + } +} diff --git a/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs b/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs index 55bcdd0f0a5..9c39c4724ce 100644 --- a/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs +++ b/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Ninja.Systems; using Robust.Shared.Audio; +using Robust.Shared.GameStates; namespace Content.Shared.Ninja.Components; @@ -7,32 +8,33 @@ namespace Content.Shared.Ninja.Components; /// Component for draining power from APCs/substations/SMESes, when ProviderUid is set to a battery cell. /// Does not rely on relay, simply being on the user and having BatteryUid set is enough. /// </summary> -[RegisterComponent, Access(typeof(SharedBatteryDrainerSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedBatteryDrainerSystem))] public sealed partial class BatteryDrainerComponent : Component { /// <summary> /// The powercell entity to drain power into. /// Determines whether draining is possible. /// </summary> - [DataField("batteryUid"), ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public EntityUid? BatteryUid; /// <summary> /// Conversion rate between joules in a device and joules added to battery. /// Should be very low since powercells store nothing compared to even an APC. /// </summary> - [DataField("drainEfficiency"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float DrainEfficiency = 0.001f; /// <summary> /// Time that the do after takes to drain charge from a battery, in seconds /// </summary> - [DataField("drainTime"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float DrainTime = 1f; /// <summary> /// Sound played after the doafter ends. /// </summary> - [DataField("sparkSound")] + [DataField] public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks"); } diff --git a/Content.Shared/Ninja/Components/BombingTargetComponent.cs b/Content.Shared/Ninja/Components/BombingTargetComponent.cs index bf0eaec84be..c429eb6880e 100644 --- a/Content.Shared/Ninja/Components/BombingTargetComponent.cs +++ b/Content.Shared/Ninja/Components/BombingTargetComponent.cs @@ -4,6 +4,4 @@ namespace Content.Shared.Ninja.Components; /// Makes this warp point a valid bombing target for ninja's spider charge. /// </summary> [RegisterComponent] -public sealed partial class BombingTargetComponent : Component -{ -} +public sealed partial class BombingTargetComponent : Component; diff --git a/Content.Shared/Ninja/Components/DashAbilityComponent.cs b/Content.Shared/Ninja/Components/DashAbilityComponent.cs index ba4060c7035..464f48f187e 100644 --- a/Content.Shared/Ninja/Components/DashAbilityComponent.cs +++ b/Content.Shared/Ninja/Components/DashAbilityComponent.cs @@ -8,6 +8,7 @@ namespace Content.Shared.Ninja.Components; /// <summary> /// Adds an action to dash, teleport to clicked position, when this item is held. +/// Cancel <see cref="CheckDashEvent"/> to prevent using it. /// </summary> [RegisterComponent, NetworkedComponent, Access(typeof(DashAbilitySystem)), AutoGenerateComponentState] public sealed partial class DashAbilityComponent : Component @@ -16,19 +17,10 @@ public sealed partial class DashAbilityComponent : Component /// The action id for dashing. /// </summary> [DataField] - public EntProtoId DashAction = "ActionEnergyKatanaDash"; + public EntProtoId<WorldTargetActionComponent> DashAction = "ActionEnergyKatanaDash"; [DataField, AutoNetworkedField] public EntityUid? DashActionEntity; - - /// <summary> - /// Sound played when using dash action. - /// </summary> - [DataField("blinkSound"), ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg") - { - Params = AudioParams.Default.WithVolume(5f) - }; } -public sealed partial class DashEvent : WorldTargetActionEvent { } +public sealed partial class DashEvent : WorldTargetActionEvent; diff --git a/Content.Shared/Ninja/Components/EmagProviderComponent.cs b/Content.Shared/Ninja/Components/EmagProviderComponent.cs index db7678f61d7..ae3e85cbe42 100644 --- a/Content.Shared/Ninja/Components/EmagProviderComponent.cs +++ b/Content.Shared/Ninja/Components/EmagProviderComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.Tag; using Content.Shared.Whitelist; using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; namespace Content.Shared.Ninja.Components; @@ -10,19 +10,18 @@ namespace Content.Shared.Ninja.Components; /// Component for emagging things on click. /// No charges but checks against a whitelist. /// </summary> -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(EmagProviderSystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(EmagProviderSystem))] public sealed partial class EmagProviderComponent : Component { /// <summary> /// The tag that marks an entity as immune to emagging. /// </summary> - [DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>))] - public string EmagImmuneTag = "EmagImmune"; + [DataField] + public ProtoId<TagPrototype> EmagImmuneTag = "EmagImmune"; /// <summary> /// Whitelist that entities must be on to work. /// </summary> - [DataField("whitelist"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public EntityWhitelist? Whitelist = null; + [DataField] + public EntityWhitelist? Whitelist; } diff --git a/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs b/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs index 33b8fc78933..84c58bb6480 100644 --- a/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs +++ b/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs @@ -7,6 +7,4 @@ namespace Content.Shared.Ninja.Components; /// Requires a ninja with a suit for abilities to work. /// </summary> [RegisterComponent, NetworkedComponent] -public sealed partial class EnergyKatanaComponent : Component -{ -} +public sealed partial class EnergyKatanaComponent : Component; diff --git a/Content.Shared/Ninja/Components/ItemCreatorComponent.cs b/Content.Shared/Ninja/Components/ItemCreatorComponent.cs new file mode 100644 index 00000000000..d9f66d21a31 --- /dev/null +++ b/Content.Shared/Ninja/Components/ItemCreatorComponent.cs @@ -0,0 +1,52 @@ +using Content.Shared.Actions; +using Content.Shared.Ninja.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Ninja.Components; + +/// <summary> +/// Uses battery charge to spawn an item and place it in the user's hands. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedItemCreatorSystem))] +public sealed partial class ItemCreatorComponent : Component +{ + /// <summary> + /// The battery entity to use charge from + /// </summary> + [DataField, AutoNetworkedField] + public EntityUid? Battery; + + /// <summary> + /// The action id for creating an item. + /// </summary> + [DataField(required: true)] + public EntProtoId<InstantActionComponent> Action = string.Empty; + + [DataField, AutoNetworkedField] + public EntityUid? ActionEntity; + + /// <summary> + /// Battery charge used to create an item. + /// </summary> + [DataField(required: true)] + public float Charge = 14.4f; + + /// <summary> + /// Item to create with the action + /// </summary> + [DataField(required: true)] + public EntProtoId SpawnedPrototype = string.Empty; + + /// <summary> + /// Popup shown to the user when there isn't enough power to create an item. + /// </summary> + [DataField(required: true)] + public LocId NoPowerPopup = string.Empty; +} + +/// <summary> +/// Action event to use an <see cref="ItemCreator"/>. +/// </summary> +public sealed partial class CreateItemEvent : InstantActionEvent; diff --git a/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs b/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs index 7b57926330b..3b9e2a5e356 100644 --- a/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs +++ b/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs @@ -1,20 +1,17 @@ -using Content.Shared.DoAfter; using Content.Shared.Ninja.Systems; -using Content.Shared.Toggleable; -using Content.Shared.Whitelist; +using Content.Shared.Objectives.Components; using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Utility; namespace Content.Shared.Ninja.Components; /// <summary> /// Component for toggling glove powers. -/// Powers being enabled is controlled by User not being null. /// </summary> +/// <remarks> +/// Requires <c>ItemToggleComponent</c>. +/// </remarks> [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedNinjaGlovesSystem))] public sealed partial class NinjaGlovesComponent : Component @@ -22,24 +19,33 @@ public sealed partial class NinjaGlovesComponent : Component /// <summary> /// Entity of the ninja using these gloves, usually means enabled /// </summary> - [DataField("user"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? User; /// <summary> - /// The action id for toggling ninja gloves abilities + /// Abilities to give to the user when enabled. /// </summary> - [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string ToggleAction = "ActionToggleNinjaGloves"; + [DataField(required: true)] + public List<NinjaGloveAbility> Abilities = new(); +} - [DataField, AutoNetworkedField] - public EntityUid? ToggleActionEntity; +/// <summary> +/// An ability that adds components to the user when the gloves are enabled. +/// </summary> +[DataRecord] +public record struct NinjaGloveAbility() +{ + /// <summary> + /// If not null, checks if an objective with this prototype has been completed. + /// If it has, the ability components are skipped to prevent doing the objective twice. + /// The objective must have <c>CodeConditionComponent</c> to be checked. + /// </summary> + [DataField] + public EntProtoId<ObjectiveComponent>? Objective; /// <summary> - /// The whitelist used for the emag provider to emag airlocks only (not regular doors). + /// Components to add and remove. /// </summary> - [DataField("doorjackWhitelist")] - public EntityWhitelist DoorjackWhitelist = new() - { - Components = new[] {"Airlock"} - }; + [DataField(required: true)] + public ComponentRegistry Components = new(); } diff --git a/Content.Shared/Ninja/Components/NinjaSuitComponent.cs b/Content.Shared/Ninja/Components/NinjaSuitComponent.cs index 7e7b1ffcd30..8b477b2aa5f 100644 --- a/Content.Shared/Ninja/Components/NinjaSuitComponent.cs +++ b/Content.Shared/Ninja/Components/NinjaSuitComponent.cs @@ -3,9 +3,6 @@ using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Utility; namespace Content.Shared.Ninja.Components; @@ -14,68 +11,27 @@ namespace Content.Shared.Ninja.Components; /// Component for ninja suit abilities and power consumption. /// As an implementation detail, dashing with katana is a suit action which isn't ideal. /// </summary> -[RegisterComponent, NetworkedComponent, Access(typeof(SharedNinjaSuitSystem)), AutoGenerateComponentState] -[AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedNinjaSuitSystem))] public sealed partial class NinjaSuitComponent : Component { - /// <summary> - /// Battery charge used passively, in watts. Will last 1000 seconds on a small-capacity power cell. - /// </summary> - [DataField("passiveWattage")] - public float PassiveWattage = 0.36f; - - /// <summary> - /// Battery charge used while cloaked, stacks with passive. Will last 200 seconds while cloaked on a small-capacity power cell. - /// </summary> - [DataField("cloakWattage")] - public float CloakWattage = 1.44f; - /// <summary> /// Sound played when a ninja is hit while cloaked. /// </summary> - [DataField("revealSound")] + [DataField] public SoundSpecifier RevealSound = new SoundPathSpecifier("/Audio/Effects/chime.ogg"); /// <summary> - /// How long to disable all abilities when revealed. - /// Normally, ninjas are revealed when attacking or getting damaged. - /// </summary> - [DataField("disableTime")] - public TimeSpan DisableTime = TimeSpan.FromSeconds(5); - - /// <summary> - /// Time at which we will be able to use our abilities again + /// ID of the use delay to disable all ninja abilities. /// </summary> - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - [AutoPausedField] - public TimeSpan DisableCooldown; - - /// <summary> - /// The action id for creating throwing stars. - /// </summary> - [DataField("createThrowingStarAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string CreateThrowingStarAction = "ActionCreateThrowingStar"; - - [DataField, AutoNetworkedField] - public EntityUid? CreateThrowingStarActionEntity; - - /// <summary> - /// Battery charge used to create a throwing star. Can do it 25 times on a small-capacity power cell. - /// </summary> - [DataField("throwingStarCharge")] - public float ThrowingStarCharge = 14.4f; - - /// <summary> - /// Throwing star item to create with the action - /// </summary> - [DataField("throwingStarPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string ThrowingStarPrototype = "ThrowingStarNinja"; + [DataField] + public string DisableDelayId = "suit_powers"; /// <summary> /// The action id for recalling a bound energy katana /// </summary> - [DataField("recallKatanaAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string RecallKatanaAction = "ActionRecallKatana"; + [DataField] + public EntProtoId RecallKatanaAction = "ActionRecallKatana"; [DataField, AutoNetworkedField] public EntityUid? RecallKatanaActionEntity; @@ -84,14 +40,14 @@ public sealed partial class NinjaSuitComponent : Component /// Battery charge used per tile the katana teleported. /// Uses 1% of a default battery per tile. /// </summary> - [DataField("recallCharge")] + [DataField] public float RecallCharge = 3.6f; /// <summary> /// The action id for creating an EMP burst /// </summary> - [DataField("empAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] - public string EmpAction = "ActionNinjaEmp"; + [DataField] + public EntProtoId EmpAction = "ActionNinjaEmp"; [DataField, AutoNetworkedField] public EntityUid? EmpActionEntity; @@ -99,36 +55,29 @@ public sealed partial class NinjaSuitComponent : Component /// <summary> /// Battery charge used to create an EMP burst. Can do it 2 times on a small-capacity power cell. /// </summary> - [DataField("empCharge")] + [DataField] public float EmpCharge = 180f; + // TODO: EmpOnTrigger bruh /// <summary> /// Range of the EMP in tiles. /// </summary> - [DataField("empRange")] + [DataField] public float EmpRange = 6f; /// <summary> /// Power consumed from batteries by the EMP /// </summary> - [DataField("empConsumption")] + [DataField] public float EmpConsumption = 100000f; /// <summary> /// How long the EMP effects last for, in seconds /// </summary> - [DataField("empDuration")] + [DataField] public float EmpDuration = 60f; } -public sealed partial class CreateThrowingStarEvent : InstantActionEvent -{ -} - -public sealed partial class RecallKatanaEvent : InstantActionEvent -{ -} +public sealed partial class RecallKatanaEvent : InstantActionEvent; -public sealed partial class NinjaEmpEvent : InstantActionEvent -{ -} +public sealed partial class NinjaEmpEvent : InstantActionEvent; diff --git a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs index 91c816df5c9..a19537be1c8 100644 --- a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs +++ b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs @@ -7,34 +7,28 @@ namespace Content.Shared.Ninja.Components; /// <summary> /// Component placed on a mob to make it a space ninja, able to use suit and glove powers. -/// Contains ids of all ninja equipment and the game rule. +/// Contains ids of all ninja equipment. /// </summary> [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedSpaceNinjaSystem))] public sealed partial class SpaceNinjaComponent : Component { - /// <summary> - /// The ninja game rule that spawned this ninja. - /// </summary> - [DataField("rule")] - public EntityUid? Rule; - /// <summary> /// Currently worn suit /// </summary> - [DataField("suit"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? Suit; /// <summary> - /// Currently worn gloves + /// Currently worn gloves, if enabled. /// </summary> - [DataField("gloves"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? Gloves; /// <summary> /// Bound katana, set once picked up and never removed /// </summary> - [DataField("katana"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? Katana; /// <summary> @@ -55,6 +49,9 @@ public sealed partial class SpaceNinjaComponent : Component [DataField] public EntProtoId SpiderChargeObjective = "SpiderChargeObjective"; + /// <summary> + /// Alert to show for suit power. + /// </summary> [DataField] public ProtoId<AlertPrototype> SuitPowerAlert = "SuitPower"; } diff --git a/Content.Shared/Ninja/Components/SpiderChargeComponent.cs b/Content.Shared/Ninja/Components/SpiderChargeComponent.cs index dacf47bb235..3ba4494cca4 100644 --- a/Content.Shared/Ninja/Components/SpiderChargeComponent.cs +++ b/Content.Shared/Ninja/Components/SpiderChargeComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Ninja.Systems; using Robust.Shared.GameStates; namespace Content.Shared.Ninja.Components; @@ -6,14 +7,14 @@ namespace Content.Shared.Ninja.Components; /// Component for the Space Ninja's unique Spider Charge. /// Only this component detonating can trigger the ninja's objective. /// </summary> -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, Access(typeof(SharedSpiderChargeSystem))] public sealed partial class SpiderChargeComponent : Component { /// Range for planting within the target area - [DataField("range")] + [DataField] public float Range = 10f; /// The ninja that planted this charge - [DataField("planter")] - public EntityUid? Planter = null; + [DataField] + public EntityUid? Planter; } diff --git a/Content.Shared/Ninja/Components/StunProviderComponent.cs b/Content.Shared/Ninja/Components/StunProviderComponent.cs index 37a27074a49..2da094291d7 100644 --- a/Content.Shared/Ninja/Components/StunProviderComponent.cs +++ b/Content.Shared/Ninja/Components/StunProviderComponent.cs @@ -3,7 +3,6 @@ using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Ninja.Components; @@ -11,32 +10,33 @@ namespace Content.Shared.Ninja.Components; /// Component for stunning mobs on click outside of harm mode. /// Knocks them down for a bit and deals shock damage. /// </summary> -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunProviderSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedStunProviderSystem))] public sealed partial class StunProviderComponent : Component { /// <summary> /// The powercell entity to take power from. /// Determines whether stunning is possible. /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? BatteryUid; /// <summary> /// Sound played when stunning someone. /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier Sound = new SoundCollectionSpecifier("sparks"); /// <summary> /// Joules required in the battery to stun someone. Defaults to 10 uses on a small battery. /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float StunCharge = 36f; /// <summary> /// Damage dealt when stunning someone /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public DamageSpecifier StunDamage = new() { DamageDict = new() @@ -48,34 +48,30 @@ public sealed partial class StunProviderComponent : Component /// <summary> /// Time that someone is stunned for, stacks if done multiple times. /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan StunTime = TimeSpan.FromSeconds(5); /// <summary> /// How long stunning is disabled after stunning something. /// </summary> - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan Cooldown = TimeSpan.FromSeconds(2); /// <summary> - /// Locale string to popup when there is no power + /// ID of the cooldown use delay. /// </summary> - [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] - public string NoPowerPopup = string.Empty; + [DataField] + public string DelayId = "stun_cooldown"; /// <summary> - /// Whitelist for what counts as a mob. + /// Locale string to popup when there is no power /// </summary> - [DataField] - public EntityWhitelist Whitelist = new() - { - Components = new[] {"Stamina"} - }; + [DataField(required: true)] + public LocId NoPowerPopup = string.Empty; /// <summary> - /// When someone can next be stunned. - /// Essentially a UseDelay unique to this component. + /// Whitelist for what counts as a mob. /// </summary> - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - public TimeSpan NextStun = TimeSpan.Zero; + [DataField(required: true)] + public EntityWhitelist Whitelist = new(); } diff --git a/Content.Shared/Ninja/Systems/DashAbilitySystem.cs b/Content.Shared/Ninja/Systems/DashAbilitySystem.cs index 4853968b61f..1385219e473 100644 --- a/Content.Shared/Ninja/Systems/DashAbilitySystem.cs +++ b/Content.Shared/Ninja/Systems/DashAbilitySystem.cs @@ -16,6 +16,7 @@ namespace Content.Shared.Ninja.Systems; /// </summary> public sealed class DashAbilitySystem : EntitySystem { + [Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedChargesSystem _charges = default!; @@ -23,48 +24,40 @@ public sealed class DashAbilitySystem : EntitySystem [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly ActionContainerSystem _actionContainer = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<DashAbilityComponent, GetItemActionsEvent>(OnGetItemActions); + SubscribeLocalEvent<DashAbilityComponent, GetItemActionsEvent>(OnGetActions); SubscribeLocalEvent<DashAbilityComponent, DashEvent>(OnDash); SubscribeLocalEvent<DashAbilityComponent, MapInitEvent>(OnMapInit); } - private void OnMapInit(EntityUid uid, DashAbilityComponent component, MapInitEvent args) + private void OnMapInit(Entity<DashAbilityComponent> ent, ref MapInitEvent args) { - _actionContainer.EnsureAction(uid, ref component.DashActionEntity, component.DashAction); - Dirty(uid, component); + var (uid, comp) = ent; + _actionContainer.EnsureAction(uid, ref comp.DashActionEntity, comp.DashAction); + Dirty(uid, comp); } - private void OnGetItemActions(EntityUid uid, DashAbilityComponent comp, GetItemActionsEvent args) + private void OnGetActions(Entity<DashAbilityComponent> ent, ref GetItemActionsEvent args) { - var ev = new AddDashActionEvent(args.User); - RaiseLocalEvent(uid, ev); - - if (ev.Cancelled) - return; - - args.AddAction(ref comp.DashActionEntity, comp.DashAction); + if (CheckDash(ent, args.User)) + args.AddAction(ent.Comp.DashActionEntity); } /// <summary> /// Handle charges and teleport to a visible location. /// </summary> - private void OnDash(EntityUid uid, DashAbilityComponent comp, DashEvent args) + private void OnDash(Entity<DashAbilityComponent> ent, ref DashEvent args) { if (!_timing.IsFirstTimePredicted) return; + var (uid, comp) = ent; var user = args.Performer; - args.Handled = true; - - var ev = new DashAttemptEvent(user); - RaiseLocalEvent(uid, ev); - if (ev.Cancelled) + if (!CheckDash(uid, user)) return; if (!_hands.IsHolding(user, uid, out var _)) @@ -73,15 +66,8 @@ private void OnDash(EntityUid uid, DashAbilityComponent comp, DashEvent args) return; } - TryComp<LimitedChargesComponent>(uid, out var charges); - if (_charges.IsEmpty(uid, charges)) - { - _popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user); - return; - } var origin = _transform.GetMapCoordinates(user); var target = args.Target.ToMap(EntityManager, _transform); - // prevent collision with the user duh if (!_examine.InRangeUnOccluded(origin, target, SharedInteractionSystem.MaxRaycastRange, null)) { // can only dash if the destination is visible on screen @@ -89,36 +75,28 @@ private void OnDash(EntityUid uid, DashAbilityComponent comp, DashEvent args) return; } - _transform.SetCoordinates(user, args.Target); - _transform.AttachToGridOrMap(user); - _audio.PlayPredicted(comp.BlinkSound, user, user); - if (charges != null) - _charges.UseCharge(uid, charges); - } -} + if (!_charges.TryUseCharge(uid)) + { + _popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user); + return; + } -/// <summary> -/// Raised on the item before adding the dash action -/// </summary> -public sealed class AddDashActionEvent : CancellableEntityEventArgs -{ - public EntityUid User; + var xform = Transform(user); + _transform.SetCoordinates(user, xform, args.Target); + _transform.AttachToGridOrMap(user, xform); + args.Handled = true; + } - public AddDashActionEvent(EntityUid user) + public bool CheckDash(EntityUid uid, EntityUid user) { - User = user; + var ev = new CheckDashEvent(user); + RaiseLocalEvent(uid, ref ev); + return !ev.Cancelled; } } /// <summary> -/// Raised on the item before dashing is done. +/// Raised on the item before adding the dash action and when using the action. /// </summary> -public sealed class DashAttemptEvent : CancellableEntityEventArgs -{ - public EntityUid User; - - public DashAttemptEvent(EntityUid user) - { - User = user; - } -} +[ByRefEvent] +public record struct CheckDashEvent(EntityUid User, bool Cancelled = false); diff --git a/Content.Shared/Ninja/Systems/EmagProviderSystem.cs b/Content.Shared/Ninja/Systems/EmagProviderSystem.cs index df9cf8ac825..aa6c975412f 100644 --- a/Content.Shared/Ninja/Systems/EmagProviderSystem.cs +++ b/Content.Shared/Ninja/Systems/EmagProviderSystem.cs @@ -1,6 +1,6 @@ using Content.Shared.Administration.Logs; -using Content.Shared.Emag.Systems; using Content.Shared.Database; +using Content.Shared.Emag.Systems; using Content.Shared.Interaction; using Content.Shared.Ninja.Components; using Content.Shared.Tag; @@ -14,9 +14,11 @@ namespace Content.Shared.Ninja.Systems; public sealed class EmagProviderSystem : EntitySystem { [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; - [Dependency] private readonly TagSystem _tags = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -28,18 +30,20 @@ public override void Initialize() /// <summary> /// Emag clicked entities that are on the whitelist. /// </summary> - private void OnBeforeInteractHand(EntityUid uid, EmagProviderComponent comp, BeforeInteractHandEvent args) + private void OnBeforeInteractHand(Entity<EmagProviderComponent> ent, ref BeforeInteractHandEvent args) { // TODO: change this into a generic check event thing - if (args.Handled || !_gloves.AbilityCheck(uid, args, out var target)) + if (args.Handled || !_gloves.AbilityCheck(ent, args, out var target)) return; + var (uid, comp) = ent; + // only allowed to emag entities on the whitelist - if (comp.Whitelist != null && !comp.Whitelist.IsValid(target, EntityManager)) + if (_whitelist.IsWhitelistFail(comp.Whitelist, target)) return; // only allowed to emag non-immune entities - if (_tags.HasTag(target, comp.EmagImmuneTag)) + if (_tag.HasTag(target, comp.EmagImmuneTag)) return; var handled = _emag.DoEmagEffect(uid, target); @@ -51,18 +55,6 @@ private void OnBeforeInteractHand(EntityUid uid, EmagProviderComponent comp, Bef RaiseLocalEvent(uid, ref ev); args.Handled = true; } - - /// <summary> - /// Set the whitelist for emagging something outside of yaml. - /// </summary> - public void SetWhitelist(EntityUid uid, EntityWhitelist? whitelist, EmagProviderComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.Whitelist = whitelist; - Dirty(uid, comp); - } } /// <summary> diff --git a/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs b/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs index d427ffa39b4..281b97a648a 100644 --- a/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs +++ b/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs @@ -15,33 +15,20 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<EnergyKatanaComponent, GotEquippedEvent>(OnEquipped); - SubscribeLocalEvent<EnergyKatanaComponent, AddDashActionEvent>(OnAddDashAction); - SubscribeLocalEvent<EnergyKatanaComponent, DashAttemptEvent>(OnDashAttempt); + SubscribeLocalEvent<EnergyKatanaComponent, CheckDashEvent>(OnCheckDash); } /// <summary> /// When equipped by a ninja, try to bind it. /// </summary> - private void OnEquipped(EntityUid uid, EnergyKatanaComponent comp, GotEquippedEvent args) + private void OnEquipped(Entity<EnergyKatanaComponent> ent, ref GotEquippedEvent args) { - // check if user isnt a ninja or already has a katana bound - var user = args.Equipee; - if (!TryComp<SpaceNinjaComponent>(user, out var ninja) || ninja.Katana != null) - return; - - // bind it since its unbound - _ninja.BindKatana(user, uid, ninja); - } - - private void OnAddDashAction(EntityUid uid, EnergyKatanaComponent comp, AddDashActionEvent args) - { - if (!HasComp<SpaceNinjaComponent>(args.User)) - args.Cancel(); + _ninja.BindKatana(args.Equipee, ent); } - private void OnDashAttempt(EntityUid uid, EnergyKatanaComponent comp, DashAttemptEvent args) + private void OnCheckDash(Entity<EnergyKatanaComponent> ent, ref CheckDashEvent args) { - if (!TryComp<SpaceNinjaComponent>(args.User, out var ninja) || ninja.Katana != uid) - args.Cancel(); + if (!_ninja.IsNinja(args.User)) + args.Cancelled = true; } } diff --git a/Content.Shared/Ninja/Systems/ItemCreatorSystem.cs b/Content.Shared/Ninja/Systems/ItemCreatorSystem.cs new file mode 100644 index 00000000000..56112e9a697 --- /dev/null +++ b/Content.Shared/Ninja/Systems/ItemCreatorSystem.cs @@ -0,0 +1,56 @@ +using Content.Shared.Actions; +using Content.Shared.Ninja.Components; + +namespace Content.Shared.Ninja.Systems; + +/// <summary> +/// Handles predicting that the action exists, creating items is done serverside. +/// </summary> +public abstract class SharedItemCreatorSystem : EntitySystem +{ + [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<ItemCreatorComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<ItemCreatorComponent, GetItemActionsEvent>(OnGetActions); + } + + private void OnMapInit(Entity<ItemCreatorComponent> ent, ref MapInitEvent args) + { + var (uid, comp) = ent; + // test funny dont mind me + if (string.IsNullOrEmpty(comp.Action)) + return; + + _actionContainer.EnsureAction(uid, ref comp.ActionEntity, comp.Action); + Dirty(uid, comp); + } + + private void OnGetActions(Entity<ItemCreatorComponent> ent, ref GetItemActionsEvent args) + { + if (CheckItemCreator(ent, args.User)) + args.AddAction(ent.Comp.ActionEntity); + } + + public bool CheckItemCreator(EntityUid uid, EntityUid user) + { + var ev = new CheckItemCreatorEvent(user); + RaiseLocalEvent(uid, ref ev); + return !ev.Cancelled; + } +} + +/// <summary> +/// Raised on the item creator before adding the action. +/// </summary> +[ByRefEvent] +public record struct CheckItemCreatorEvent(EntityUid User, bool Cancelled = false); + +/// <summary> +/// Raised on the item creator before creating an item. +/// </summary> +[ByRefEvent] +public record struct CreateItemAttemptEvent(EntityUid User, bool Cancelled = false); diff --git a/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs b/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs index ac11063eb71..0abcca7d1bd 100644 --- a/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs +++ b/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs @@ -18,34 +18,32 @@ public override void Initialize() } /// <summary> - /// Cancel any drain doafters if the battery is removed or gets filled. + /// Cancel any drain doafters if the battery is removed or, on the server, gets filled. /// </summary> - protected virtual void OnDoAfterAttempt(EntityUid uid, BatteryDrainerComponent comp, DoAfterAttemptEvent<DrainDoAfterEvent> args) + protected virtual void OnDoAfterAttempt(Entity<BatteryDrainerComponent> ent, ref DoAfterAttemptEvent<DrainDoAfterEvent> args) { - if (comp.BatteryUid == null) - { + if (ent.Comp.BatteryUid == null) args.Cancel(); - } } /// <summary> /// Drain power from a power source (on server) and repeat if it succeeded. /// Client will predict always succeeding since power is serverside. /// </summary> - private void OnDoAfter(EntityUid uid, BatteryDrainerComponent comp, DrainDoAfterEvent args) + private void OnDoAfter(Entity<BatteryDrainerComponent> ent, ref DrainDoAfterEvent args) { - if (args.Cancelled || args.Handled || args.Target == null) + if (args.Cancelled || args.Handled || args.Target is not {} target) return; // repeat if there is still power to drain - args.Repeat = TryDrainPower(uid, comp, args.Target.Value); + args.Repeat = TryDrainPower(ent, target); } /// <summary> /// Attempt to drain as much power as possible into the powercell. /// Client always predicts this as succeeding since power is serverside and it can only fail once, when the powercell is filled or the target is emptied. /// </summary> - protected virtual bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp, EntityUid target) + protected virtual bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target) { return true; } @@ -53,12 +51,13 @@ protected virtual bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp /// <summary> /// Sets the battery field on the drainer. /// </summary> - public void SetBattery(EntityUid uid, EntityUid? battery, BatteryDrainerComponent? comp = null) + public void SetBattery(Entity<BatteryDrainerComponent?> ent, EntityUid? battery) { - if (!Resolve(uid, ref comp)) + if (!Resolve(ent, ref ent.Comp) || ent.Comp.BatteryUid == battery) return; - comp.BatteryUid = battery; + ent.Comp.BatteryUid = battery; + Dirty(ent, ent.Comp); } } @@ -66,4 +65,4 @@ public void SetBattery(EntityUid uid, EntityUid? battery, BatteryDrainerComponen /// DoAfter event for <see cref="BatteryDrainerComponent"/>. /// </summary> [Serializable, NetSerializable] -public sealed partial class DrainDoAfterEvent : SimpleDoAfterEvent { } +public sealed partial class DrainDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs b/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs index f61d0c6a908..8b892190b7b 100644 --- a/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs +++ b/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs @@ -1,15 +1,13 @@ -using Content.Shared.Actions; +using Content.Shared.Clothing.Components; using Content.Shared.CombatMode; -using Content.Shared.Communications; -using Content.Shared.CriminalRecords.Components; using Content.Shared.Examine; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Inventory.Events; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Ninja.Components; using Content.Shared.Popups; -using Content.Shared.Research.Components; -using Content.Shared.Toggleable; using Robust.Shared.Timing; namespace Content.Shared.Ninja.Systems; @@ -20,85 +18,105 @@ namespace Content.Shared.Ninja.Systems; public abstract class SharedNinjaGlovesSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; - [Dependency] protected readonly SharedInteractionSystem Interaction = default!; - [Dependency] protected readonly SharedPopupSystem Popup = default!; - [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<NinjaGlovesComponent, GetItemActionsEvent>(OnGetItemActions); + SubscribeLocalEvent<NinjaGlovesComponent, ToggleClothingCheckEvent>(OnToggleCheck); + SubscribeLocalEvent<NinjaGlovesComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt); + SubscribeLocalEvent<NinjaGlovesComponent, ItemToggledEvent>(OnToggled); SubscribeLocalEvent<NinjaGlovesComponent, ExaminedEvent>(OnExamined); - SubscribeLocalEvent<NinjaGlovesComponent, GotUnequippedEvent>(OnUnequipped); - SubscribeLocalEvent<NinjaGlovesComponent, MapInitEvent>(OnMapInit); - } - - private void OnMapInit(EntityUid uid, NinjaGlovesComponent component, MapInitEvent args) - { - _actionContainer.EnsureAction(uid, ref component.ToggleActionEntity, component.ToggleAction); - Dirty(uid, component); } /// <summary> /// Disable glove abilities and show the popup if they were enabled previously. /// </summary> - public void DisableGloves(EntityUid uid, NinjaGlovesComponent? comp = null) + private void DisableGloves(Entity<NinjaGlovesComponent> ent) { + var (uid, comp) = ent; + // already disabled? - if (!Resolve(uid, ref comp) || comp.User == null) + if (comp.User is not {} user) return; - var user = comp.User.Value; comp.User = null; Dirty(uid, comp); - Appearance.SetData(uid, ToggleVisuals.Toggled, false); - Popup.PopupClient(Loc.GetString("ninja-gloves-off"), user, user); - - RemComp<BatteryDrainerComponent>(user); - RemComp<EmagProviderComponent>(user); - RemComp<StunProviderComponent>(user); - RemComp<ResearchStealerComponent>(user); - RemComp<CommsHackerComponent>(user); - RemComp<CriminalRecordsHackerComponent>(user); + foreach (var ability in comp.Abilities) + { + EntityManager.RemoveComponents(user, ability.Components); + } } /// <summary> - /// Adds the toggle action when equipped. + /// Adds the toggle action when equipped by a ninja only. /// </summary> - private void OnGetItemActions(EntityUid uid, NinjaGlovesComponent comp, GetItemActionsEvent args) + private void OnToggleCheck(Entity<NinjaGlovesComponent> ent, ref ToggleClothingCheckEvent args) { - if (HasComp<SpaceNinjaComponent>(args.User)) - args.AddAction(ref comp.ToggleActionEntity, comp.ToggleAction); + if (!_ninja.IsNinja(args.User)) + args.Cancelled = true; } /// <summary> /// Show if the gloves are enabled when examining. /// </summary> - private void OnExamined(EntityUid uid, NinjaGlovesComponent comp, ExaminedEvent args) + private void OnExamined(Entity<NinjaGlovesComponent> ent, ref ExaminedEvent args) { if (!args.IsInDetailsRange) return; - args.PushText(Loc.GetString(comp.User != null ? "ninja-gloves-examine-on" : "ninja-gloves-examine-off")); + var on = _toggle.IsActivated(ent.Owner) ? "on" : "off"; + args.PushText(Loc.GetString($"ninja-gloves-examine-{on}")); } - /// <summary> - /// Disable gloves when unequipped and clean up ninja's gloves reference - /// </summary> - private void OnUnequipped(EntityUid uid, NinjaGlovesComponent comp, GotUnequippedEvent args) + private void OnActivateAttempt(Entity<NinjaGlovesComponent> ent, ref ItemToggleActivateAttemptEvent args) { - if (comp.User != null) + if (args.User is not {} user + || !_ninja.NinjaQuery.TryComp(user, out var ninja) + // need to wear suit to enable gloves + || !HasComp<NinjaSuitComponent>(ninja.Suit)) { - var user = comp.User.Value; - Popup.PopupClient(Loc.GetString("ninja-gloves-off"), user, user); - DisableGloves(uid, comp); + args.Cancelled = true; + args.Popup = Loc.GetString("ninja-gloves-not-wearing-suit"); + return; } } + private void OnToggled(Entity<NinjaGlovesComponent> ent, ref ItemToggledEvent args) + { + if ((args.User ?? ent.Comp.User) is not {} user) + return; + + var message = Loc.GetString(args.Activated ? "ninja-gloves-on" : "ninja-gloves-off"); + _popup.PopupClient(message, user, user); + + if (args.Activated && _ninja.NinjaQuery.TryComp(user, out var ninja)) + EnableGloves(ent, (user, ninja)); + else + DisableGloves(ent); + } + + protected virtual void EnableGloves(Entity<NinjaGlovesComponent> ent, Entity<SpaceNinjaComponent> user) + { + var (uid, comp) = ent; + comp.User = user; + Dirty(uid, comp); + _ninja.AssignGloves(user, uid); + + // yeah this is just ComponentToggler but with objective checking + foreach (var ability in comp.Abilities) + { + // can't predict the objective related abilities + if (ability.Objective == null) + EntityManager.AddComponents(user, ability.Components); + } + } // TODO: generic event thing /// <summary> @@ -112,6 +130,6 @@ public bool AbilityCheck(EntityUid uid, BeforeInteractHandEvent args, out Entity && !_combatMode.IsInCombatMode(uid) && TryComp<HandsComponent>(uid, out var hands) && hands.ActiveHandEntity == null - && Interaction.InRangeUnobstructed(uid, target); + && _interaction.InRangeUnobstructed(uid, target); } } diff --git a/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs b/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs index fed41eaed8a..3800d15b267 100644 --- a/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs +++ b/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs @@ -1,11 +1,14 @@ using Content.Shared.Actions; +using Content.Shared.Clothing; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.Inventory.Events; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Ninja.Components; using Content.Shared.Popups; +using Content.Shared.Timing; using Robust.Shared.Audio.Systems; -using Robust.Shared.Timing; namespace Content.Shared.Ninja.Systems; @@ -14,137 +17,158 @@ namespace Content.Shared.Ninja.Systems; /// </summary> public abstract class SharedNinjaSuitSystem : EntitySystem { - [Dependency] protected readonly IGameTiming GameTiming = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; - [Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; - [Dependency] protected readonly StealthClothingSystem StealthClothing = default!; + [Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent<NinjaSuitComponent, MapInitEvent>(OnMapInit); - - SubscribeLocalEvent<NinjaSuitComponent, GotEquippedEvent>(OnEquipped); + SubscribeLocalEvent<NinjaSuitComponent, ClothingGotEquippedEvent>(OnEquipped); SubscribeLocalEvent<NinjaSuitComponent, GetItemActionsEvent>(OnGetItemActions); - SubscribeLocalEvent<NinjaSuitComponent, AddStealthActionEvent>(OnAddStealthAction); + SubscribeLocalEvent<NinjaSuitComponent, ToggleClothingCheckEvent>(OnCloakCheck); + SubscribeLocalEvent<NinjaSuitComponent, CheckItemCreatorEvent>(OnStarCheck); + SubscribeLocalEvent<NinjaSuitComponent, CreateItemAttemptEvent>(OnCreateStarAttempt); + SubscribeLocalEvent<NinjaSuitComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt); SubscribeLocalEvent<NinjaSuitComponent, GotUnequippedEvent>(OnUnequipped); } - private void OnMapInit(EntityUid uid, NinjaSuitComponent component, MapInitEvent args) + private void OnEquipped(Entity<NinjaSuitComponent> ent, ref ClothingGotEquippedEvent args) { - _actionContainer.EnsureAction(uid, ref component.RecallKatanaActionEntity, component.RecallKatanaAction); - _actionContainer.EnsureAction(uid, ref component.CreateThrowingStarActionEntity, component.CreateThrowingStarAction); - _actionContainer.EnsureAction(uid, ref component.EmpActionEntity, component.EmpAction); - Dirty(uid, component); + var user = args.Wearer; + if (_ninja.NinjaQuery.TryComp(user, out var ninja)) + NinjaEquipped(ent, (user, ninja)); } - /// <summary> - /// Call the shared and serverside code for when a ninja equips the suit. - /// </summary> - private void OnEquipped(EntityUid uid, NinjaSuitComponent comp, GotEquippedEvent args) + protected virtual void NinjaEquipped(Entity<NinjaSuitComponent> ent, Entity<SpaceNinjaComponent> user) { - var user = args.Equipee; - if (!TryComp<SpaceNinjaComponent>(user, out var ninja)) - return; + // mark the user as wearing this suit, used when being attacked among other things + _ninja.AssignSuit(user, ent); + } - NinjaEquippedSuit(uid, comp, user, ninja); + private void OnMapInit(Entity<NinjaSuitComponent> ent, ref MapInitEvent args) + { + var (uid, comp) = ent; + _actionContainer.EnsureAction(uid, ref comp.RecallKatanaActionEntity, comp.RecallKatanaAction); + _actionContainer.EnsureAction(uid, ref comp.EmpActionEntity, comp.EmpAction); + Dirty(uid, comp); } /// <summary> /// Add all the actions when a suit is equipped by a ninja. /// </summary> - private void OnGetItemActions(EntityUid uid, NinjaSuitComponent comp, GetItemActionsEvent args) + private void OnGetItemActions(Entity<NinjaSuitComponent> ent, ref GetItemActionsEvent args) { - if (!HasComp<SpaceNinjaComponent>(args.User)) + if (!_ninja.IsNinja(args.User)) return; + var comp = ent.Comp; args.AddAction(ref comp.RecallKatanaActionEntity, comp.RecallKatanaAction); - args.AddAction(ref comp.CreateThrowingStarActionEntity, comp.CreateThrowingStarAction); args.AddAction(ref comp.EmpActionEntity, comp.EmpAction); } /// <summary> - /// Only add stealth clothing's toggle action when equipped by a ninja. + /// Only add toggle cloak action when equipped by a ninja. /// </summary> - private void OnAddStealthAction(EntityUid uid, NinjaSuitComponent comp, AddStealthActionEvent args) + private void OnCloakCheck(Entity<NinjaSuitComponent> ent, ref ToggleClothingCheckEvent args) { - if (!HasComp<SpaceNinjaComponent>(args.User)) - args.Cancel(); + if (!_ninja.IsNinja(args.User)) + args.Cancelled = true; } - /// <summary> - /// Call the shared and serverside code for when anyone unequips a suit. - /// </summary> - private void OnUnequipped(EntityUid uid, NinjaSuitComponent comp, GotUnequippedEvent args) + private void OnStarCheck(Entity<NinjaSuitComponent> ent, ref CheckItemCreatorEvent args) + { + if (!_ninja.IsNinja(args.User)) + args.Cancelled = true; + } + + private void OnCreateStarAttempt(Entity<NinjaSuitComponent> ent, ref CreateItemAttemptEvent args) { - UserUnequippedSuit(uid, comp, args.Equipee); + if (CheckDisabled(ent, args.User)) + args.Cancelled = true; } /// <summary> - /// Called when a suit is equipped by a space ninja. - /// In the future it might be changed to an explicit activation toggle/verb like gloves are. + /// Call the shared and serverside code for when anyone unequips a suit. /// </summary> - protected virtual void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, SpaceNinjaComponent ninja) + private void OnUnequipped(Entity<NinjaSuitComponent> ent, ref GotUnequippedEvent args) { - // mark the user as wearing this suit, used when being attacked among other things - _ninja.AssignSuit(user, uid, ninja); - - // initialize phase cloak, but keep it off - StealthClothing.SetEnabled(uid, user, false); + var user = args.Equipee; + if (_ninja.NinjaQuery.TryComp(user, out var ninja)) + UserUnequippedSuit(ent, (user, ninja)); } /// <summary> /// Force uncloaks the user and disables suit abilities. /// </summary> - public void RevealNinja(EntityUid uid, EntityUid user, bool disable = true, NinjaSuitComponent? comp = null, StealthClothingComponent? stealthClothing = null) + public void RevealNinja(Entity<NinjaSuitComponent?> ent, EntityUid user, bool disable = true) { - if (!Resolve(uid, ref comp, ref stealthClothing)) + if (!Resolve(ent, ref ent.Comp)) return; - if (!StealthClothing.SetEnabled(uid, user, false, stealthClothing)) - return; - - if (!disable) + var uid = ent.Owner; + var comp = ent.Comp; + if (_toggle.TryDeactivate(uid, user) || !disable) return; // previously cloaked, disable abilities for a short time _audio.PlayPredicted(comp.RevealSound, uid, user); Popup.PopupClient(Loc.GetString("ninja-revealed"), user, user, PopupType.MediumCaution); - comp.DisableCooldown = GameTiming.CurTime + comp.DisableTime; + _useDelay.TryResetDelay(uid, id: comp.DisableDelayId); + } + + private void OnActivateAttempt(Entity<NinjaSuitComponent> ent, ref ItemToggleActivateAttemptEvent args) + { + if (!_ninja.IsNinja(args.User)) + { + args.Cancelled = true; + return; + } + + if (IsDisabled((ent, ent.Comp, null))) + { + args.Cancelled = true; + args.Popup = Loc.GetString("ninja-suit-cooldown"); + } } - // TODO: modify PowerCellDrain /// <summary> - /// Returns the power used by a suit + /// Returns true if the suit is currently disabled /// </summary> - public float SuitWattage(EntityUid uid, NinjaSuitComponent? suit = null) + public bool IsDisabled(Entity<NinjaSuitComponent?, UseDelayComponent?> ent) + { + if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2)) + return false; + + return _useDelay.IsDelayed((ent, ent.Comp2), ent.Comp1.DisableDelayId); + } + + protected bool CheckDisabled(Entity<NinjaSuitComponent> ent, EntityUid user) { - if (!Resolve(uid, ref suit)) - return 0f; + if (IsDisabled((ent, ent.Comp, null))) + { + Popup.PopupEntity(Loc.GetString("ninja-suit-cooldown"), user, user, PopupType.Medium); + return true; + } - float wattage = suit.PassiveWattage; - if (TryComp<StealthClothingComponent>(uid, out var stealthClothing) && stealthClothing.Enabled) - wattage += suit.CloakWattage; - return wattage; + return false; } /// <summary> /// Called when a suit is unequipped, not necessarily by a space ninja. /// In the future it might be changed to also have explicit deactivation via toggle. /// </summary> - protected virtual void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user) + protected virtual void UserUnequippedSuit(Entity<NinjaSuitComponent> ent, Entity<SpaceNinjaComponent> user) { - if (!TryComp<SpaceNinjaComponent>(user, out var ninja)) - return; - // mark the user as not wearing a suit - _ninja.AssignSuit(user, null, ninja); + _ninja.AssignSuit(user, null); // disable glove abilities - if (ninja.Gloves != null && TryComp<NinjaGlovesComponent>(ninja.Gloves.Value, out var gloves)) - _gloves.DisableGloves(ninja.Gloves.Value, gloves); + if (user.Comp.Gloves is {} uid) + _toggle.TryDeactivate(uid, user: user); } } diff --git a/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs b/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs index 522f29fe420..d738f2dd8a2 100644 --- a/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs +++ b/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Popups; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Ninja.Systems; @@ -14,49 +15,59 @@ public abstract class SharedSpaceNinjaSystem : EntitySystem [Dependency] protected readonly SharedNinjaSuitSystem Suit = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; + public EntityQuery<SpaceNinjaComponent> NinjaQuery; + public override void Initialize() { base.Initialize(); + NinjaQuery = GetEntityQuery<SpaceNinjaComponent>(); + SubscribeLocalEvent<SpaceNinjaComponent, AttackedEvent>(OnNinjaAttacked); SubscribeLocalEvent<SpaceNinjaComponent, MeleeAttackEvent>(OnNinjaAttack); SubscribeLocalEvent<SpaceNinjaComponent, ShotAttemptedEvent>(OnShotAttempted); } + public bool IsNinja([NotNullWhen(true)] EntityUid? uid) + { + return NinjaQuery.HasComp(uid); + } + /// <summary> /// Set the ninja's worn suit entity /// </summary> - public void AssignSuit(EntityUid uid, EntityUid? suit, SpaceNinjaComponent? comp = null) + public void AssignSuit(Entity<SpaceNinjaComponent> ent, EntityUid? suit) { - if (!Resolve(uid, ref comp) || comp.Suit == suit) + if (ent.Comp.Suit == suit) return; - comp.Suit = suit; - Dirty(uid, comp); + ent.Comp.Suit = suit; + Dirty(ent, ent.Comp); } /// <summary> /// Set the ninja's worn gloves entity /// </summary> - public void AssignGloves(EntityUid uid, EntityUid? gloves, SpaceNinjaComponent? comp = null) + public void AssignGloves(Entity<SpaceNinjaComponent> ent, EntityUid? gloves) { - if (!Resolve(uid, ref comp) || comp.Gloves == gloves) + if (ent.Comp.Gloves == gloves) return; - comp.Gloves = gloves; - Dirty(uid, comp); + ent.Comp.Gloves = gloves; + Dirty(ent, ent.Comp); } /// <summary> /// Bind a katana entity to a ninja, letting it be recalled and dash. + /// Does nothing if the player is not a ninja or already has a katana bound. /// </summary> - public void BindKatana(EntityUid uid, EntityUid? katana, SpaceNinjaComponent? comp = null) + public void BindKatana(Entity<SpaceNinjaComponent?> ent, EntityUid katana) { - if (!Resolve(uid, ref comp) || comp.Katana == katana) + if (!NinjaQuery.Resolve(ent, ref ent.Comp) || ent.Comp.Katana != null) return; - comp.Katana = katana; - Dirty(uid, comp); + ent.Comp.Katana = katana; + Dirty(ent, ent.Comp); } /// <summary> @@ -71,32 +82,32 @@ public virtual bool TryUseCharge(EntityUid user, float charge) /// <summary> /// Handle revealing ninja if cloaked when attacked. /// </summary> - private void OnNinjaAttacked(EntityUid uid, SpaceNinjaComponent comp, AttackedEvent args) + private void OnNinjaAttacked(Entity<SpaceNinjaComponent> ent, ref AttackedEvent args) { - if (comp.Suit != null && TryComp<StealthClothingComponent>(comp.Suit, out var stealthClothing) && stealthClothing.Enabled) - { - Suit.RevealNinja(comp.Suit.Value, uid, true, null, stealthClothing); - } + TryRevealNinja(ent, disable: true); } /// <summary> /// Handle revealing ninja if cloaked when attacking. /// Only reveals, there is no cooldown. /// </summary> - private void OnNinjaAttack(EntityUid uid, SpaceNinjaComponent comp, ref MeleeAttackEvent args) + private void OnNinjaAttack(Entity<SpaceNinjaComponent> ent, ref MeleeAttackEvent args) + { + TryRevealNinja(ent, disable: false); + } + + private void TryRevealNinja(Entity<SpaceNinjaComponent> ent, bool disable) { - if (comp.Suit != null && TryComp<StealthClothingComponent>(comp.Suit, out var stealthClothing) && stealthClothing.Enabled) - { - Suit.RevealNinja(comp.Suit.Value, uid, false, null, stealthClothing); - } + if (ent.Comp.Suit is {} uid && TryComp<NinjaSuitComponent>(ent.Comp.Suit, out var suit)) + Suit.RevealNinja((uid, suit), ent, disable: disable); } /// <summary> /// Require ninja to fight with HONOR, no guns! /// </summary> - private void OnShotAttempted(EntityUid uid, SpaceNinjaComponent comp, ref ShotAttemptedEvent args) + private void OnShotAttempted(Entity<SpaceNinjaComponent> ent, ref ShotAttemptedEvent args) { - Popup.PopupClient(Loc.GetString("gun-disabled"), uid, uid); + Popup.PopupClient(Loc.GetString("gun-disabled"), ent, ent); args.Cancel(); } } diff --git a/Content.Shared/Ninja/Systems/SharedSpiderChargeSystem.cs b/Content.Shared/Ninja/Systems/SharedSpiderChargeSystem.cs new file mode 100644 index 00000000000..f4b158aced4 --- /dev/null +++ b/Content.Shared/Ninja/Systems/SharedSpiderChargeSystem.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Ninja.Systems; + +/// <summary> +/// Sticking triggering and exploding are all in server so this is just for access. +/// </summary> +public abstract class SharedSpiderChargeSystem : EntitySystem; diff --git a/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs b/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs index 61b6e4313ed..061c019c9b6 100644 --- a/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs +++ b/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs @@ -11,22 +11,12 @@ public abstract class SharedStunProviderSystem : EntitySystem /// <summary> /// Set the battery field on the stun provider. /// </summary> - public void SetBattery(EntityUid uid, EntityUid? battery, StunProviderComponent? comp = null) + public void SetBattery(Entity<StunProviderComponent?> ent, EntityUid? battery) { - if (!Resolve(uid, ref comp)) + if (!Resolve(ent, ref ent.Comp) || ent.Comp.BatteryUid == battery) return; - comp.BatteryUid = battery; - } - - /// <summary> - /// Set the no power popup field on the stun provider. - /// </summary> - public void SetNoPowerPopup(EntityUid uid, string popup, StunProviderComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.NoPowerPopup = popup; + ent.Comp.BatteryUid = battery; + Dirty(ent, ent.Comp); } } diff --git a/Content.Shared/NukeOps/NukeOperativeComponent.cs b/Content.Shared/NukeOps/NukeOperativeComponent.cs index d19f0ae3e9d..80416b8f710 100644 --- a/Content.Shared/NukeOps/NukeOperativeComponent.cs +++ b/Content.Shared/NukeOps/NukeOperativeComponent.cs @@ -17,6 +17,6 @@ public sealed partial class NukeOperativeComponent : Component /// <summary> /// /// </summary> - [DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))] + [DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<FactionIconPrototype>))] public string SyndStatusIcon = "SyndicateFaction"; } diff --git a/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs b/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs index 2708c823d2c..06c533e6460 100644 --- a/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs +++ b/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs @@ -35,10 +35,4 @@ public sealed partial class InfantComponent : Component [DataField("infantEndTime", customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan InfantEndTime; - - /// <summary> - /// The entity's name before the "baby" prefix is added. - /// </summary> - [DataField("originalName")] - public string OriginalName = string.Empty; } diff --git a/Content.Shared/Nutrition/Components/FoodMetamorphableByAddingComponent.cs b/Content.Shared/Nutrition/Components/FoodMetamorphableByAddingComponent.cs new file mode 100644 index 00000000000..01704041d07 --- /dev/null +++ b/Content.Shared/Nutrition/Components/FoodMetamorphableByAddingComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Nutrition.Components; + +/// <summary> +/// Attempts to metamorphose a modular food when a new ingredient is added. +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(SharedFoodSequenceSystem))] +public sealed partial class FoodMetamorphableByAddingComponent : Component +{ + /// <summary> + /// if true, the metamorphosis will only be attempted when the sequence ends, not when each element is added. + /// </summary> + [DataField] + public bool OnlyFinal = true; +} diff --git a/Content.Shared/Nutrition/Components/FoodSequenceElementComponent.cs b/Content.Shared/Nutrition/Components/FoodSequenceElementComponent.cs new file mode 100644 index 00000000000..50bc786129c --- /dev/null +++ b/Content.Shared/Nutrition/Components/FoodSequenceElementComponent.cs @@ -0,0 +1,25 @@ +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.Components; + +/// <summary> +/// Indicates that this entity can be inserted into FoodSequence, which will transfer all reagents to the target. +/// </summary> +[RegisterComponent, Access(typeof(SharedFoodSequenceSystem))] +public sealed partial class FoodSequenceElementComponent : Component +{ + /// <summary> + /// The same object can be used in different sequences, and it will have a different data in then. + /// </summary> + [DataField(required: true)] + public Dictionary<ProtoId<TagPrototype>, ProtoId<FoodSequenceElementPrototype>> Entries = new(); + + /// <summary> + /// Which solution we will add to the main dish + /// </summary> + [DataField] + public string Solution = "food"; +} diff --git a/Content.Shared/Nutrition/Components/FoodSequenceStartPointComponent.cs b/Content.Shared/Nutrition/Components/FoodSequenceStartPointComponent.cs new file mode 100644 index 00000000000..c87110287ad --- /dev/null +++ b/Content.Shared/Nutrition/Components/FoodSequenceStartPointComponent.cs @@ -0,0 +1,159 @@ +using System.Numerics; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.Tag; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Nutrition.Components; + +/// <summary> +/// A starting point for the creation of procedural food. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedFoodSequenceSystem))] +public sealed partial class FoodSequenceStartPointComponent : Component +{ + /// <summary> + /// A key that determines which types of food elements can be attached to a food. + /// </summary> + [DataField(required: true)] + public ProtoId<TagPrototype> Key = string.Empty; + + /// <summary> + /// The maximum number of layers of food that can be placed on this item. + /// </summary> + [DataField] + public int MaxLayers = 10; + + /// <summary> + /// Can we put more layers? + /// </summary> + [DataField] + public bool Finished; + + /// <summary> + /// solution where reagents will be added from newly added ingredients + /// </summary> + [DataField] + public string Solution = "food"; + + #region name generation + + /// <summary> + /// LocId with a name generation pattern. + /// </summary> + [DataField] + public LocId? NameGeneration; + + /// <summary> + /// the part of the name generation used in the pattern + /// </summary> + [DataField] + public LocId? NamePrefix; + + /// <summary> + /// content in the form of all added ingredients will be separated by these symbols + /// </summary> + [DataField] + public string? ContentSeparator; + + /// <summary> + /// the part of the name generation used in the pattern + /// </summary> + [DataField] + public LocId? NameSuffix; + + #endregion + + #region visual + + /// <summary> + /// list of sprite states to be displayed on this object. + /// </summary> + [DataField, AutoNetworkedField] + public List<FoodSequenceVisualLayer> FoodLayers = new(); + + /// <summary> + /// If true, the generative layers will be placed in reverse order. + /// </summary> + [DataField] + public bool InverseLayers; + + /// <summary> + /// target layer, where new layers will be added. This allows you to control the order of generative layers and static layers. + /// </summary> + [DataField] + public string TargetLayerMap = "foodSequenceLayers"; + + /// <summary> + /// Start shift from the center of the sprite where the first layer of food will be placed. + /// </summary> + [DataField] + public Vector2 StartPosition = Vector2.Zero; + + /// <summary> + /// Shift from the start position applied to each subsequent layer. + /// </summary> + [DataField] + public Vector2 Offset = Vector2.Zero; + + /// <summary> + /// each layer will get a random offset in the specified range + /// </summary> + [DataField] + public Vector2 MaxLayerOffset = Vector2.Zero; + + /// <summary> + /// each layer will get a random offset in the specified range + /// </summary> + [DataField] + public Vector2 MinLayerOffset = Vector2.Zero; + + [DataField] + public bool AllowHorizontalFlip = true; + + public HashSet<string> RevealedLayers = new(); + + #endregion +} + +/// <summary> +/// class that synchronizes with the client +/// Stores all the necessary information for rendering the FoodSequence element +/// </summary> +[DataRecord, Serializable, NetSerializable] +public record struct FoodSequenceVisualLayer +{ + /// <summary> + /// reference to the original prototype of the layer. Used to edit visual layers. + /// </summary> + public ProtoId<FoodSequenceElementPrototype> Proto; + + /// <summary> + /// Sprite rendered in sequence + /// </summary> + public SpriteSpecifier? Sprite { get; set; } = SpriteSpecifier.Invalid; + + /// <summary> + /// Relative size of the sprite displayed in FoodSequence + /// </summary> + public Vector2 Scale { get; set; } = Vector2.One; + + /// <summary> + /// The offset of a particular layer. Allows a little position randomization of each layer. + /// </summary> + public Vector2 LocalOffset { get; set; } = Vector2.Zero; + + public FoodSequenceVisualLayer(ProtoId<FoodSequenceElementPrototype> proto, + SpriteSpecifier? sprite, + Vector2 scale, + Vector2 offset) + { + Proto = proto; + Sprite = sprite; + Scale = scale; + LocalOffset = offset; + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs index 6535390d646..a435f32fca6 100644 --- a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs @@ -30,18 +30,18 @@ public sealed class HungerSystem : EntitySystem [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly IConfigurationManager _config = default!; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<SatiationIconPrototype>] private const string HungerIconOverfedId = "HungerIconOverfed"; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<SatiationIconPrototype>] private const string HungerIconPeckishId = "HungerIconPeckish"; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<SatiationIconPrototype>] private const string HungerIconStarvingId = "HungerIconStarving"; - private StatusIconPrototype? _hungerIconOverfed; - private StatusIconPrototype? _hungerIconPeckish; - private StatusIconPrototype? _hungerIconStarving; + private SatiationIconPrototype? _hungerIconOverfed; + private SatiationIconPrototype? _hungerIconPeckish; + private SatiationIconPrototype? _hungerIconStarving; public override void Initialize() { @@ -227,7 +227,7 @@ private bool GetMovementThreshold(HungerThreshold threshold) } } - public bool TryGetStatusIconPrototype(HungerComponent component, [NotNullWhen(true)] out StatusIconPrototype? prototype) + public bool TryGetStatusIconPrototype(HungerComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype) { switch (component.CurrentThreshold) { diff --git a/Content.Shared/Nutrition/EntitySystems/SharedFoodSequenceSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedFoodSequenceSystem.cs new file mode 100644 index 00000000000..2623e4a7162 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SharedFoodSequenceSystem.cs @@ -0,0 +1,5 @@ +namespace Content.Shared.Nutrition.EntitySystems; + +public abstract class SharedFoodSequenceSystem : EntitySystem +{ +} diff --git a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs index 4ff49e795c2..152d3ca3492 100644 --- a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs @@ -26,18 +26,18 @@ public sealed class ThirstSystem : EntitySystem [Dependency] private readonly SharedJetpackSystem _jetpack = default!; [Dependency] private readonly IConfigurationManager _config = default!; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<SatiationIconPrototype>] private const string ThirstIconOverhydratedId = "ThirstIconOverhydrated"; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<SatiationIconPrototype>] private const string ThirstIconThirstyId = "ThirstIconThirsty"; - [ValidatePrototypeId<StatusIconPrototype>] + [ValidatePrototypeId<SatiationIconPrototype>] private const string ThirstIconParchedId = "ThirstIconParched"; - private StatusIconPrototype? _thirstIconOverhydrated = null; - private StatusIconPrototype? _thirstIconThirsty = null; - private StatusIconPrototype? _thirstIconParched = null; + private SatiationIconPrototype? _thirstIconOverhydrated = null; + private SatiationIconPrototype? _thirstIconThirsty = null; + private SatiationIconPrototype? _thirstIconParched = null; public override void Initialize() { @@ -133,7 +133,7 @@ private bool IsMovementThreshold(ThirstThreshold threshold) } } - public bool TryGetStatusIconPrototype(ThirstComponent component, out StatusIconPrototype? prototype) + public bool TryGetStatusIconPrototype(ThirstComponent component, out SatiationIconPrototype? prototype) { switch (component.CurrentThirstThreshold) { diff --git a/Content.Shared/Nutrition/Events.cs b/Content.Shared/Nutrition/Events.cs index f2936d603d9..d15fcbbf1e3 100644 --- a/Content.Shared/Nutrition/Events.cs +++ b/Content.Shared/Nutrition/Events.cs @@ -1,5 +1,8 @@ using Content.Shared.Chemistry.Components; using Content.Shared.DoAfter; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Nutrition; @@ -59,3 +62,16 @@ public VapeDoAfterEvent(Solution solution, bool forced) /// </summary> [ByRefEvent] public record struct SliceFoodEvent(); + +/// <summary> +/// is called after a successful attempt at slicing food. +/// </summary> +[Serializable, NetSerializable] +public sealed partial class SliceFoodDoAfterEvent : SimpleDoAfterEvent +{ +} + +/// <summary> +/// Raised on FoodSequence start element entity when new ingredient is added to FoodSequence +/// </summary> +public record struct FoodSequenceIngredientAddedEvent(EntityUid Start, EntityUid Element, ProtoId<FoodSequenceElementPrototype> Proto, EntityUid? User = null); diff --git a/Content.Shared/Nutrition/FoodMetamorphRules/FoodMetamorphRule.cs b/Content.Shared/Nutrition/FoodMetamorphRules/FoodMetamorphRule.cs new file mode 100644 index 00000000000..9e59573ff85 --- /dev/null +++ b/Content.Shared/Nutrition/FoodMetamorphRules/FoodMetamorphRule.cs @@ -0,0 +1,218 @@ +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Destructible.Thresholds; +using Content.Shared.Nutrition.Components; +using Content.Shared.Tag; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Nutrition.FoodMetamorphRules; + +/// <summary> +/// abstract rules that are used to verify the correct foodSequence for recipe +/// </summary> +[ImplicitDataDefinitionForInheritors] +[Serializable, NetSerializable] +public abstract partial class FoodMetamorphRule +{ + public abstract bool Check(IPrototypeManager protoMan, EntityManager entMan, EntityUid food, List<FoodSequenceVisualLayer> ingredients); +} + +/// <summary> +/// The requirement that the sequence be within the specified size limit +/// </summary> +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class SequenceLength : FoodMetamorphRule +{ + [DataField(required: true)] + public MinMax Range; + + public override bool Check(IPrototypeManager protoMan, EntityManager entMan, EntityUid food, List<FoodSequenceVisualLayer> ingredients) + { + return ingredients.Count <= Range.Max && ingredients.Count >= Range.Min; + } +} + +/// <summary> +/// A requirement that the last element of the sequence have one or all of the required tags +/// </summary> +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class LastElementHasTags : FoodMetamorphRule +{ + [DataField(required: true)] + public List<ProtoId<TagPrototype>> Tags = new (); + + [DataField] + public bool NeedAll = true; + + public override bool Check(IPrototypeManager protoMan, EntityManager entMan, EntityUid food, List<FoodSequenceVisualLayer> ingredients) + { + var lastIngredient = ingredients[ingredients.Count - 1]; + + if (!protoMan.TryIndex(lastIngredient.Proto, out var protoIndexed)) + return false; + + foreach (var tag in Tags) + { + var containsTag = protoIndexed.Tags.Contains(tag); + + if (NeedAll && !containsTag) + { + return false; + } + + if (!NeedAll && containsTag) + { + return true; + } + } + + return NeedAll; + } +} + +/// <summary> +/// A requirement that the specified sequence element have one or all of the required tags +/// </summary> +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class ElementHasTags : FoodMetamorphRule +{ + [DataField(required: true)] + public int ElementNumber = 0; + + [DataField(required: true)] + public List<ProtoId<TagPrototype>> Tags = new (); + + [DataField] + public bool NeedAll = true; + + public override bool Check(IPrototypeManager protoMan, EntityManager entMan, EntityUid food, List<FoodSequenceVisualLayer> ingredients) + { + if (ingredients.Count < ElementNumber + 1) + return false; + + if (!protoMan.TryIndex(ingredients[ElementNumber].Proto, out var protoIndexed)) + return false; + + foreach (var tag in Tags) + { + var containsTag = protoIndexed.Tags.Contains(tag); + + if (NeedAll && !containsTag) + { + return false; + } + + if (!NeedAll && containsTag) + { + return true; + } + } + + return NeedAll; + } +} + +/// <summary> +/// requirement that the food contains certain reagents (e.g. sauces) +/// </summary> +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class FoodHasReagent : FoodMetamorphRule +{ + [DataField(required: true)] + public ProtoId<ReagentPrototype> Reagent = new(); + + [DataField(required: true)] + public MinMax Count; + + [DataField] + public string Solution = "food"; + + public override bool Check(IPrototypeManager protoMan, EntityManager entMan, EntityUid food, List<FoodSequenceVisualLayer> ingredients) + { + if (!entMan.TryGetComponent<SolutionContainerManagerComponent>(food, out var solMan)) + return false; + + var solutionMan = entMan.System<SharedSolutionContainerSystem>(); + + if (!solutionMan.TryGetSolution(food, Solution, out var foodSoln, out var foodSolution)) + return false; + + foreach (var (id, quantity) in foodSoln.Value.Comp.Solution.Contents) + { + if (id.Prototype != Reagent.Id) + continue; + + if (quantity < Count.Min || quantity > Count.Max) + break; + + return true; + } + + return false; + } +} + +/// <summary> +/// A requirement that there be X ingredients in the sequence that have one or all of the specified tags. +/// </summary> +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class IngredientsWithTags : FoodMetamorphRule +{ + [DataField(required: true)] + public List<ProtoId<TagPrototype>> Tags = new (); + + [DataField(required: true)] + public MinMax Count = new(); + + [DataField] + public bool NeedAll = true; + + public override bool Check(IPrototypeManager protoMan, EntityManager entMan, EntityUid food, List<FoodSequenceVisualLayer> ingredients) + { + var count = 0; + foreach (var ingredient in ingredients) + { + if (!protoMan.TryIndex(ingredient.Proto, out var protoIndexed)) + continue; + + var allowed = false; + if (NeedAll) + { + allowed = true; + foreach (var tag in Tags) + { + if (!protoIndexed.Tags.Contains(tag)) + { + allowed = false; + break; + } + } + } + else + { + allowed = false; + foreach (var tag in Tags) + { + if (protoIndexed.Tags.Contains(tag)) + { + allowed = true; + break; + } + } + } + + if (allowed) + count++; + } + + return count >= Count.Min && count <= Count.Max; + } +} diff --git a/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs b/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs new file mode 100644 index 00000000000..a3448715e4a --- /dev/null +++ b/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs @@ -0,0 +1,38 @@ +using Content.Shared.Tag; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Nutrition.Prototypes; + +/// <summary> +/// Unique data storage block for different FoodSequence layers +/// </summary> +[Prototype("foodSequenceElement")] +public sealed partial class FoodSequenceElementPrototype : IPrototype +{ + [IdDataField] public string ID { get; private set; } = default!; + + /// <summary> + /// sprite options. A random one will be selected and used to display the layer. + /// </summary> + [DataField] + public List<SpriteSpecifier> Sprites { get; private set; } = new(); + + /// <summary> + /// A localized name piece to build into the item name generator. + /// </summary> + [DataField] + public LocId? Name { get; private set; } + + /// <summary> + /// If the layer is the final one, it can be added over the limit, but no other layers can be added after it. + /// </summary> + [DataField] + public bool Final { get; private set; } + + /// <summary> + /// Tag list of this layer. Used for recipes for food metamorphosis. + /// </summary> + [DataField] + public List<ProtoId<TagPrototype>> Tags { get; set; } = new(); +} diff --git a/Content.Shared/Nutrition/Prototypes/MetamorphRecipePrototype.cs b/Content.Shared/Nutrition/Prototypes/MetamorphRecipePrototype.cs new file mode 100644 index 00000000000..977cb7a74d9 --- /dev/null +++ b/Content.Shared/Nutrition/Prototypes/MetamorphRecipePrototype.cs @@ -0,0 +1,32 @@ +using Content.Shared.Nutrition.FoodMetamorphRules; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.Prototypes; + +/// <summary> +/// Stores a recipe so that FoodSequence assembled in the right sequence can turn into a special meal. +/// </summary> +[Prototype] +public sealed partial class MetamorphRecipePrototype : IPrototype +{ + [IdDataField] public string ID { get; private set; } = default!; + + /// <summary> + /// The key of the FoodSequence being collected. For example “burger” “taco” etc. + /// </summary> + [DataField(required: true)] + public ProtoId<TagPrototype> Key = string.Empty; + + /// <summary> + /// The entity that will be created as a result of this recipe, and into which all the reagents will be transferred. + /// </summary> + [DataField(required: true)] + public EntProtoId Result = default!; + + /// <summary> + /// A sequence of rules that must be followed for FoodSequence to metamorphose into a special food. + /// </summary> + [DataField] + public List<FoodMetamorphRule> Rules = new(); +} diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs index 5f4e6184346..7dc85781170 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs @@ -163,8 +163,7 @@ protected void StartInsertDoAfter(EntityUid inserter, EntityUid toInsert, Entity var ev = new PseudoItemInsertDoAfterEvent(); var args = new DoAfterArgs(EntityManager, inserter, 5f, ev, toInsert, toInsert, storageEntity) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }; diff --git a/Content.Server/Nyanotrasen/NPC/Systems/Components/Faction/ClothingAddFactionComponent.cs b/Content.Shared/Nyanotrasen/NPC/Components/Faction/ClothingAddFactionComponent.cs similarity index 86% rename from Content.Server/Nyanotrasen/NPC/Systems/Components/Faction/ClothingAddFactionComponent.cs rename to Content.Shared/Nyanotrasen/NPC/Components/Faction/ClothingAddFactionComponent.cs index ceff6c582df..b113107aa6f 100644 --- a/Content.Server/Nyanotrasen/NPC/Systems/Components/Faction/ClothingAddFactionComponent.cs +++ b/Content.Shared/Nyanotrasen/NPC/Components/Faction/ClothingAddFactionComponent.cs @@ -1,6 +1,8 @@ +using Content.Shared.NPC.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.NPC.Components + +namespace Content.Shared.Nyanotrasen.NPC.Components.Faction { [RegisterComponent] /// <summary> diff --git a/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs b/Content.Shared/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs similarity index 91% rename from Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs rename to Content.Shared/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs index 3ce0ca73ebb..3b2502d1d51 100644 --- a/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs +++ b/Content.Shared/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs @@ -1,8 +1,9 @@ -using Content.Server.NPC.Components; +using Content.Shared.NPC.Components; -namespace Content.Server.NPC.Systems; -public partial class NpcFactionSystem : EntitySystem +namespace Content.Shared.NPC.Systems; + +public sealed partial class NpcFactionSystem { public void InitializeCore() { diff --git a/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs b/Content.Shared/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs similarity index 71% rename from Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs rename to Content.Shared/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs index 0b971c33946..62f3c33cf62 100644 --- a/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs +++ b/Content.Shared/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs @@ -1,15 +1,17 @@ -using Content.Server.NPC.Components; -using Content.Server.Store.Systems; using Content.Shared.Clothing.Components; using Content.Shared.Inventory.Events; +using Content.Shared.NPC.Components; +using Content.Shared.Nyanotrasen.NPC.Components.Faction; +using Content.Shared.Store; -namespace Content.Server.NPC.Systems; +namespace Content.Shared.NPC.Systems; -public partial class NpcFactionSystem : EntitySystem +public sealed partial class NpcFactionSystem { + public void InitializeItems() { - SubscribeLocalEvent<NpcFactionMemberComponent, ItemPurchasedEvent>(OnItemPurchased); + SubscribeLocalEvent<NpcFactionMemberComponent, StoreBuyFinishedEvent>(OnStoreBuyFinished); SubscribeLocalEvent<ClothingAddFactionComponent, GotEquippedEvent>(OnClothingEquipped); SubscribeLocalEvent<ClothingAddFactionComponent, GotUnequippedEvent>(OnClothingUnequipped); @@ -19,10 +21,12 @@ public void InitializeItems() /// If we bought something we probably don't want it to start biting us after it's automatically placed in our hands. /// If you do, consider finding a better solution to grenade penguin CBT. /// </summary> - private void OnItemPurchased(EntityUid uid, NpcFactionMemberComponent component, ref ItemPurchasedEvent args) - { - component.ExceptionalFriendlies.Add(args.Purchaser); - } + // TODO: check if this is the right uid?? we might be cooked. + private void OnStoreBuyFinished( + Entity<NpcFactionMemberComponent> entity, + ref StoreBuyFinishedEvent args + ) => + entity.Comp.ExceptionalFriendlies.Add(args.Buyer); private void OnClothingEquipped(EntityUid uid, ClothingAddFactionComponent component, GotEquippedEvent args) { diff --git a/Content.Shared/Nyanotrasen/ReverseEngineering/ReverseEngineeringComponent.cs b/Content.Shared/Nyanotrasen/ReverseEngineering/ReverseEngineeringComponent.cs index e928180e0ac..ac03cc7b36b 100644 --- a/Content.Shared/Nyanotrasen/ReverseEngineering/ReverseEngineeringComponent.cs +++ b/Content.Shared/Nyanotrasen/ReverseEngineering/ReverseEngineeringComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Research.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -14,7 +15,7 @@ public sealed partial class ReverseEngineeringComponent : Component /// Does not neccesarily line up with lathe recipes. /// </summary> [DataField("recipes")] - public List<string>? Recipes; + public List<ProtoId<LatheRecipePrototype>>? Recipes; /// <summary> /// Difficulty score 1-5 how hard this is to reverse engineer. diff --git a/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs b/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs index 07032a00ce9..8d2c4dcfebe 100644 --- a/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs +++ b/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs @@ -92,7 +92,7 @@ public bool CanBeAssigned(EntityUid uid, EntityUid mindId, MindComponent mind, O } /// <summary> - /// Get the title, description, icon and progress of an objective using <see cref="ObjectiveGetInfoEvent"/>. + /// Get the title, description, icon and progress of an objective using <see cref="ObjectiveGetProgressEvent"/>. /// If any of them are null it is logged and null is returned. /// </summary> /// <param name="uid"/>ID of the condition entity</param> @@ -103,20 +103,43 @@ public bool CanBeAssigned(EntityUid uid, EntityUid mindId, MindComponent mind, O if (!Resolve(mindId, ref mind)) return null; - var ev = new ObjectiveGetProgressEvent(mindId, mind); - RaiseLocalEvent(uid, ref ev); + if (GetProgress(uid, (mindId, mind)) is not {} progress) + return null; var comp = Comp<ObjectiveComponent>(uid); var meta = MetaData(uid); var title = meta.EntityName; var description = meta.EntityDescription; - if (comp.Icon == null || ev.Progress == null) + if (comp.Icon == null) { - Log.Error($"An objective {ToPrettyString(uid):objective} of {_mind.MindOwnerLoggingString(mind)} is missing icon or progress ({ev.Progress})"); + Log.Error($"An objective {ToPrettyString(uid):objective} of {_mind.MindOwnerLoggingString(mind)} is missing an icon!"); return null; } - return new ObjectiveInfo(title, description, comp.Icon, ev.Progress.Value); + return new ObjectiveInfo(title, description, comp.Icon, progress); + } + + /// <summary> + /// Gets the progress of an objective using <see cref="ObjectiveGetProgressEvent"/>. + /// Returning null is a programmer error. + /// </summary> + public float? GetProgress(EntityUid uid, Entity<MindComponent> mind) + { + var ev = new ObjectiveGetProgressEvent(mind, mind.Comp); + RaiseLocalEvent(uid, ref ev); + if (ev.Progress != null) + return ev.Progress; + + Log.Error($"Objective {ToPrettyString(uid):objective} of {_mind.MindOwnerLoggingString(mind.Comp)} didn't set a progress value!"); + return null; + } + + /// <summary> + /// Returns true if an objective is completed. + /// </summary> + public bool IsCompleted(EntityUid uid, Entity<MindComponent> mind) + { + return (GetProgress(uid, mind) ?? 0f) >= 0.999f; } /// <summary> diff --git a/Content.Shared/Overlays/ShowHealthBarsComponent.cs b/Content.Shared/Overlays/ShowHealthBarsComponent.cs index ed9ce4b7653..32fe331bce7 100644 --- a/Content.Shared/Overlays/ShowHealthBarsComponent.cs +++ b/Content.Shared/Overlays/ShowHealthBarsComponent.cs @@ -1,5 +1,7 @@ using Content.Shared.Damage.Prototypes; +using Content.Shared.StatusIcon; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; namespace Content.Shared.Overlays; @@ -15,4 +17,7 @@ public sealed partial class ShowHealthBarsComponent : Component /// </summary> [DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageContainerPrototype>)), AutoNetworkedField] public List<string> DamageContainers = new(); + + [DataField] + public ProtoId<HealthIconPrototype>? HealthStatusIcon = "HealthIconFine"; } diff --git a/Content.Shared/Paint/PaintRemoverSystem.cs b/Content.Shared/Paint/PaintRemoverSystem.cs index e2565e0f220..b67ef156fa7 100644 --- a/Content.Shared/Paint/PaintRemoverSystem.cs +++ b/Content.Shared/Paint/PaintRemoverSystem.cs @@ -36,8 +36,7 @@ private void OnInteract(EntityUid uid, PaintRemoverComponent component, AfterInt _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.CleanDelay, new PaintRemoverDoAfterEvent(), uid, args.Target, uid) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 1.0f, }); @@ -83,8 +82,7 @@ private void OnPaintRemoveVerb(EntityUid uid, PaintRemoverComponent component, G args.Target, uid) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 1.0f, }); diff --git a/Content.Shared/Pinpointer/SharedNavMapSystem.cs b/Content.Shared/Pinpointer/SharedNavMapSystem.cs index 8575d121f39..37d60dec28a 100644 --- a/Content.Shared/Pinpointer/SharedNavMapSystem.cs +++ b/Content.Shared/Pinpointer/SharedNavMapSystem.cs @@ -25,7 +25,7 @@ public abstract class SharedNavMapSystem : EntitySystem [Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!; [Robust.Shared.IoC.Dependency] private readonly INetManager _net = default!; - private readonly string[] _wallTags = ["Wall", "Window"]; + private static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"}; private EntityQuery<NavMapDoorComponent> _doorQuery; public override void Initialize() @@ -59,7 +59,7 @@ public NavMapChunkType GetEntityType(EntityUid uid) if (_doorQuery.HasComp(uid)) return NavMapChunkType.Airlock; - if (_tagSystem.HasAnyTag(uid, _wallTags)) + if (_tagSystem.HasAnyTag(uid, WallTags)) return NavMapChunkType.Wall; return NavMapChunkType.Invalid; diff --git a/Content.Shared/Pinpointer/SharedProximityBeeper.cs b/Content.Shared/Pinpointer/SharedProximityBeeper.cs deleted file mode 100644 index 51631126833..00000000000 --- a/Content.Shared/Pinpointer/SharedProximityBeeper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Pinpointer; - -[Serializable, NetSerializable] -public enum ProximityBeeperVisuals : byte -{ - Enabled -} diff --git a/Content.Shared/Pinpointer/StationMapComponent.cs b/Content.Shared/Pinpointer/StationMapComponent.cs new file mode 100644 index 00000000000..07cc99605ed --- /dev/null +++ b/Content.Shared/Pinpointer/StationMapComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Pinpointer; + +[RegisterComponent] +public sealed partial class StationMapComponent : Component +{ + /// <summary> + /// Whether or not to show the user's location on the map. + /// </summary> + [DataField] + public bool ShowLocation = true; +} diff --git a/Content.Shared/Placeable/ItemPlacerSystem.cs b/Content.Shared/Placeable/ItemPlacerSystem.cs index 9be6a4acd5a..d1ea88b82a0 100644 --- a/Content.Shared/Placeable/ItemPlacerSystem.cs +++ b/Content.Shared/Placeable/ItemPlacerSystem.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Physics.Events; +using Content.Shared.Whitelist; +using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; namespace Content.Shared.Placeable; @@ -11,6 +12,7 @@ public sealed class ItemPlacerSystem : EntitySystem { [Dependency] private readonly CollisionWakeSystem _wake = default!; [Dependency] private readonly PlaceableSurfaceSystem _placeableSurface = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -22,7 +24,7 @@ public override void Initialize() private void OnStartCollide(EntityUid uid, ItemPlacerComponent comp, ref StartCollideEvent args) { - if (comp.Whitelist != null && !comp.Whitelist.IsValid(args.OtherEntity)) + if (_whitelistSystem.IsWhitelistFail(comp.Whitelist, args.OtherEntity)) return; if (TryComp<CollisionWakeComponent>(args.OtherEntity, out var wakeComp)) diff --git a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs index b0031cfa33f..a1c87f0de71 100644 --- a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs +++ b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs @@ -1,29 +1,37 @@ using System.Numerics; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; +using Content.Shared.Storage; using Content.Shared.Storage.Components; -namespace Content.Shared.Placeable +namespace Content.Shared.Placeable; + +public sealed class PlaceableSurfaceSystem : EntitySystem { - public sealed class PlaceableSurfaceSystem : EntitySystem - { - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent<PlaceableSurfaceComponent, AfterInteractUsingEvent>(OnAfterInteractUsing); - } + SubscribeLocalEvent<PlaceableSurfaceComponent, AfterInteractUsingEvent>(OnAfterInteractUsing); + SubscribeLocalEvent<PlaceableSurfaceComponent, StorageInteractUsingAttemptEvent>(OnStorageInteractUsingAttempt); + SubscribeLocalEvent<PlaceableSurfaceComponent, StorageAfterOpenEvent>(OnStorageAfterOpen); + SubscribeLocalEvent<PlaceableSurfaceComponent, StorageAfterCloseEvent>(OnStorageAfterClose); + } public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceComponent? surface = null) { if (!Resolve(uid, ref surface, false)) return; - surface.IsPlaceable = isPlaceable; - Dirty(uid, surface); - } + if (surface.IsPlaceable == isPlaceable) + return; + + surface.IsPlaceable = isPlaceable; + Dirty(uid, surface); + } public void SetPlaceCentered(EntityUid uid, bool placeCentered, PlaceableSurfaceComponent? surface = null) { @@ -59,12 +67,24 @@ private void OnAfterInteractUsing(EntityUid uid, PlaceableSurfaceComponent surfa if (!_handsSystem.TryDrop(args.User, args.Used)) return; - if (surface.PlaceCentered) - Transform(args.Used).LocalPosition = Transform(uid).LocalPosition + surface.PositionOffset; - else - Transform(args.Used).Coordinates = args.ClickLocation; + _transformSystem.SetCoordinates(args.Used, + surface.PlaceCentered ? Transform(uid).Coordinates.Offset(surface.PositionOffset) : args.ClickLocation); - args.Handled = true; - } + args.Handled = true; + } + + private void OnStorageInteractUsingAttempt(Entity<PlaceableSurfaceComponent> ent, ref StorageInteractUsingAttemptEvent args) + { + args.Cancelled = true; + } + + private void OnStorageAfterOpen(Entity<PlaceableSurfaceComponent> ent, ref StorageAfterOpenEvent args) + { + SetPlaceable(ent.Owner, true, ent.Comp); + } + + private void OnStorageAfterClose(Entity<PlaceableSurfaceComponent> ent, ref StorageAfterCloseEvent args) + { + SetPlaceable(ent.Owner, false, ent.Comp); } } diff --git a/Content.Shared/Plunger/Systems/PlungerSystem.cs b/Content.Shared/Plunger/Systems/PlungerSystem.cs index 6e07657941b..6117636dee9 100644 --- a/Content.Shared/Plunger/Systems/PlungerSystem.cs +++ b/Content.Shared/Plunger/Systems/PlungerSystem.cs @@ -45,7 +45,7 @@ private void OnInteract(EntityUid uid, PlungerComponent component, AfterInteract _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.PlungeDuration, new PlungerDoAfterEvent(), uid, target, uid) { - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 1.0f, }); diff --git a/Content.Shared/Power/Components/ApcPowerReceiverComponentState.cs b/Content.Shared/Power/Components/ApcPowerReceiverComponentState.cs new file mode 100644 index 00000000000..9b18d6ad938 --- /dev/null +++ b/Content.Shared/Power/Components/ApcPowerReceiverComponentState.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Power.Components; + +[Serializable, NetSerializable] +public sealed class ApcPowerReceiverComponentState : ComponentState +{ + public bool Powered; +} diff --git a/Content.Shared/Power/Components/SharedApcPowerReceiverComponent.cs b/Content.Shared/Power/Components/SharedApcPowerReceiverComponent.cs new file mode 100644 index 00000000000..d73993357a3 --- /dev/null +++ b/Content.Shared/Power/Components/SharedApcPowerReceiverComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Power.Components; + +[NetworkedComponent] +public abstract partial class SharedApcPowerReceiverComponent : Component +{ + [ViewVariables] + public bool Powered; +} diff --git a/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs b/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs new file mode 100644 index 00000000000..2bc2af78314 --- /dev/null +++ b/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Examine; +using Content.Shared.Power.Components; + +namespace Content.Shared.Power.EntitySystems; + +public abstract class SharedPowerReceiverSystem : EntitySystem +{ + public abstract bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component); + + public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity) + { + if (!ResolveApc(entity.Owner, ref entity.Comp)) + return true; + + return entity.Comp.Powered; + } + + protected string GetExamineText(bool powered) + { + return Loc.GetString("power-receiver-component-on-examine-main", + ("stateText", Loc.GetString(powered + ? "power-receiver-component-on-examine-powered" + : "power-receiver-component-on-examine-unpowered"))); + } +} diff --git a/Content.Shared/Power/Generator/ActiveGeneratorRevvingComponent.cs b/Content.Shared/Power/Generator/ActiveGeneratorRevvingComponent.cs index 25f97dc15a0..fde0319a37d 100644 --- a/Content.Shared/Power/Generator/ActiveGeneratorRevvingComponent.cs +++ b/Content.Shared/Power/Generator/ActiveGeneratorRevvingComponent.cs @@ -3,7 +3,7 @@ namespace Content.Shared.Power.Generator; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class ActiveGeneratorRevvingComponent: Component +public sealed partial class ActiveGeneratorRevvingComponent : Component { [DataField, ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] public TimeSpan CurrentTime = TimeSpan.Zero; diff --git a/Content.Shared/Power/Generator/ActiveGeneratorRevvingSystem.cs b/Content.Shared/Power/Generator/ActiveGeneratorRevvingSystem.cs index 9cd11ae6045..459b2fd78de 100644 --- a/Content.Shared/Power/Generator/ActiveGeneratorRevvingSystem.cs +++ b/Content.Shared/Power/Generator/ActiveGeneratorRevvingSystem.cs @@ -1,6 +1,6 @@ namespace Content.Shared.Power.Generator; -public sealed class ActiveGeneratorRevvingSystem: EntitySystem +public sealed class ActiveGeneratorRevvingSystem : EntitySystem { public override void Initialize() { @@ -25,7 +25,7 @@ private void OnAnchorStateChanged(EntityUid uid, ActiveGeneratorRevvingComponent /// <param name="component">ActiveGeneratorRevvingComponent of the generator entity.</param> public void StartAutoRevving(EntityUid uid, ActiveGeneratorRevvingComponent? component = null) { - if (Resolve(uid, ref component)) + if (Resolve(uid, ref component, false)) { // reset the revving component.CurrentTime = TimeSpan.FromSeconds(0); diff --git a/Content.Shared/Power/PowerChangedEvent.cs b/Content.Shared/Power/PowerChangedEvent.cs new file mode 100644 index 00000000000..578a34142a8 --- /dev/null +++ b/Content.Shared/Power/PowerChangedEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Power; + +/// <summary> +/// Raised whenever an ApcPowerReceiver becomes powered / unpowered. +/// Does nothing on the client. +/// </summary> +[ByRefEvent] +public readonly record struct PowerChangedEvent(bool Powered, float ReceivingPower); \ No newline at end of file diff --git a/Content.Shared/Power/SharedPowerCharge.cs b/Content.Shared/Power/SharedPowerCharge.cs new file mode 100644 index 00000000000..5599930f37a --- /dev/null +++ b/Content.Shared/Power/SharedPowerCharge.cs @@ -0,0 +1,77 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Power; + +/// <summary> +/// Sent to the server to set whether the machine should be on or off +/// </summary> +[Serializable, NetSerializable] +public sealed class SwitchChargingMachineMessage : BoundUserInterfaceMessage +{ + public bool On; + + public SwitchChargingMachineMessage(bool on) + { + On = on; + } +} + +[Serializable, NetSerializable] +public sealed class PowerChargeState : BoundUserInterfaceState +{ + public bool On; + // 0 -> 255 + public byte Charge; + public PowerChargePowerStatus PowerStatus; + public short PowerDraw; + public short PowerDrawMax; + public short EtaSeconds; + + public PowerChargeState( + bool on, + byte charge, + PowerChargePowerStatus powerStatus, + short powerDraw, + short powerDrawMax, + short etaSeconds) + { + On = on; + Charge = charge; + PowerStatus = powerStatus; + PowerDraw = powerDraw; + PowerDrawMax = powerDrawMax; + EtaSeconds = etaSeconds; + } +} + +[Serializable, NetSerializable] +public enum PowerChargeUiKey +{ + Key +} + +[Serializable, NetSerializable] +public enum PowerChargeVisuals +{ + State, + Charge, + Active +} + +[Serializable, NetSerializable] +public enum PowerChargeStatus +{ + Broken, + Unpowered, + Off, + On +} + +[Serializable, NetSerializable] +public enum PowerChargePowerStatus : byte +{ + Off, + Discharging, + Charging, + FullyCharged +} diff --git a/Content.Shared/Power/SharedPowerChargeComponent.cs b/Content.Shared/Power/SharedPowerChargeComponent.cs new file mode 100644 index 00000000000..96f26a7903c --- /dev/null +++ b/Content.Shared/Power/SharedPowerChargeComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.Power; + +/// <summary> +/// Component for a powered machine that slowly powers on and off over a period of time. +/// </summary> +public abstract partial class SharedPowerChargeComponent : Component +{ + /// <summary> + /// The title used for the default charged machine window if used + /// </summary> + [DataField] + public LocId WindowTitle { get; set; } = string.Empty; + +} diff --git a/Content.Shared/PowerCell/PowerCellDrawComponent.cs b/Content.Shared/PowerCell/PowerCellDrawComponent.cs index 708a86a8eaf..e4402e9eff4 100644 --- a/Content.Shared/PowerCell/PowerCellDrawComponent.cs +++ b/Content.Shared/PowerCell/PowerCellDrawComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Item.ItemToggle.Components; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -6,6 +7,10 @@ namespace Content.Shared.PowerCell; /// <summary> /// Indicates that the entity's ActivatableUI requires power or else it closes. /// </summary> +/// <remarks> +/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween. +/// Requires <see cref="ItemToggleComponent"/> to work. +/// </remarks> [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] public sealed partial class PowerCellDrawComponent : Component { @@ -26,10 +31,12 @@ public sealed partial class PowerCellDrawComponent : Component #endregion /// <summary> - /// Is this power cell currently drawing power every tick. + /// Whether drawing is enabled, regardless of ItemToggle. + /// Having no cell will still disable it. + /// Only use this if you really don't want it to use power for some time. /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("enabled")] - public bool Drawing; + [DataField, AutoNetworkedField] + public bool Enabled = true; /// <summary> /// How much the entity draws while the UI is open. @@ -51,4 +58,10 @@ public sealed partial class PowerCellDrawComponent : Component [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan NextUpdateTime; + + /// <summary> + /// How long to wait between power drawing. + /// </summary> + [DataField] + public TimeSpan Delay = TimeSpan.FromSeconds(1); } diff --git a/Content.Shared/PowerCell/SharedPowerCellSystem.cs b/Content.Shared/PowerCell/SharedPowerCellSystem.cs index 508bfc85f08..2b2a836633c 100644 --- a/Content.Shared/PowerCell/SharedPowerCellSystem.cs +++ b/Content.Shared/PowerCell/SharedPowerCellSystem.cs @@ -1,4 +1,6 @@ using Content.Shared.Containers.ItemSlots; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.PowerCell.Components; using Content.Shared.Rejuvenate; using Robust.Shared.Containers; @@ -11,14 +13,19 @@ public abstract class SharedPowerCellSystem : EntitySystem [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] protected readonly ItemToggleSystem Toggle = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(OnRejuvenate); SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellInserted); SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellRemoved); SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellInsertAttempt); + + SubscribeLocalEvent<PowerCellDrawComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt); + SubscribeLocalEvent<PowerCellDrawComponent, ItemToggledEvent>(OnToggled); } private void OnRejuvenate(EntityUid uid, PowerCellSlotComponent component, RejuvenateEvent args) @@ -63,13 +70,25 @@ protected virtual void OnCellRemoved(EntityUid uid, PowerCellSlotComponent compo RaiseLocalEvent(uid, new PowerCellChangedEvent(true), false); } - public void SetPowerCellDrawEnabled(EntityUid uid, bool enabled, PowerCellDrawComponent? component = null) + private void OnActivateAttempt(Entity<PowerCellDrawComponent> ent, ref ItemToggleActivateAttemptEvent args) + { + if (!HasDrawCharge(ent, ent.Comp, user: args.User) + || !HasActivatableCharge(ent, ent.Comp, user: args.User)) + args.Cancelled = true; + } + + private void OnToggled(Entity<PowerCellDrawComponent> ent, ref ItemToggledEvent args) + { + ent.Comp.NextUpdateTime = Timing.CurTime; + } + + public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled) { - if (!Resolve(uid, ref component, false) || enabled == component.Drawing) + if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled) return; - component.Drawing = enabled; - component.NextUpdateTime = Timing.CurTime; + ent.Comp.Enabled = enabled; + Dirty(ent, ent.Comp); } /// <summary> diff --git a/Content.Shared/Projectiles/ProjectileComponent.cs b/Content.Shared/Projectiles/ProjectileComponent.cs index c24cf2b5ee5..8349252df2b 100644 --- a/Content.Shared/Projectiles/ProjectileComponent.cs +++ b/Content.Shared/Projectiles/ProjectileComponent.cs @@ -8,6 +8,12 @@ namespace Content.Shared.Projectiles; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ProjectileComponent : Component { + /// <summary> + /// The angle of the fired projectile. + /// </summary> + [DataField, AutoNetworkedField] + public Angle Angle; + /// <summary> /// The effect that appears when a projectile collides with an entity. /// </summary> diff --git a/Content.Shared/Projectiles/ProjectileSpreadComponent.cs b/Content.Shared/Projectiles/ProjectileSpreadComponent.cs new file mode 100644 index 00000000000..1edffe3ba56 --- /dev/null +++ b/Content.Shared/Projectiles/ProjectileSpreadComponent.cs @@ -0,0 +1,32 @@ +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Projectiles; + +/// <summary> +/// Spawns a spread of the projectiles when fired +/// </summary> +[RegisterComponent, NetworkedComponent, Access(typeof(SharedGunSystem))] +public sealed partial class ProjectileSpreadComponent : Component +{ + /// <summary> + /// The entity prototype that will be fired by the rest of the spread. + /// Will generally be the same entity prototype as the first projectile being fired. + /// Needed for ammo components that do not specify a fired prototype, unlike cartridges. + /// </summary> + [DataField(required: true)] + public EntProtoId Proto; + + /// <summary> + /// How much the ammo spreads when shot, in degrees. Does nothing if count is 0. + /// </summary> + [DataField] + public Angle Spread = Angle.FromDegrees(5); + + /// <summary> + /// How many prototypes are spawned when shot. + /// </summary> + [DataField] + public int Count = 1; +} diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index b7b3ce74761..0118df002e8 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -90,8 +90,7 @@ private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent compon new RemoveEmbeddedProjectileEvent(), eventTarget: uid, target: uid) { DistanceThreshold = SharedInteractionSystem.InteractionRange, - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, NeedHand = true, }); } @@ -270,3 +269,9 @@ public record struct ProjectileReflectAttemptEvent(EntityUid ProjUid, Projectile /// </summary> [ByRefEvent] public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null); + +/// <summary> +/// Raised after a projectile has dealt it's damage. +/// </summary> +[ByRefEvent] +public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target); diff --git a/Content.Shared/ProximityDetection/Components/ProximityDetectorComponent.cs b/Content.Shared/ProximityDetection/Components/ProximityDetectorComponent.cs index 09cb7f06d5d..7e2bb4dfe62 100644 --- a/Content.Shared/ProximityDetection/Components/ProximityDetectorComponent.cs +++ b/Content.Shared/ProximityDetection/Components/ProximityDetectorComponent.cs @@ -10,12 +10,6 @@ namespace Content.Shared.ProximityDetection.Components; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState ,Access(typeof(ProximityDetectionSystem))] public sealed partial class ProximityDetectorComponent : Component { - /// <summary> - /// Whether or not it's on. - /// </summary> - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public bool Enabled = true; - /// <summary> /// The criteria used to filter entities /// Note: RequireAll is only supported for tags, all components are required to count as a match! @@ -35,13 +29,13 @@ public sealed partial class ProximityDetectorComponent : Component [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] public FixedPoint2 Distance = -1; - /// <summary> /// The farthest distance to search for targets /// </summary> [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] public FixedPoint2 Range = 10f; + // TODO: use timespans not this public float AccumulatedFrameTime; [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] diff --git a/Content.Shared/ProximityDetection/Systems/ProximityDetectionSystem.cs b/Content.Shared/ProximityDetection/Systems/ProximityDetectionSystem.cs index db25e8bc511..df302f94771 100644 --- a/Content.Shared/ProximityDetection/Systems/ProximityDetectionSystem.cs +++ b/Content.Shared/ProximityDetection/Systems/ProximityDetectionSystem.cs @@ -1,4 +1,6 @@ -using Content.Shared.ProximityDetection.Components; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.ProximityDetection.Components; using Content.Shared.Tag; using Robust.Shared.Network; @@ -9,6 +11,7 @@ namespace Content.Shared.ProximityDetection.Systems; public sealed class ProximityDetectionSystem : EntitySystem { [Dependency] private readonly EntityLookupSystem _entityLookup = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly INetManager _net = default!; @@ -17,10 +20,10 @@ public sealed class ProximityDetectionSystem : EntitySystem public override void Initialize() { - SubscribeLocalEvent<ProximityDetectorComponent, EntityPausedEvent>(OnPaused); - SubscribeLocalEvent<ProximityDetectorComponent, EntityUnpausedEvent>(OnUnpaused); - SubscribeLocalEvent<ProximityDetectorComponent, ComponentInit>(OnCompInit); + base.Initialize(); + SubscribeLocalEvent<ProximityDetectorComponent, ComponentInit>(OnCompInit); + SubscribeLocalEvent<ProximityDetectorComponent, ItemToggledEvent>(OnToggled); } private void OnCompInit(EntityUid uid, ProximityDetectorComponent component, ComponentInit args) @@ -30,57 +33,39 @@ private void OnCompInit(EntityUid uid, ProximityDetectorComponent component, Com Log.Debug("DetectorComponent only supports requireAll = false for tags. All components are required for a match!"); } - private void OnPaused(EntityUid owner, ProximityDetectorComponent component, EntityPausedEvent args) - { - SetEnable_Internal(owner,component,false); - } - - private void OnUnpaused(EntityUid owner, ProximityDetectorComponent detector, ref EntityUnpausedEvent args) - { - SetEnable_Internal(owner, detector,true); - } - public void SetEnable(EntityUid owner, bool enabled, ProximityDetectorComponent? detector = null) - { - if (!Resolve(owner, ref detector) || detector.Enabled == enabled) - return; - SetEnable_Internal(owner ,detector, enabled); - } - public override void Update(float frameTime) { if (_net.IsClient) return; + var query = EntityQueryEnumerator<ProximityDetectorComponent>(); while (query.MoveNext(out var owner, out var detector)) { - if (!detector.Enabled) + if (!_toggle.IsActivated(owner)) continue; + detector.AccumulatedFrameTime += frameTime; if (detector.AccumulatedFrameTime < detector.UpdateRate) continue; + detector.AccumulatedFrameTime -= detector.UpdateRate; RunUpdate_Internal(owner, detector); } } - public bool GetEnable(EntityUid owner, ProximityDetectorComponent? detector = null) + private void OnToggled(Entity<ProximityDetectorComponent> ent, ref ItemToggledEvent args) { - return Resolve(owner, ref detector, false) && detector.Enabled; - } - - private void SetEnable_Internal(EntityUid owner,ProximityDetectorComponent detector, bool enabled) - { - detector.Enabled = enabled; - var noDetectEvent = new ProximityTargetUpdatedEvent(detector, detector.TargetEnt, detector.Distance); - RaiseLocalEvent(owner, ref noDetectEvent); - if (!enabled) + if (args.Activated) { - detector.AccumulatedFrameTime = 0; - RunUpdate_Internal(owner, detector); - Dirty(owner, detector); + RunUpdate_Internal(ent, ent.Comp); return; } - RunUpdate_Internal(owner, detector); + + var noDetectEvent = new ProximityTargetUpdatedEvent(ent.Comp, Target: null, ent.Comp.Distance); + RaiseLocalEvent(ent, ref noDetectEvent); + + ent.Comp.AccumulatedFrameTime = 0; + Dirty(ent, ent.Comp); } public void ForceUpdate(EntityUid owner, ProximityDetectorComponent? detector = null) @@ -90,11 +75,31 @@ public void ForceUpdate(EntityUid owner, ProximityDetectorComponent? detector = RunUpdate_Internal(owner, detector); } + private void ClearTarget(Entity<ProximityDetectorComponent> ent) + { + var (uid, comp) = ent; + if (comp.TargetEnt == null) + return; + + comp.Distance = -1; + comp.TargetEnt = null; + var noDetectEvent = new ProximityTargetUpdatedEvent(comp, null, -1); + RaiseLocalEvent(uid, ref noDetectEvent); + var newTargetEvent = new NewProximityTargetEvent(comp, null); + RaiseLocalEvent(uid, ref newTargetEvent); + Dirty(uid, comp); + } private void RunUpdate_Internal(EntityUid owner,ProximityDetectorComponent detector) { if (!_net.IsServer) //only run detection checks on the server! return; + + if (Deleted(detector.TargetEnt)) + { + ClearTarget((owner, detector)); + } + var xformQuery = GetEntityQuery<TransformComponent>(); var xform = xformQuery.GetComponent(owner); List<(EntityUid TargetEnt, float Distance)> detections = new(); @@ -173,15 +178,7 @@ private void UpdateTargetFromClosest(EntityUid owner, ProximityDetectorComponent { if (detections.Count == 0) { - if (detector.TargetEnt == null) - return; - detector.Distance = -1; - detector.TargetEnt = null; - var noDetectEvent = new ProximityTargetUpdatedEvent(detector, null, -1); - RaiseLocalEvent(owner, ref noDetectEvent); - var newTargetEvent = new NewProximityTargetEvent(detector, null); - RaiseLocalEvent(owner, ref newTargetEvent); - Dirty(owner, detector); + ClearTarget((owner, detector)); return; } var closestDistance = detections[0].Distance; @@ -198,6 +195,7 @@ private void UpdateTargetFromClosest(EntityUid owner, ProximityDetectorComponent var newData = newTarget || detector.Distance != closestDistance; detector.TargetEnt = closestEnt; detector.Distance = closestDistance; + Dirty(owner, detector); if (newTarget) { var newTargetEvent = new NewProximityTargetEvent(detector, closestEnt); diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs index a59c09ca535..ab87585c706 100644 --- a/Content.Shared/Prying/Systems/PryingSystem.cs +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -137,8 +137,7 @@ private bool StartPry(EntityUid target, EntityUid user, EntityUid? tool, float t var doAfterArgs = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(modEv.BaseTime * modEv.PryTimeModifier / toolModifier), new DoorPryDoAfterEvent(), target, target, tool) { BreakOnDamage = true, - BreakOnUserMove = true, - BreakOnWeightlessMove = true, + BreakOnMove = true, }; if (tool != null) diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index bbdfc5d0218..f2c18f43f25 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -211,8 +211,7 @@ private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInterac { BreakOnDamage = true, BreakOnHandChange = true, - BreakOnUserMove = true, - BreakOnTargetMove = args.Target != null, + BreakOnMove = true, AttemptFrequency = AttemptFrequency.EveryTick, CancelDuplicate = false, BlockDuplicate = false, diff --git a/Content.Shared/Radio/Components/IntercomComponent.cs b/Content.Shared/Radio/Components/IntercomComponent.cs index be2734ff168..8d7b87597b1 100644 --- a/Content.Shared/Radio/Components/IntercomComponent.cs +++ b/Content.Shared/Radio/Components/IntercomComponent.cs @@ -1,23 +1,32 @@ using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Prototypes; namespace Content.Shared.Radio.Components; /// <summary> /// Handles intercom ui and is authoritative on the channels an intercom can access. /// </summary> -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class IntercomComponent : Component { /// <summary> - /// Does this intercom require popwer to function + /// Does this intercom require power to function /// </summary> - [DataField("requiresPower"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool RequiresPower = true; + [DataField, AutoNetworkedField] + public bool SpeakerEnabled; + + [DataField, AutoNetworkedField] + public bool MicrophoneEnabled; + + [DataField, AutoNetworkedField] + public ProtoId<RadioChannelPrototype>? CurrentChannel; + /// <summary> /// The list of radio channel prototypes this intercom can choose between. /// </summary> - [DataField("supportedChannels", customTypeSerializer: typeof(PrototypeIdListSerializer<RadioChannelPrototype>))] - public List<string> SupportedChannels = new(); + [DataField, AutoNetworkedField] + public List<ProtoId<RadioChannelPrototype>> SupportedChannels = new(); } diff --git a/Content.Shared/Radio/Components/TelecomExemptComponent.cs b/Content.Shared/Radio/Components/TelecomExemptComponent.cs new file mode 100644 index 00000000000..7af5c1c78ca --- /dev/null +++ b/Content.Shared/Radio/Components/TelecomExemptComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Radio.Components; + +/// <summary> +/// This is used for a radio that doesn't need a telecom server in order to broadcast. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class TelecomExemptComponent : Component; diff --git a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs index a15b6f76e90..e04fd0a4a5c 100644 --- a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs +++ b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs @@ -31,6 +31,7 @@ public sealed partial class EncryptionKeySystem : EntitySystem [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedWiresSystem _wires = default!; public override void Initialize() { @@ -104,7 +105,7 @@ private void OnInteractUsing(EntityUid uid, EncryptionKeyHolderComponent compone TryInsertKey(uid, component, args); } else if (TryComp<ToolComponent>(args.Used, out var tool) - && tool.Qualities.Contains(component.KeysExtractionMethod) + && _tool.HasQuality(args.Used, component.KeysExtractionMethod, tool) && component.KeyContainer.ContainedEntities.Count > 0) // dont block deconstruction { args.Handled = true; @@ -150,7 +151,7 @@ private void TryRemoveKey(EntityUid uid, EncryptionKeyHolderComponent component, return; } - if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open) + if (!_wires.IsPanelOpen(uid)) { _popup.PopupClient(Loc.GetString("encryption-keys-panel-locked"), uid, args.User); return; @@ -186,8 +187,15 @@ private void OnHolderExamined(EntityUid uid, EncryptionKeyHolderComponent compon if (component.Channels.Count > 0) { - args.PushMarkup(Loc.GetString("examine-encryption-channels-prefix")); - AddChannelsExamine(component.Channels, component.DefaultChannel, args, _protoManager, "examine-encryption-channel"); + using (args.PushGroup(nameof(EncryptionKeyComponent))) + { + args.PushMarkup(Loc.GetString("examine-encryption-channels-prefix")); + AddChannelsExamine(component.Channels, + component.DefaultChannel, + args, + _protoManager, + "examine-encryption-channel"); + } } } diff --git a/Content.Shared/Radio/SharedIntercom.cs b/Content.Shared/Radio/SharedIntercom.cs index 410843312fd..f697add8b90 100644 --- a/Content.Shared/Radio/SharedIntercom.cs +++ b/Content.Shared/Radio/SharedIntercom.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Serialization; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; namespace Content.Shared.Radio; @@ -8,23 +9,6 @@ public enum IntercomUiKey Key, } -[Serializable, NetSerializable] -public sealed class IntercomBoundUIState : BoundUserInterfaceState -{ - public bool MicEnabled; - public bool SpeakerEnabled; - public List<string> AvailableChannels; - public string SelectedChannel; - - public IntercomBoundUIState(bool micEnabled, bool speakerEnabled, List<string> availableChannels, string selectedChannel) - { - MicEnabled = micEnabled; - SpeakerEnabled = speakerEnabled; - AvailableChannels = availableChannels; - SelectedChannel = selectedChannel; - } -} - [Serializable, NetSerializable] public sealed class ToggleIntercomMicMessage : BoundUserInterfaceMessage { diff --git a/Content.Shared/Random/RulesSystem.cs b/Content.Shared/Random/RulesSystem.cs index f8711fb63e0..2e8c044c3ca 100644 --- a/Content.Shared/Random/RulesSystem.cs +++ b/Content.Shared/Random/RulesSystem.cs @@ -1,6 +1,7 @@ using System.Numerics; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; +using Content.Shared.Whitelist; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; @@ -14,7 +15,7 @@ public sealed class RulesSystem : EntitySystem [Dependency] private readonly AccessReaderSystem _reader = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public bool IsTrue(EntityUid uid, RulesPrototype rules) { var inRange = new HashSet<Entity<IComponent>>(); @@ -158,7 +159,7 @@ public bool IsTrue(EntityUid uid, RulesPrototype rules) foreach (var ent in _lookup.GetEntitiesInRange(xform.MapID, worldPos, entity.Range)) { - if (!entity.Whitelist.IsValid(ent, EntityManager)) + if (_whitelistSystem.IsWhitelistFail(entity.Whitelist, ent)) continue; count++; diff --git a/Content.Shared/RatKing/SharedRatKingSystem.cs b/Content.Shared/RatKing/SharedRatKingSystem.cs index d7a8ee5460d..ea489e332db 100644 --- a/Content.Shared/RatKing/SharedRatKingSystem.cs +++ b/Content.Shared/RatKing/SharedRatKingSystem.cs @@ -119,7 +119,7 @@ private void OnGetVerb(EntityUid uid, RatKingRummageableComponent component, Get { BlockDuplicate = true, BreakOnDamage = true, - BreakOnUserMove = true, + BreakOnMove = true, DistanceThreshold = 2f }); } diff --git a/Content.Shared/Research/Prototypes/LatheRecipePrototype.cs b/Content.Shared/Research/Prototypes/LatheRecipePrototype.cs index 8b0c866cbe3..10a0c7209da 100644 --- a/Content.Shared/Research/Prototypes/LatheRecipePrototype.cs +++ b/Content.Shared/Research/Prototypes/LatheRecipePrototype.cs @@ -1,101 +1,58 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; using Content.Shared.Lathe.Prototypes; using Content.Shared.Materials; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Robust.Shared.Utility; namespace Content.Shared.Research.Prototypes { - [NetSerializable, Serializable, Prototype("latheRecipe")] + [NetSerializable, Serializable, Prototype] public sealed partial class LatheRecipePrototype : IPrototype { [ViewVariables] [IdDataField] public string ID { get; private set; } = default!; - [DataField("name")] - private string _name = string.Empty; + /// <summary> + /// Name displayed in the lathe GUI. + /// </summary> + [DataField] + public LocId? Name; - [DataField("description")] - private string _description = string.Empty; + /// <summary> + /// Short description displayed in the lathe GUI. + /// </summary> + [DataField] + public LocId? Description; /// <summary> /// The prototype name of the resulting entity when the recipe is printed. /// </summary> - [DataField("result", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] - public string Result = string.Empty; + [DataField] + public EntProtoId? Result; + + [DataField] + public Dictionary<ProtoId<ReagentPrototype>, FixedPoint2>? ResultReagents; /// <summary> /// An entity whose sprite is displayed in the ui in place of the actual recipe result. /// </summary> - [DataField("icon")] + [DataField] public SpriteSpecifier? Icon; [DataField("completetime")] - private TimeSpan _completeTime = TimeSpan.FromSeconds(5); - - [DataField("materials", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, MaterialPrototype>))] - private Dictionary<string, int> _requiredMaterials = new(); - - //todo make these function calls because we're eating tons of resolves here. - /// <summary> - /// Name displayed in the lathe GUI. - /// </summary> - [ViewVariables] - public string Name - { - get - { - if (_name.Trim().Length != 0) - return _name; - var protoMan = IoCManager.Resolve<IPrototypeManager>(); - protoMan.TryIndex(Result, out EntityPrototype? prototype); - if (prototype?.Name != null) - _name = prototype.Name; - return _name; - } - } - - /// <summary> - /// Short description displayed in the lathe GUI. - /// </summary> - [ViewVariables] - public string Description - { - get - { - if (_description.Trim().Length != 0) - return _description; - var protoMan = IoCManager.Resolve<IPrototypeManager>(); - protoMan.TryIndex(Result, out EntityPrototype? prototype); - if (prototype?.Description != null) - _description = prototype.Description; - return _description; - } - } + public TimeSpan CompleteTime = TimeSpan.FromSeconds(5); /// <summary> /// The materials required to produce this recipe. /// Takes a material ID as string. /// </summary> - [ViewVariables] - public Dictionary<string, int> RequiredMaterials - { - get => _requiredMaterials; - private set => _requiredMaterials = value; - } - - - /// <summary> - /// How many milliseconds it'll take for the lathe to finish this recipe. - /// Might lower depending on the lathe's upgrade level. - /// </summary> - [ViewVariables] - public TimeSpan CompleteTime => _completeTime; + [DataField] + public Dictionary<ProtoId<MaterialPrototype>, int> Materials = new(); - [DataField("applyMaterialDiscount")] + [DataField] public bool ApplyMaterialDiscount = true; /// <summary> @@ -103,5 +60,12 @@ public Dictionary<string, int> RequiredMaterials /// </summary> [DataField] public ProtoId<LatheCategoryPrototype>? Category; + + /// <summary> + /// DeltaV: Number of mining points this recipe adds to an oreproc when printed. + /// Scales with stack count. + /// </summary> + [DataField] + public uint MiningPoints; } } diff --git a/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs b/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs index 1fffa8c0257..070ff40b70d 100644 --- a/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs +++ b/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs @@ -46,8 +46,8 @@ private void OnBeforeInteractHand(EntityUid uid, ResearchStealerComponent comp, var doAfterArgs = new DoAfterArgs(EntityManager, uid, comp.Delay, new ResearchStealDoAfterEvent(), target: target, used: uid, eventTarget: uid) { BreakOnDamage = true, - BreakOnUserMove = true, - MovementThreshold = 0.5f + BreakOnMove = true, + MovementThreshold = 0.5f, }; _doAfter.TryStartDoAfter(doAfterArgs); diff --git a/Content.Shared/Research/Systems/SharedResearchSystem.cs b/Content.Shared/Research/Systems/SharedResearchSystem.cs index 9819e949b8b..d8ce0634d3e 100644 --- a/Content.Shared/Research/Systems/SharedResearchSystem.cs +++ b/Content.Shared/Research/Systems/SharedResearchSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Shared.Lathe; using Content.Shared.Research.Components; using Content.Shared.Research.Prototypes; using JetBrains.Annotations; @@ -12,6 +13,7 @@ public abstract class SharedResearchSystem : EntitySystem { [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedLatheSystem _lathe = default!; public override void Initialize() { @@ -185,7 +187,7 @@ public FormattedMessage GetTechnologyDescription( var recipeProto = PrototypeManager.Index(recipe); description.PushNewline(); description.AddMarkup(Loc.GetString("research-console-unlocks-list-entry", - ("name",recipeProto.Name))); + ("name", _lathe.GetRecipeName(recipeProto)))); } foreach (var generic in technology.GenericUnlocks) { @@ -280,4 +282,23 @@ public void ClearTechs(EntityUid uid, TechnologyDatabaseComponent? comp = null) comp.UnlockedTechnologies.Clear(); Dirty(uid, comp); } + + /// <summary> + /// Adds a lathe recipe to the specified technology database + /// without checking if it can be unlocked. + /// </summary> + public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (component.UnlockedRecipes.Contains(recipe)) + return; + + component.UnlockedRecipes.Add(recipe); + Dirty(uid, component); + + var ev = new TechnologyDatabaseModifiedEvent(); + RaiseLocalEvent(uid, ref ev); + } } diff --git a/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs b/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs new file mode 100644 index 00000000000..ce8a138bdbd --- /dev/null +++ b/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Random; +using Content.Shared.Research.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Research.TechnologyDisk.Components; + +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class TechnologyDiskComponent : Component +{ + /// <summary> + /// The recipe that will be added. If null, one will be randomly generated + /// </summary> + [DataField] + [AutoNetworkedField] + public List<ProtoId<LatheRecipePrototype>>? Recipes; + + /// <summary> + /// A weighted random prototype for how rare each tier should be. + /// </summary> + [DataField] + public ProtoId<WeightedRandomPrototype> TierWeightPrototype = "TechDiskTierWeights"; +} diff --git a/Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs b/Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs new file mode 100644 index 00000000000..93c7c22471c --- /dev/null +++ b/Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs @@ -0,0 +1,95 @@ +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Lathe; +using Content.Shared.Popups; +using Content.Shared.Random.Helpers; +using Content.Shared.Research.Components; +using Content.Shared.Research.Prototypes; +using Content.Shared.Research.Systems; +using Content.Shared.Research.TechnologyDisk.Components; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Shared.Research.TechnologyDisk.Systems; + +public sealed class TechnologyDiskSystem : EntitySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedResearchSystem _research = default!; + [Dependency] private readonly SharedLatheSystem _lathe = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract); + SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine); + } + + private void OnMapInit(Entity<TechnologyDiskComponent> ent, ref MapInitEvent args) + { + if (ent.Comp.Recipes != null) + return; + + var weightedRandom = _protoMan.Index(ent.Comp.TierWeightPrototype); + var tier = int.Parse(weightedRandom.Pick(_random)); + + //get a list of every distinct recipe in all the technologies. + var techs = new HashSet<ProtoId<LatheRecipePrototype>>(); + foreach (var tech in _protoMan.EnumeratePrototypes<TechnologyPrototype>()) + { + if (tech.Tier != tier) + continue; + + techs.UnionWith(tech.RecipeUnlocks); + } + + if (techs.Count == 0) + return; + + //pick one + ent.Comp.Recipes = []; + ent.Comp.Recipes.Add(_random.Pick(techs)); + Dirty(ent); + } + + private void OnAfterInteract(Entity<TechnologyDiskComponent> ent, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { } target) + return; + + if (!HasComp<ResearchServerComponent>(target) || !TryComp<TechnologyDatabaseComponent>(target, out var database)) + return; + + if (ent.Comp.Recipes != null) + { + foreach (var recipe in ent.Comp.Recipes) + { + _research.AddLatheRecipe(target, recipe, database); + } + } + _popup.PopupClient(Loc.GetString("tech-disk-inserted"), target, args.User); + if (_net.IsServer) + QueueDel(ent); + args.Handled = true; + } + + private void OnExamine(Entity<TechnologyDiskComponent> ent, ref ExaminedEvent args) + { + var message = Loc.GetString("tech-disk-examine-none"); + if (ent.Comp.Recipes != null && ent.Comp.Recipes.Count > 0) + { + var prototype = _protoMan.Index(ent.Comp.Recipes[0]); + message = Loc.GetString("tech-disk-examine", ("result", _lathe.GetRecipeName(prototype))); + + if (ent.Comp.Recipes.Count > 1) //idk how to do this well. sue me. + message += " " + Loc.GetString("tech-disk-examine-more"); + } + args.PushMarkup(message); + } +} diff --git a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs index d2c8374fefe..12589850e66 100644 --- a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs +++ b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs @@ -9,13 +9,13 @@ namespace Content.Shared.Revolutionary.Components; /// Component used for marking a Head Rev for conversion and winning/losing. /// </summary> [RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] -public sealed partial class HeadRevolutionaryComponent : Component, IAntagStatusIconComponent +public sealed partial class HeadRevolutionaryComponent : Component { /// <summary> /// The status icon corresponding to the head revolutionary. /// </summary> [DataField, ViewVariables(VVAccess.ReadWrite)] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "HeadRevolutionaryFaction"; + public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "HeadRevolutionaryFaction"; /// <summary> /// How long the stun will last after the user is converted. @@ -24,7 +24,4 @@ public sealed partial class HeadRevolutionaryComponent : Component, IAntagStatus public TimeSpan StunTime = TimeSpan.FromSeconds(3); public override bool SessionSpecific => true; - - [DataField] - public bool IconVisibleToGhost { get; set; } = true; } diff --git a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs index 73f533cf690..513e8897bdc 100644 --- a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs +++ b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs @@ -10,13 +10,13 @@ namespace Content.Shared.Revolutionary.Components; /// Used for marking regular revs as well as storing icon prototypes so you can see fellow revs. /// </summary> [RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] -public sealed partial class RevolutionaryComponent : Component, IAntagStatusIconComponent +public sealed partial class RevolutionaryComponent : Component { /// <summary> /// The status icon prototype displayed for revolutionaries /// </summary> [DataField, ViewVariables(VVAccess.ReadWrite)] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "RevolutionaryFaction"; + public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "RevolutionaryFaction"; /// <summary> /// Sound that plays when you are chosen as Rev. (Placeholder until I find something cool I guess) @@ -25,7 +25,4 @@ public sealed partial class RevolutionaryComponent : Component, IAntagStatusIcon public SoundSpecifier RevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/headrev_start.ogg"); public override bool SessionSpecific => true; - - [DataField] - public bool IconVisibleToGhost { get; set; } = true; } diff --git a/Content.Shared/Revolutionary/Components/ShowRevIconsComponent.cs b/Content.Shared/Revolutionary/Components/ShowRevIconsComponent.cs deleted file mode 100644 index 11e20db3f89..00000000000 --- a/Content.Shared/Revolutionary/Components/ShowRevIconsComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Revolutionary.Components; - -/// <summary> -/// Determines whether Someone can see rev/headrev icons on revs. -/// </summary> -[RegisterComponent, NetworkedComponent] -public sealed partial class ShowRevIconsComponent: Component -{ -} diff --git a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs index ddaf73fcc9f..bbf91193cc3 100644 --- a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs +++ b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Ghost; using Content.Shared.IdentityManagement; using Content.Shared.Mindshield.Components; using Content.Shared.Popups; @@ -6,10 +5,11 @@ using Content.Shared.Stunnable; using Robust.Shared.GameStates; using Robust.Shared.Player; +using Content.Shared.Antag; namespace Content.Shared.Revolutionary; -public sealed class SharedRevolutionarySystem : EntitySystem +public abstract class SharedRevolutionarySystem : EntitySystem { [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedStunSystem _sharedStun = default!; @@ -23,7 +23,7 @@ public override void Initialize() SubscribeLocalEvent<HeadRevolutionaryComponent, ComponentGetStateAttemptEvent>(OnRevCompGetStateAttempt); SubscribeLocalEvent<RevolutionaryComponent, ComponentStartup>(DirtyRevComps); SubscribeLocalEvent<HeadRevolutionaryComponent, ComponentStartup>(DirtyRevComps); - SubscribeLocalEvent<ShowRevIconsComponent, ComponentStartup>(DirtyRevComps); + SubscribeLocalEvent<ShowAntagIconsComponent, ComponentStartup>(DirtyRevComps); } /// <summary> @@ -52,7 +52,7 @@ private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, MapIni /// </summary> private void OnRevCompGetStateAttempt(EntityUid uid, HeadRevolutionaryComponent comp, ref ComponentGetStateAttemptEvent args) { - args.Cancelled = !CanGetState(args.Player, comp.IconVisibleToGhost); + args.Cancelled = !CanGetState(args.Player); } /// <summary> @@ -60,30 +60,24 @@ private void OnRevCompGetStateAttempt(EntityUid uid, HeadRevolutionaryComponent /// </summary> private void OnRevCompGetStateAttempt(EntityUid uid, RevolutionaryComponent comp, ref ComponentGetStateAttemptEvent args) { - args.Cancelled = !CanGetState(args.Player, comp.IconVisibleToGhost); + args.Cancelled = !CanGetState(args.Player); } /// <summary> /// The criteria that determine whether a Rev/HeadRev component should be sent to a client. /// </summary> /// <param name="player"> The Player the component will be sent to.</param> - /// <param name="visibleToGhosts"> Whether the component permits the icon to be visible to observers. </param> /// <returns></returns> - private bool CanGetState(ICommonSession? player, bool visibleToGhosts) + private bool CanGetState(ICommonSession? player) { //Apparently this can be null in replays so I am just returning true. - if (player is null) + if (player?.AttachedEntity is not {} uid) return true; - var uid = player.AttachedEntity; - if (HasComp<RevolutionaryComponent>(uid) || HasComp<HeadRevolutionaryComponent>(uid)) return true; - if (visibleToGhosts && HasComp<GhostComponent>(uid)) - return true; - - return HasComp<ShowRevIconsComponent>(uid); + return HasComp<ShowAntagIconsComponent>(uid); } /// <summary> /// Dirties all the Rev components so they are sent to clients. diff --git a/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs b/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs index 4329e437a29..9e4b51866f3 100644 --- a/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs +++ b/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs @@ -36,7 +36,7 @@ public sealed partial class RoboticsConsoleComponent : Component /// Radio message sent when destroying a borg. /// </summary> [DataField] - public LocId DestroyMessage = "robotics-console-cyborg-destroyed"; + public LocId DestroyMessage = "robotics-console-cyborg-destroying"; /// <summary> /// Cooldown on destroying borgs to prevent complete abuse. diff --git a/Content.Shared/Robotics/RoboticsConsoleUi.cs b/Content.Shared/Robotics/RoboticsConsoleUi.cs index 1be89beff0b..996c65cb0e6 100644 --- a/Content.Shared/Robotics/RoboticsConsoleUi.cs +++ b/Content.Shared/Robotics/RoboticsConsoleUi.cs @@ -97,6 +97,13 @@ public record struct CyborgControlData [DataField] public bool HasBrain; + /// <summary> + /// Whether the borg can currently be disabled if the brain is installed, + /// if on cooldown then can't queue up multiple disables. + /// </summary> + [DataField] + public bool CanDisable; + /// <summary> /// When this cyborg's data will be deleted. /// Set by the console when receiving the packet. @@ -104,7 +111,7 @@ public record struct CyborgControlData [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan Timeout = TimeSpan.Zero; - public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain) + public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain, bool canDisable) { ChassisSprite = chassisSprite; ChassisName = chassisName; @@ -112,6 +119,7 @@ public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, str Charge = charge; ModuleCount = moduleCount; HasBrain = hasBrain; + CanDisable = canDisable; } } diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index 5cf8cf38fb3..2313cc4bd08 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -106,8 +106,8 @@ public sealed partial class JobPrototype : IPrototype [DataField("jobEntity", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] public string? JobEntity = null; - [DataField("icon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))] - public string Icon { get; private set; } = "JobIconUnknown"; + [DataField] + public ProtoId<JobIconPrototype> Icon { get; private set; } = "JobIconUnknown"; [DataField("special", serverOnly: true)] public JobSpecial[] Special { get; private set; } = Array.Empty<JobSpecial>(); diff --git a/Content.Shared/Roles/RoleCodeword/RoleCodewordComponent.cs b/Content.Shared/Roles/RoleCodeword/RoleCodewordComponent.cs new file mode 100644 index 00000000000..a1723dbc7ec --- /dev/null +++ b/Content.Shared/Roles/RoleCodeword/RoleCodewordComponent.cs @@ -0,0 +1,37 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Roles.RoleCodeword; + +/// <summary> +/// Used to display and highlight codewords in chat messages on the client. +/// </summary> +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedRoleCodewordSystem), Other = AccessPermissions.Read)] +public sealed partial class RoleCodewordComponent : Component +{ + /// <summary> + /// Contains the codewords tied to a role. + /// Key string should be unique for the role. + /// </summary> + [DataField, AutoNetworkedField] + public Dictionary<string, CodewordsData> RoleCodewords = new(); + + public override bool SessionSpecific => true; +} + +[DataDefinition, Serializable, NetSerializable] +public partial struct CodewordsData +{ + [DataField] + public Color Color; + + [DataField] + public List<string> Codewords; + + public CodewordsData(Color color, List<string> codewords) + { + Color = color; + Codewords = codewords; + } +} diff --git a/Content.Shared/Roles/RoleCodeword/SharedRoleCodewordSystem.cs b/Content.Shared/Roles/RoleCodeword/SharedRoleCodewordSystem.cs new file mode 100644 index 00000000000..9f860715fbd --- /dev/null +++ b/Content.Shared/Roles/RoleCodeword/SharedRoleCodewordSystem.cs @@ -0,0 +1,49 @@ +using Content.Shared.Mind; +using Robust.Shared.GameStates; +using Robust.Shared.Player; + +namespace Content.Shared.Roles.RoleCodeword; + +public abstract class SharedRoleCodewordSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mindSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<RoleCodewordComponent, ComponentGetStateAttemptEvent>(OnCodewordCompGetStateAttempt); + } + + /// <summary> + /// Determines if a codeword component should be sent to the client. + /// </summary> + private void OnCodewordCompGetStateAttempt(EntityUid uid, RoleCodewordComponent comp, ref ComponentGetStateAttemptEvent args) + { + args.Cancelled = !CanGetState(args.Player, comp); + } + + /// <summary> + /// The criteria that determine whether a codeword component should be sent to a client. + /// Sends the component if its owner is the player mind. + /// </summary> + /// <param name="player"> The Player the component will be sent to.</param> + /// <param name="comp"> The component being checked against</param> + /// <returns></returns> + private bool CanGetState(ICommonSession? player, RoleCodewordComponent comp) + { + if (!_mindSystem.TryGetMind(player, out EntityUid mindId, out var _)) + return false; + + if (!TryComp(mindId, out RoleCodewordComponent? playerComp) && comp != playerComp) + return false; + + return true; + } + + public void SetRoleCodewords(RoleCodewordComponent comp, string key, List<string> codewords, Color color) + { + var data = new CodewordsData(color, codewords); + comp.RoleCodewords[key] = data; + } +} diff --git a/Content.Shared/Roles/StartingGearPrototype.cs b/Content.Shared/Roles/StartingGearPrototype.cs index 0ff65617bc9..883fe283a6b 100644 --- a/Content.Shared/Roles/StartingGearPrototype.cs +++ b/Content.Shared/Roles/StartingGearPrototype.cs @@ -1,11 +1,11 @@ -using Content.Shared.DeltaV.Harpy; using Content.Shared.Preferences; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; namespace Content.Shared.Roles; [Prototype("startingGear")] -public sealed partial class StartingGearPrototype : IPrototype +public sealed partial class StartingGearPrototype : IPrototype, IInheritingPrototype { [DataField] public Dictionary<string, EntProtoId> Equipment = new(); @@ -35,6 +35,14 @@ public sealed partial class StartingGearPrototype : IPrototype [IdDataField] public string ID { get; private set; } = string.Empty; + /// <inheritdoc/> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<StartingGearPrototype>))] + public string[]? Parents { get; private set; } + + /// <inheritdoc/> + [AbstractDataField] + public bool Abstract { get; } + public string GetGear(string slot, HumanoidCharacterProfile? profile) { if (profile != null) diff --git a/Content.Shared/SSDIndicator/SSDIndicatorComponent.cs b/Content.Shared/SSDIndicator/SSDIndicatorComponent.cs index 66310505a1a..634dfc397b3 100644 --- a/Content.Shared/SSDIndicator/SSDIndicatorComponent.cs +++ b/Content.Shared/SSDIndicator/SSDIndicatorComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.StatusIcon; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.SSDIndicator; @@ -16,6 +17,6 @@ public sealed partial class SSDIndicatorComponent : Component public bool IsSSD = true; [ViewVariables(VVAccess.ReadWrite)] - [DataField("icon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))] - public string Icon = "SSDIcon"; + [DataField] + public ProtoId<SsdIconPrototype> Icon = "SSDIcon"; } diff --git a/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs b/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs index 1ada22876b6..f94558b0b30 100644 --- a/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs +++ b/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs @@ -155,8 +155,7 @@ private void OnFultonInteract(EntityUid uid, FultonComponent component, AfterInt { CancelDuplicate = true, MovementThreshold = 0.5f, - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, Broadcast = true, NeedHand = true, }); diff --git a/Content.Shared/Security/Components/CriminalRecordComponent.cs b/Content.Shared/Security/Components/CriminalRecordComponent.cs index 25fab3f1350..aa6bd6ad5bd 100644 --- a/Content.Shared/Security/Components/CriminalRecordComponent.cs +++ b/Content.Shared/Security/Components/CriminalRecordComponent.cs @@ -11,5 +11,5 @@ public sealed partial class CriminalRecordComponent : Component /// The icon that should be displayed based on the criminal status of the entity. /// </summary> [DataField, AutoNetworkedField] - public ProtoId<StatusIconPrototype> StatusIcon = "SecurityIconWanted"; + public ProtoId<SecurityIconPrototype> StatusIcon = "SecurityIconWanted"; } diff --git a/Content.Shared/Sericulture/SericultureSystem.cs b/Content.Shared/Sericulture/SericultureSystem.cs index 514ec79f68e..f7586cc1ec3 100644 --- a/Content.Shared/Sericulture/SericultureSystem.cs +++ b/Content.Shared/Sericulture/SericultureSystem.cs @@ -61,7 +61,7 @@ private void OnSericultureStart(EntityUid uid, SericultureComponent comp, Sericu var doAfter = new DoAfterArgs(EntityManager, uid, comp.ProductionLength, new SericultureDoAfterEvent(), uid) { // I'm not sure if more things should be put here, but imo ideally it should probably be set in the component/YAML. Not sure if this is currently possible. - BreakOnUserMove = true, + BreakOnMove = true, BlockDuplicate = true, BreakOnDamage = true, CancelDuplicate = true, diff --git a/Content.Shared/Shadowkin/EtherealComponent.cs b/Content.Shared/Shadowkin/EtherealComponent.cs index 113f90df8e6..264cc0f1756 100644 --- a/Content.Shared/Shadowkin/EtherealComponent.cs +++ b/Content.Shared/Shadowkin/EtherealComponent.cs @@ -1,4 +1,7 @@ +using Content.Shared.NPC.Prototypes; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + namespace Content.Shared.Shadowkin; @@ -41,6 +44,6 @@ public sealed partial class EtherealComponent : Component public int OldMobLayer; - public List<string> SuppressedFactions = new(); + public List<ProtoId<NpcFactionPrototype>> SuppressedFactions = new(); public bool HasDoorBumpTag; } diff --git a/Content.Shared/Shadowkin/SharedEtherealSystem.cs b/Content.Shared/Shadowkin/SharedEtherealSystem.cs index 4d9a5e47b0d..5ab5e3eeeaf 100644 --- a/Content.Shared/Shadowkin/SharedEtherealSystem.cs +++ b/Content.Shared/Shadowkin/SharedEtherealSystem.cs @@ -16,6 +16,8 @@ using Content.Shared.Tag; using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; +using Content.Shared.Standing; + namespace Content.Shared.Shadowkin; @@ -27,6 +29,7 @@ public abstract class SharedEtherealSystem : EntitySystem [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly StandingStateSystem _standingState = default!; public override void Initialize() { diff --git a/Content.Shared/Shuttles/BUIStates/NavInterfaceState.cs b/Content.Shared/Shuttles/BUIStates/NavInterfaceState.cs index a6f4c01657d..70716742dd2 100644 --- a/Content.Shared/Shuttles/BUIStates/NavInterfaceState.cs +++ b/Content.Shared/Shuttles/BUIStates/NavInterfaceState.cs @@ -1,5 +1,6 @@ using Robust.Shared.Map; using Robust.Shared.Serialization; +using Content.Shared._NF.Shuttles.Events; // Frontier - InertiaDampeningMode access namespace Content.Shared.Shuttles.BUIStates; @@ -20,16 +21,23 @@ public sealed class NavInterfaceState public Dictionary<NetEntity, List<DockingPortState>> Docks; + /// <summary> + /// Frontier - the state of the shuttle's inertial dampeners + /// </summary> + public InertiaDampeningMode DampeningMode; + public NavInterfaceState( float maxRange, NetCoordinates? coordinates, Angle? angle, - Dictionary<NetEntity, List<DockingPortState>> docks) + Dictionary<NetEntity, List<DockingPortState>> docks, + InertiaDampeningMode dampeningMode) // Frontier: add dampeningMode { MaxRange = maxRange; Coordinates = coordinates; Angle = angle; Docks = docks; + DampeningMode = dampeningMode; // Frontier } } diff --git a/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs b/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs index a382e943ff9..f6929278ed7 100644 --- a/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs +++ b/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Whitelist; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; @@ -125,7 +126,7 @@ public bool CanDraw(EntityUid gridUid, PhysicsComponent? physics = null, IFFComp if (!Resolve(gridUid, ref physics)) return true; - if (physics.Mass < 10f) + if (physics.BodyType != BodyType.Static && physics.Mass < 10f) { return false; } diff --git a/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs b/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs index ced89e78608..c021baa86bc 100644 --- a/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs +++ b/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs @@ -51,7 +51,7 @@ private void TryStartup(EntityUid user, EntityUid target, DeadStartupButtonCompo var args = new DoAfterArgs(EntityManager, user, comp.DoAfterInterval, new OnDoAfterButtonPressedEvent(), target, target:target) { BreakOnDamage = true, - BreakOnUserMove = true, + BreakOnMove = true, }; _doAfterSystem.TryStartDoAfter(args); } diff --git a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs index e1776873da9..de0fe0bce38 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs @@ -15,12 +15,6 @@ namespace Content.Shared.Silicons.Borgs.Components; [RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem)), AutoGenerateComponentState] public sealed partial class BorgChassisComponent : Component { - /// <summary> - /// Whether or not the borg is activated, meaning it has access to modules and a heightened movement speed - /// </summary> - [DataField("activated"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public bool Activated; - #region Brain /// <summary> /// A whitelist for which entities count as valid brains @@ -68,7 +62,7 @@ public sealed partial class BorgChassisComponent : Component /// <summary> /// The currently selected module /// </summary> - [DataField("selectedModule")] + [DataField("selectedModule"), AutoNetworkedField] public EntityUid? SelectedModule; #region Visuals diff --git a/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs index 8c15e20d5d0..577056bb46d 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs @@ -23,12 +23,25 @@ public sealed partial class BorgTransponderComponent : Component public string Name = string.Empty; /// <summary> - /// Popup shown to everyone when a borg is disabled. + /// Popup shown to everyone after a borg is disabled. /// Gets passed a string "name". /// </summary> [DataField] public LocId DisabledPopup = "borg-transponder-disabled-popup"; + /// <summary> + /// Popup shown to the borg when it is being disabled. + /// </summary> + [DataField] + public LocId DisablingPopup = "borg-transponder-disabling-popup"; + + /// <summary> + /// Popup shown to everyone when a borg is being destroyed. + /// Gets passed a string "name". + /// </summary> + [DataField] + public LocId DestroyingPopup = "borg-transponder-destroying-popup"; + /// <summary> /// How long to wait between each broadcast. /// </summary> @@ -40,4 +53,28 @@ public sealed partial class BorgTransponderComponent : Component /// </summary> [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan NextBroadcast = TimeSpan.Zero; + + /// <summary> + /// When to next disable the borg. + /// </summary> + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? NextDisable; + + /// <summary> + /// How long to wait to disable the borg after RD has ordered it. + /// </summary> + [DataField] + public TimeSpan DisableDelay = TimeSpan.FromSeconds(5); + + /// <summary> + /// Pretend that the borg cannot be disabled due to being on delay. + /// </summary> + [DataField] + public bool FakeDisabling; + + /// <summary> + /// Pretend that the borg has no brain inserted. + /// </summary> + [DataField] + public bool FakeDisabled; } diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs index 0431d95a42f..c62e63481d6 100644 --- a/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs +++ b/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs @@ -1,10 +1,12 @@ -using Content.Shared.Access.Components; using Content.Shared.Containers.ItemSlots; +using Content.Shared.IdentityManagement; +using Content.Shared.Item.ItemToggle; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.PowerCell.Components; using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.UserInterface; using Content.Shared.Wires; using Robust.Shared.Containers; @@ -17,6 +19,7 @@ public abstract partial class SharedBorgSystem : EntitySystem { [Dependency] protected readonly SharedContainerSystem Container = default!; [Dependency] protected readonly ItemSlotsSystem ItemSlots = default!; + [Dependency] protected readonly ItemToggleSystem Toggle = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; /// <inheritdoc/> @@ -30,10 +33,28 @@ public override void Initialize() SubscribeLocalEvent<BorgChassisComponent, EntInsertedIntoContainerMessage>(OnInserted); SubscribeLocalEvent<BorgChassisComponent, EntRemovedFromContainerMessage>(OnRemoved); SubscribeLocalEvent<BorgChassisComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers); + SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt); + SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo); InitializeRelay(); } + private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent args) + { + if (args.Handled) + { + return; + } + + if (!HasComp<BorgChassisComponent>(args.ForActor)) + { + return; + } + + args.Title = Name(args.ForActor).Trim(); + args.Handled = true; + } + private void OnItemSlotInsertAttempt(EntityUid uid, BorgChassisComponent component, ref ItemSlotInsertAttemptEvent args) { if (args.Cancelled) @@ -75,6 +96,13 @@ private void OnStartup(EntityUid uid, BorgChassisComponent component, ComponentS component.ModuleContainer = Container.EnsureContainer<Container>(uid, component.ModuleContainerId, containerManager); } + private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args) + { + // borgs can't view their own ui + if (args.User == uid) + args.Cancel(); + } + protected virtual void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args) { @@ -87,7 +115,7 @@ protected virtual void OnRemoved(EntityUid uid, BorgChassisComponent component, private void OnRefreshMovementSpeedModifiers(EntityUid uid, BorgChassisComponent component, RefreshMovementSpeedModifiersEvent args) { - if (component.Activated) + if (Toggle.IsActivated(uid)) return; if (!TryComp<MovementSpeedModifierComponent>(uid, out var movement)) diff --git a/Content.Shared/Silicons/Laws/SiliconLawEditEuiState.cs b/Content.Shared/Silicons/Laws/SiliconLawEditEuiState.cs new file mode 100644 index 00000000000..b5b35837153 --- /dev/null +++ b/Content.Shared/Silicons/Laws/SiliconLawEditEuiState.cs @@ -0,0 +1,29 @@ +using Content.Shared.Eui; +using Robust.Shared.Serialization; + +namespace Content.Shared.Silicons.Laws; + +[Serializable, NetSerializable] +public sealed class SiliconLawsEuiState : EuiStateBase +{ + public List<SiliconLaw> Laws { get; } + public NetEntity Target { get; } + public SiliconLawsEuiState(List<SiliconLaw> laws, NetEntity target) + { + Laws = laws; + Target = target; + } +} + +[Serializable, NetSerializable] +public sealed class SiliconLawsSaveMessage : EuiMessageBase +{ + public List<SiliconLaw> Laws { get; } + public NetEntity Target { get; } + + public SiliconLawsSaveMessage(List<SiliconLaw> laws, NetEntity target) + { + Laws = laws; + Target = target; + } +} diff --git a/Content.Shared/Silicons/StationAi/StationAiOverlayComponent.cs b/Content.Shared/Silicons/StationAi/StationAiOverlayComponent.cs new file mode 100644 index 00000000000..8416d44d5a1 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiOverlayComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Silicons.StationAi; + +/// <summary> +/// Handles the static overlay for station AI. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class StationAiOverlayComponent : Component; diff --git a/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs b/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs new file mode 100644 index 00000000000..94aef8ad366 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Silicons.StationAi; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]//, Access(typeof(SharedStationAiSystem))] +public sealed partial class StationAiVisionComponent : Component +{ + [DataField, AutoNetworkedField] + public bool Enabled = true; + + [DataField, AutoNetworkedField] + public bool Occluded = true; + + /// <summary> + /// Range in tiles + /// </summary> + [DataField, AutoNetworkedField] + public float Range = 7.5f; +} diff --git a/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs b/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs new file mode 100644 index 00000000000..c144f330e11 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs @@ -0,0 +1,522 @@ +using Robust.Shared.Map.Components; +using Robust.Shared.Threading; +using Robust.Shared.Utility; + +namespace Content.Shared.Silicons.StationAi; + +public sealed class StationAiVisionSystem : EntitySystem +{ + /* + * This class handles 2 things: + * 1. It handles general "what tiles are visible" line of sight checks. + * 2. It does single-tile lookups to tell if they're visible or not with support for a faster range-only path. + */ + + [Dependency] private readonly IParallelManager _parallel = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedMapSystem _maps = default!; + [Dependency] private readonly SharedTransformSystem _xforms = default!; + + private SeedJob _seedJob; + private ViewJob _job; + + private readonly HashSet<Entity<OccluderComponent>> _occluders = new(); + private readonly HashSet<Entity<StationAiVisionComponent>> _seeds = new(); + private readonly HashSet<Vector2i> _viewportTiles = new(); + + // Dummy set + private readonly HashSet<Vector2i> _singleTiles = new(); + + // Occupied tiles per-run. + // For now it's only 1-grid supported but updating to TileRefs if required shouldn't be too hard. + private readonly HashSet<Vector2i> _opaque = new(); + + /// <summary> + /// Do we skip line of sight checks and just check vision ranges. + /// </summary> + private bool FastPath; + + /// <summary> + /// Have we found the target tile if we're only checking for a single one. + /// </summary> + private bool TargetFound; + + public override void Initialize() + { + base.Initialize(); + + _seedJob = new() + { + System = this, + }; + + _job = new ViewJob() + { + EntManager = EntityManager, + Maps = _maps, + System = this, + }; + } + + /// <summary> + /// Returns whether a tile is accessible based on vision. + /// </summary> + public bool IsAccessible(Entity<MapGridComponent> grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false) + { + _viewportTiles.Clear(); + _opaque.Clear(); + _seeds.Clear(); + _viewportTiles.Add(tile); + var localBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize); + var expandedBounds = localBounds.Enlarged(expansionSize); + + _seedJob.Grid = grid; + _seedJob.ExpandedBounds = expandedBounds; + _parallel.ProcessNow(_seedJob); + _job.Data.Clear(); + FastPath = fastPath; + + foreach (var seed in _seeds) + { + if (!seed.Comp.Enabled) + continue; + + _job.Data.Add(seed); + } + + if (_seeds.Count == 0) + return false; + + // Skip occluders step if we're just doing range checks. + if (!fastPath) + { + var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, expandedBounds, ignoreEmpty: false); + + // Get all other relevant tiles. + while (tileEnumerator.MoveNext(out var tileRef)) + { + if (IsOccluded(grid, tileRef.GridIndices)) + { + _opaque.Add(tileRef.GridIndices); + } + } + } + + for (var i = _job.Vis1.Count; i < _job.Data.Count; i++) + { + _job.Vis1.Add(new Dictionary<Vector2i, int>()); + _job.Vis2.Add(new Dictionary<Vector2i, int>()); + _job.SeedTiles.Add(new HashSet<Vector2i>()); + _job.BoundaryTiles.Add(new HashSet<Vector2i>()); + } + + _job.TargetTile = tile; + TargetFound = false; + _singleTiles.Clear(); + _job.Grid = grid; + _job.VisibleTiles = _singleTiles; + _parallel.ProcessNow(_job, _job.Data.Count); + + return TargetFound; + } + + private bool IsOccluded(Entity<MapGridComponent> grid, Vector2i tile) + { + var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(-0.05f); + _occluders.Clear(); + _lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static); + var anyOccluders = false; + + foreach (var occluder in _occluders) + { + if (!occluder.Comp.Enabled) + continue; + + anyOccluders = true; + break; + } + + return anyOccluders; + } + + /// <summary> + /// Gets a byond-equivalent for tiles in the specified worldAABB. + /// </summary> + /// <param name="expansionSize">How much to expand the bounds before to find vision intersecting it. Makes this the largest vision size + 1 tile.</param> + public void GetView(Entity<MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f) + { + _viewportTiles.Clear(); + _opaque.Clear(); + _seeds.Clear(); + var expandedBounds = worldBounds.Enlarged(expansionSize); + + // TODO: Would be nice to be able to run this while running the other stuff. + _seedJob.Grid = grid; + var localAABB = _xforms.GetInvWorldMatrix(grid).TransformBox(expandedBounds); + _seedJob.ExpandedBounds = localAABB; + _parallel.ProcessNow(_seedJob); + _job.Data.Clear(); + FastPath = false; + + foreach (var seed in _seeds) + { + if (!seed.Comp.Enabled) + continue; + + _job.Data.Add(seed); + } + + if (_seeds.Count == 0) + return; + + // Get viewport tiles + var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false); + + while (tileEnumerator.MoveNext(out var tileRef)) + { + if (IsOccluded(grid, tileRef.GridIndices)) + { + _opaque.Add(tileRef.GridIndices); + } + + _viewportTiles.Add(tileRef.GridIndices); + } + + tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false); + + // Get all other relevant tiles. + while (tileEnumerator.MoveNext(out var tileRef)) + { + if (_viewportTiles.Contains(tileRef.GridIndices)) + continue; + + if (IsOccluded(grid, tileRef.GridIndices)) + { + _opaque.Add(tileRef.GridIndices); + } + } + + // Wait for seed job here + + for (var i = _job.Vis1.Count; i < _job.Data.Count; i++) + { + _job.Vis1.Add(new Dictionary<Vector2i, int>()); + _job.Vis2.Add(new Dictionary<Vector2i, int>()); + _job.SeedTiles.Add(new HashSet<Vector2i>()); + _job.BoundaryTiles.Add(new HashSet<Vector2i>()); + } + + _job.TargetTile = null; + TargetFound = false; + _job.Grid = grid; + _job.VisibleTiles = visibleTiles; + _parallel.ProcessNow(_job, _job.Data.Count); + } + + private int GetMaxDelta(Vector2i tile, Vector2i center) + { + var delta = tile - center; + return Math.Max(Math.Abs(delta.X), Math.Abs(delta.Y)); + } + + private int GetSumDelta(Vector2i tile, Vector2i center) + { + var delta = tile - center; + return Math.Abs(delta.X) + Math.Abs(delta.Y); + } + + /// <summary> + /// Checks if any of a tile's neighbors are visible. + /// </summary> + private bool CheckNeighborsVis( + Dictionary<Vector2i, int> vis, + Vector2i index, + int d) + { + for (var x = -1; x <= 1; x++) + { + for (var y = -1; y <= 1; y++) + { + if (x == 0 && y == 0) + continue; + + var neighbor = index + new Vector2i(x, y); + var neighborD = vis.GetValueOrDefault(neighbor); + + if (neighborD == d) + return true; + } + } + return false; + } + + /// Checks whether this tile fits the definition of a "corner" + /// </summary> + private bool IsCorner( + HashSet<Vector2i> tiles, + HashSet<Vector2i> blocked, + Dictionary<Vector2i, int> vis1, + Vector2i index, + Vector2i delta) + { + var diagonalIndex = index + delta; + + if (!tiles.TryGetValue(diagonalIndex, out var diagonal)) + return false; + + var cardinal1 = new Vector2i(index.X, diagonal.Y); + var cardinal2 = new Vector2i(diagonal.X, index.Y); + + return vis1.GetValueOrDefault(diagonal) != 0 && + vis1.GetValueOrDefault(cardinal1) != 0 && + vis1.GetValueOrDefault(cardinal2) != 0 && + blocked.Contains(cardinal1) && + blocked.Contains(cardinal2) && + !blocked.Contains(diagonal); + } + + /// <summary> + /// Gets the relevant vision seeds for later. + /// </summary> + private record struct SeedJob() : IRobustJob + { + public StationAiVisionSystem System; + + public Entity<MapGridComponent> Grid; + public Box2 ExpandedBounds; + + public void Execute() + { + System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds); + } + } + + private record struct ViewJob() : IParallelRobustJob + { + public int BatchSize => 1; + + public IEntityManager EntManager; + public SharedMapSystem Maps; + public StationAiVisionSystem System; + + public Entity<MapGridComponent> Grid; + public List<Entity<StationAiVisionComponent>> Data = new(); + + // If we're doing range-checks might be able to early out + public Vector2i? TargetTile; + + public HashSet<Vector2i> VisibleTiles; + + public readonly List<Dictionary<Vector2i, int>> Vis1 = new(); + public readonly List<Dictionary<Vector2i, int>> Vis2 = new(); + + public readonly List<HashSet<Vector2i>> SeedTiles = new(); + public readonly List<HashSet<Vector2i>> BoundaryTiles = new(); + + public void Execute(int index) + { + // If we're looking for a single tile then early-out if someone else has found it. + if (TargetTile != null) + { + lock (System) + { + if (System.TargetFound) + { + return; + } + } + } + + var seed = Data[index]; + var seedXform = EntManager.GetComponent<TransformComponent>(seed); + + // Fastpath just get tiles in range. + // Either xray-vision or system is doing a quick-and-dirty check. + if (!seed.Comp.Occluded || System.FastPath) + { + var squircles = Maps.GetLocalTilesIntersecting(Grid.Owner, + Grid.Comp, + new Circle(System._xforms.GetWorldPosition(seedXform), seed.Comp.Range), ignoreEmpty: false); + + // Try to find the target tile. + if (TargetTile != null) + { + foreach (var tile in squircles) + { + if (tile.GridIndices == TargetTile) + { + lock (System) + { + System.TargetFound = true; + } + + return; + } + } + } + else + { + lock (VisibleTiles) + { + foreach (var tile in squircles) + { + VisibleTiles.Add(tile.GridIndices); + } + } + } + + return; + } + + // Code based upon https://github.com/OpenDreamProject/OpenDream/blob/c4a3828ccb997bf3722673620460ebb11b95ccdf/OpenDreamShared/Dream/ViewAlgorithm.cs + + var range = seed.Comp.Range; + var vis1 = Vis1[index]; + var vis2 = Vis2[index]; + + var seedTiles = SeedTiles[index]; + var boundary = BoundaryTiles[index]; + + // Cleanup last run + vis1.Clear(); + vis2.Clear(); + + seedTiles.Clear(); + boundary.Clear(); + + var maxDepthMax = 0; + var sumDepthMax = 0; + + var eyePos = Maps.GetTileRef(Grid.Owner, Grid, seedXform.Coordinates).GridIndices; + + for (var x = Math.Floor(eyePos.X - range); x <= eyePos.X + range; x++) + { + for (var y = Math.Floor(eyePos.Y - range); y <= eyePos.Y + range; y++) + { + var tile = new Vector2i((int)x, (int)y); + var delta = tile - eyePos; + var xDelta = Math.Abs(delta.X); + var yDelta = Math.Abs(delta.Y); + + var deltaSum = xDelta + yDelta; + + maxDepthMax = Math.Max(maxDepthMax, Math.Max(xDelta, yDelta)); + sumDepthMax = Math.Max(sumDepthMax, deltaSum); + seedTiles.Add(tile); + } + } + + // Step 3, Diagonal shadow loop + for (var d = 0; d < maxDepthMax; d++) + { + foreach (var tile in seedTiles) + { + var maxDelta = System.GetMaxDelta(tile, eyePos); + + if (maxDelta == d + 1 && System.CheckNeighborsVis(vis2, tile, d)) + { + vis2[tile] = (System._opaque.Contains(tile) ? -1 : d + 1); + } + } + } + + // Step 4, Straight shadow loop + for (var d = 0; d < sumDepthMax; d++) + { + foreach (var tile in seedTiles) + { + var sumDelta = System.GetSumDelta(tile, eyePos); + + if (sumDelta == d + 1 && System.CheckNeighborsVis(vis1, tile, d)) + { + if (System._opaque.Contains(tile)) + { + vis1[tile] = -1; + } + else if (vis2.GetValueOrDefault(tile) != 0) + { + vis1[tile] = d + 1; + } + } + } + } + + // Add the eye itself + vis1[eyePos] = 1; + + // Step 6. + + // Step 7. + + // Step 8. + foreach (var tile in seedTiles) + { + vis2[tile] = vis1.GetValueOrDefault(tile, 0); + } + + // Step 9 + foreach (var tile in seedTiles) + { + if (!System._opaque.Contains(tile)) + continue; + + var tileVis1 = vis1.GetValueOrDefault(tile); + + if (tileVis1 != 0) + continue; + + if (System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.UpRight) || + System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.UpLeft) || + System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.DownLeft) || + System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.DownRight)) + { + boundary.Add(tile); + } + } + + // Make all wall/corner tiles visible + foreach (var tile in boundary) + { + vis1[tile] = -1; + } + + if (TargetTile != null) + { + if (vis1.TryGetValue(TargetTile.Value, out var tileVis)) + { + DebugTools.Assert(seedTiles.Contains(TargetTile.Value)); + + if (tileVis != 0) + { + lock (System) + { + System.TargetFound = true; + return; + } + } + } + } + else + { + // vis2 is what we care about for LOS. + foreach (var tile in seedTiles) + { + // If not in viewport don't care. + if (!System._viewportTiles.Contains(tile)) + continue; + + var tileVis = vis1.GetValueOrDefault(tile, 0); + + if (tileVis != 0) + { + // No idea if it's better to do this inside or out. + lock (VisibleTiles) + { + VisibleTiles.Add(tile); + } + } + } + } + } + } +} diff --git a/Content.Shared/Sound/SharedEmitSoundSystem.cs b/Content.Shared/Sound/SharedEmitSoundSystem.cs index a9a50698d7d..998a07d8f80 100644 --- a/Content.Shared/Sound/SharedEmitSoundSystem.cs +++ b/Content.Shared/Sound/SharedEmitSoundSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Popups; using Content.Shared.Sound.Components; using Content.Shared.Throwing; +using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -34,6 +35,7 @@ public abstract class SharedEmitSoundSystem : EntitySystem [Dependency] private readonly SharedAmbientSoundSystem _ambient = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -123,7 +125,7 @@ private void OnEmitSoundOnDrop(EntityUid uid, EmitSoundOnDropComponent component private void OnEmitSoundOnInteractUsing(Entity<EmitSoundOnInteractUsingComponent> ent, ref InteractUsingEvent args) { - if (ent.Comp.Whitelist.IsValid(args.Used, EntityManager)) + if (_whitelistSystem.IsWhitelistPass(ent.Comp.Whitelist, args.Used)) { TryEmitSound(ent, ent.Comp, args.User); } diff --git a/Content.Shared/Species/Systems/ReformSystem.cs b/Content.Shared/Species/Systems/ReformSystem.cs index dc2ce3fb900..d2ceecf28e8 100644 --- a/Content.Shared/Species/Systems/ReformSystem.cs +++ b/Content.Shared/Species/Systems/ReformSystem.cs @@ -69,7 +69,7 @@ private void OnReform(EntityUid uid, ReformComponent comp, ReformEvent args) // Create a doafter & start it var doAfter = new DoAfterArgs(EntityManager, uid, comp.ReformTime, new ReformDoAfterEvent(), uid) { - BreakOnUserMove = true, + BreakOnMove = true, BlockDuplicate = true, BreakOnDamage = true, CancelDuplicate = true, @@ -89,7 +89,7 @@ private void OnDoAfter(EntityUid uid, ReformComponent comp, ReformDoAfterEvent a return; // Spawn a new entity - // This is, to an extent, taken from polymorph. I don't use polymorph for various reasons- most notably that this is permanent. + // This is, to an extent, taken from polymorph. I don't use polymorph for various reasons- most notably that this is permanent. var child = Spawn(comp.ReformPrototype, Transform(uid).Coordinates); // This transfers the mind to the new entity @@ -105,7 +105,7 @@ private void OnZombified(EntityUid uid, ReformComponent comp, ref EntityZombifie _actionsSystem.RemoveAction(uid, comp.ActionEntity); // Zombies can't reform } - public sealed partial class ReformEvent : InstantActionEvent { } + public sealed partial class ReformEvent : InstantActionEvent { } [Serializable, NetSerializable] public sealed partial class ReformDoAfterEvent : SimpleDoAfterEvent { } diff --git a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs index fa04a50f8b0..feb1cebb8e1 100644 --- a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs +++ b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs @@ -134,8 +134,7 @@ private void OnAirlockInteract(Entity<PaintableAirlockComponent> ent, ref Intera var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.AirlockSprayTime, new SprayPainterDoorDoAfterEvent(sprite, style.Department), args.Used, target: ent, used: args.Used) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = true }; diff --git a/Content.Shared/Stacks/SharedStackSystem.cs b/Content.Shared/Stacks/SharedStackSystem.cs index e12edd323c7..7fe058afbaa 100644 --- a/Content.Shared/Stacks/SharedStackSystem.cs +++ b/Content.Shared/Stacks/SharedStackSystem.cs @@ -223,6 +223,9 @@ public bool TryMergeToContacts(EntityUid uid, StackComponent? stack = null, Tran foreach (var otherStack in intersecting) { var otherEnt = otherStack.Owner; + // if you merge a ton of stacks together, you will end up deleting a few by accident. + if (TerminatingOrDeleted(otherEnt) || EntityManager.IsQueuedForDeletion(otherEnt)) + continue; if (!TryMergeStacks(uid, otherEnt, out _, stack, otherStack)) continue; diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index e889b5c9746..4a2ad625af9 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -115,7 +115,7 @@ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingG if (raiseEvent) { var ev = new StartingGearEquippedEvent(entity); - RaiseLocalEvent(entity, ref ev, true); + RaiseLocalEvent(entity, ref ev); } } } diff --git a/Content.Shared/StationRecords/GeneralRecordsUi.cs b/Content.Shared/StationRecords/GeneralRecordsUi.cs index 860454efde5..2105c53df25 100644 --- a/Content.Shared/StationRecords/GeneralRecordsUi.cs +++ b/Content.Shared/StationRecords/GeneralRecordsUi.cs @@ -37,17 +37,22 @@ public sealed class GeneralStationRecordConsoleState : BoundUserInterfaceState public readonly GeneralStationRecord? Record; public readonly Dictionary<uint, string>? RecordListing; public readonly StationRecordsFilter? Filter; + public readonly bool CanDeleteEntries; - public GeneralStationRecordConsoleState(uint? key, GeneralStationRecord? record, - Dictionary<uint, string>? recordListing, StationRecordsFilter? newFilter) + public GeneralStationRecordConsoleState(uint? key, + GeneralStationRecord? record, + Dictionary<uint, string>? recordListing, + StationRecordsFilter? newFilter, + bool canDeleteEntries) { SelectedKey = key; Record = record; RecordListing = recordListing; Filter = newFilter; + CanDeleteEntries = canDeleteEntries; } - public GeneralStationRecordConsoleState() : this(null, null, null, null) + public GeneralStationRecordConsoleState() : this(null, null, null, null, false) { } @@ -69,3 +74,15 @@ public SelectStationRecord(uint? selectedKey) SelectedKey = selectedKey; } } + + +[Serializable, NetSerializable] +public sealed class DeleteStationRecord : BoundUserInterfaceMessage +{ + public DeleteStationRecord(uint id) + { + Id = id; + } + + public readonly uint Id; +} diff --git a/Content.Shared/StatusIcon/Components/StatusIconComponent.cs b/Content.Shared/StatusIcon/Components/StatusIconComponent.cs index 385f9760c65..c56be7c96a5 100644 --- a/Content.Shared/StatusIcon/Components/StatusIconComponent.cs +++ b/Content.Shared/StatusIcon/Components/StatusIconComponent.cs @@ -24,16 +24,4 @@ public sealed partial class StatusIconComponent : Component /// </summary> /// <param name="StatusIcons"></param> [ByRefEvent] -public record struct GetStatusIconsEvent(List<StatusIconData> StatusIcons, bool InContainer); - -/// <summary> -/// Event raised on the Client-side to determine whether to display a status icon on an entity. -/// </summary> -/// <param name="User">The player that will see the icons</param> -[ByRefEvent] -public record struct CanDisplayStatusIconsEvent(EntityUid? User = null) -{ - public EntityUid? User = User; - - public bool Cancelled = false; -} +public record struct GetStatusIconsEvent(List<StatusIconData> StatusIcons); diff --git a/Content.Shared/StatusIcon/StatusIconPrototype.cs b/Content.Shared/StatusIcon/StatusIconPrototype.cs index c5a5fd8a2c5..689bec3882b 100644 --- a/Content.Shared/StatusIcon/StatusIconPrototype.cs +++ b/Content.Shared/StatusIcon/StatusIconPrototype.cs @@ -1,3 +1,5 @@ +using Content.Shared.Stealth.Components; +using Content.Shared.Whitelist; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; @@ -15,57 +17,198 @@ public partial class StatusIconData : IComparable<StatusIconData> /// <summary> /// The icon that's displayed on the entity. /// </summary> - [DataField("icon", required: true)] + [DataField(required: true)] public SpriteSpecifier Icon = default!; /// <summary> /// A priority for the order in which the icons will be displayed. /// </summary> - [DataField("priority")] + [DataField] public int Priority = 10; + /// <summary> + /// Whether or not to hide the icon to ghosts + /// </summary> + [DataField] + public bool VisibleToGhosts = true; + + /// <summary> + /// Whether or not to hide the icon when we are inside a container like a locker or a crate. + /// </summary> + [DataField] + public bool HideInContainer = true; + + /// <summary> + /// Whether or not to hide the icon when the entity has an active <see cref="StealthComponent"/> + /// </summary> + [DataField] + public bool HideOnStealth = true; + + /// <summary> + /// Specifies what entities and components/tags this icon can be shown to. + /// </summary> + [DataField] + public EntityWhitelist? ShowTo; + /// <summary> /// A preference for where the icon will be displayed. None | Left | Right /// </summary> - [DataField("locationPreference")] + [DataField] public StatusIconLocationPreference LocationPreference = StatusIconLocationPreference.None; + /// <summary> + /// The layer the icon is displayed on. Mod is drawn above Base. Base | Mod + /// </summary> + [DataField] + public StatusIconLayer Layer = StatusIconLayer.Base; + + /// <summary> + /// Sets if the icon should be rendered with or without the effect of lighting. + /// </summary> + [DataField] + public bool IsShaded = false; + public int CompareTo(StatusIconData? other) { return Priority.CompareTo(other?.Priority ?? int.MaxValue); } +} + +/// <summary> +/// <see cref="StatusIconData"/> but in new convenient prototype form! +/// </summary> +public abstract partial class StatusIconPrototype : StatusIconData, IPrototype +{ + /// <inheritdoc/> + [IdDataField] + public string ID { get; private set; } = default!; +} + +/// <summary> +/// StatusIcons for showing jobs on the sec HUD +/// </summary> +[Prototype] +public sealed partial class JobIconPrototype : StatusIconPrototype, IInheritingPrototype +{ + /// <inheritdoc /> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<JobIconPrototype>))] + public string[]? Parents { get; } + + /// <inheritdoc /> + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } /// <summary> - /// The layer the icon is displayed on. Mod is drawn above Base. Base | Mod + /// Name of the icon used for menu tooltips. /// </summary> [DataField] - public StatusIconLayer Layer = StatusIconLayer.Base; + public string JobName { get; private set; } = string.Empty; + + [ViewVariables(VVAccess.ReadOnly)] + public string LocalizedJobName => Loc.GetString(JobName); /// <summary> - /// Sets if the icon should be rendered with or without the effect of lighting. + /// Should the agent ID or ID card console be able to use this job icon? /// </summary> [DataField] - public bool IsShaded; + public bool AllowSelection = true; } /// <summary> -/// <see cref="StatusIconData"/> but in new convenient prototype form! +/// StatusIcons for the med HUD /// </summary> -[Prototype("statusIcon")] -public sealed partial class StatusIconPrototype : StatusIconData, IPrototype, IInheritingPrototype +[Prototype] +public sealed partial class HealthIconPrototype : StatusIconPrototype, IInheritingPrototype { /// <inheritdoc /> - [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<StatusIconPrototype>))] + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<HealthIconPrototype>))] public string[]? Parents { get; } /// <inheritdoc /> [NeverPushInheritance] [AbstractDataField] public bool Abstract { get; } +} - /// <inheritdoc/> - [IdDataField] - public string ID { get; private set; } = default!; +/// <summary> +/// StatusIcons for the beer goggles and fried onion goggles +/// </summary> +[Prototype] +public sealed partial class SatiationIconPrototype : StatusIconPrototype, IInheritingPrototype +{ + /// <inheritdoc /> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<SatiationIconPrototype>))] + public string[]? Parents { get; } + + /// <inheritdoc /> + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } +} + +/// <summary> +/// StatusIcons for showing the wanted status on the sec HUD +/// </summary> +[Prototype] +public sealed partial class SecurityIconPrototype : StatusIconPrototype, IInheritingPrototype +{ + /// <inheritdoc /> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<SecurityIconPrototype>))] + public string[]? Parents { get; } + + /// <inheritdoc /> + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } +} + +/// <summary> +/// StatusIcons for faction membership +/// </summary> +[Prototype] +public sealed partial class FactionIconPrototype : StatusIconPrototype, IInheritingPrototype +{ + /// <inheritdoc /> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<FactionIconPrototype>))] + public string[]? Parents { get; } + + /// <inheritdoc /> + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } +} + +/// <summary> +/// StatusIcons for debugging purposes +/// </summary> +[Prototype] +public sealed partial class DebugIconPrototype : StatusIconPrototype, IInheritingPrototype +{ + /// <inheritdoc /> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<DebugIconPrototype>))] + public string[]? Parents { get; } + + /// <inheritdoc /> + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } +} + +/// <summary> +/// StatusIcons for the SSD indicator +/// </summary> +[Prototype] +public sealed partial class SsdIconPrototype : StatusIconPrototype, IInheritingPrototype +{ + /// <inheritdoc /> + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<SsdIconPrototype>))] + public string[]? Parents { get; } + + /// <inheritdoc /> + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } } [Serializable, NetSerializable] diff --git a/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs b/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs index d0cd5c4b4e2..5bb303cf517 100644 --- a/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs +++ b/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Gravity; using Content.Shared.StepTrigger.Components; +using Content.Shared.Whitelist; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; @@ -12,6 +13,7 @@ public sealed class StepTriggerSystem : EntitySystem [Dependency] private readonly EntityLookupSystem _entityLookup = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -62,7 +64,7 @@ private bool Update(EntityUid uid, StepTriggerComponent component, TransformComp if (ent == uid) continue; - if (component.Blacklist.IsValid(ent.Value, EntityManager)) + if (_whitelistSystem.IsBlacklistPass(component.Blacklist, ent.Value)) return false; } } diff --git a/Content.Shared/Storage/Components/ItemCounterComponent.cs b/Content.Shared/Storage/Components/ItemCounterComponent.cs index 890bc84e721..6a1444ebf62 100644 --- a/Content.Shared/Storage/Components/ItemCounterComponent.cs +++ b/Content.Shared/Storage/Components/ItemCounterComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Storage.EntitySystems; +using Content.Shared.Storage.EntitySystems; using Content.Shared.Whitelist; namespace Content.Shared.Storage.Components diff --git a/Content.Shared/Storage/Components/StoreAfterFailedInteractComponent.cs b/Content.Shared/Storage/Components/StoreAfterFailedInteractComponent.cs new file mode 100644 index 00000000000..578da52dfc1 --- /dev/null +++ b/Content.Shared/Storage/Components/StoreAfterFailedInteractComponent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Storage.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class StoreAfterFailedInteractComponent : Component +{ +} diff --git a/Content.Shared/Storage/EntitySystems/BinSystem.cs b/Content.Shared/Storage/EntitySystems/BinSystem.cs index 1cc95337ea4..8c2cb4c4fcb 100644 --- a/Content.Shared/Storage/EntitySystems/BinSystem.cs +++ b/Content.Shared/Storage/EntitySystems/BinSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Examine; @@ -7,6 +7,7 @@ using Content.Shared.Item; using Content.Shared.Storage.Components; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Network; @@ -21,6 +22,7 @@ public sealed class BinSystem : EntitySystem [Dependency] private readonly ISharedAdminLogManager _admin = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public const string BinContainerId = "bin-container"; @@ -130,7 +132,7 @@ public bool TryInsertIntoBin(EntityUid uid, EntityUid toInsert, BinComponent? co if (component.Items.Count >= component.MaxItems) return false; - if (component.Whitelist != null && !component.Whitelist.IsValid(toInsert)) + if (_whitelistSystem.IsWhitelistFail(component.Whitelist, toInsert)) return false; _container.Insert(toInsert, component.ItemContainer); diff --git a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs index da7c3aa89a0..91acde47e19 100644 --- a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs +++ b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs @@ -132,8 +132,7 @@ private void StartDoAfter(EntityUid storageUid, EntityUid targetUid, EntityUid u _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, userUid, delay, new DumpableDoAfterEvent(), storageUid, target: targetUid, used: storageUid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }); } diff --git a/Content.Shared/Storage/EntitySystems/MagnetPickupSystem.cs b/Content.Shared/Storage/EntitySystems/MagnetPickupSystem.cs index bb1efba8692..7a8961485d6 100644 --- a/Content.Shared/Storage/EntitySystems/MagnetPickupSystem.cs +++ b/Content.Shared/Storage/EntitySystems/MagnetPickupSystem.cs @@ -80,7 +80,7 @@ public override void Update(float frameTime) // the problem is that stack pickups delete the original entity, which is fine, but due to // game state handling we can't show a lerp animation for it. var nearXform = Transform(near); - var nearMap = nearXform.MapPosition; + var nearMap = _transform.GetMapCoordinates(near, xform: nearXform); var nearCoords = EntityCoordinates.FromMap(moverCoords.EntityId, nearMap, _transform, EntityManager); if (!_storage.Insert(uid, near, out var stacked, storageComp: storage, playSound: !playedSound)) diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs index bb49725e047..60ae813e580 100644 --- a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -487,9 +487,6 @@ private void ModifyComponents(EntityUid uid, SharedEntityStorageComponent? compo } } - if (TryComp<PlaceableSurfaceComponent>(uid, out var surface)) - _placeableSurface.SetPlaceable(uid, component.Open, surface); - _appearance.SetData(uid, StorageVisuals.Open, component.Open); _appearance.SetData(uid, StorageVisuals.HasContents, component.Contents.ContainedEntities.Count > 0); } diff --git a/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs b/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs index a5e151f4ea6..2557f8100c6 100644 --- a/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs @@ -1,5 +1,6 @@ -using System.Linq; +using System.Linq; using Content.Shared.Storage.Components; +using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Containers; @@ -15,6 +16,7 @@ public abstract class SharedItemMapperSystem : EntitySystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; /// <inheritdoc /> public override void Initialize() @@ -93,7 +95,7 @@ private bool TryGetLayers(EntityUid uid, var list = new List<string>(); foreach (var mapLayerData in itemMapper.MapLayers.Values) { - var count = containedLayers.Count(ent => mapLayerData.ServerWhitelist.IsValid(ent)); + var count = containedLayers.Count(ent => _whitelistSystem.IsWhitelistPass(mapLayerData.ServerWhitelist, ent)); if (count >= mapLayerData.MinCount && count <= mapLayerData.MaxCount) { list.Add(mapLayerData.Layer); diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index fad9d724388..868d26c3aea 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -2,7 +2,9 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; +using Content.Shared.Administration.Logs; using Content.Shared.Containers.ItemSlots; +using Content.Shared.Database; using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.Hands.Components; @@ -14,7 +16,6 @@ using Content.Shared.Inventory; using Content.Shared.Item; using Content.Shared.Lock; -using Content.Shared.Nyanotrasen.Item.PseudoItem; using Content.Shared.Materials; using Content.Shared.Placeable; using Content.Shared.Popups; @@ -23,6 +24,7 @@ using Content.Shared.Timing; using Content.Shared.Verbs; using Content.Shared.Whitelist; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -57,6 +59,7 @@ public abstract class SharedStorageSystem : EntitySystem [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; [Dependency] protected readonly UseDelaySystem UseDelay = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLog = default!; private EntityQuery<ItemComponent> _itemQuery; private EntityQuery<StackComponent> _stackQuery; @@ -66,6 +69,9 @@ public abstract class SharedStorageSystem : EntitySystem public const string DefaultStorageMaxItemSize = "Normal"; public const float AreaInsertDelayPerItem = 0.075f; + private static AudioParams _audioParams = AudioParams.Default + .WithMaxDistance(7f) + .WithVolume(-2f); private ItemSizePrototype _defaultStorageMaxItemSize = default!; @@ -97,6 +103,7 @@ public override void Initialize() subs.Event<BoundUIClosedEvent>(OnBoundUIClosed); }); + SubscribeLocalEvent<StorageComponent, ComponentRemove>(OnRemove); SubscribeLocalEvent<StorageComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb); SubscribeLocalEvent<StorageComponent, ComponentGetState>(OnStorageGetState); @@ -134,6 +141,11 @@ public override void Initialize() UpdatePrototypeCache(); } + private void OnRemove(Entity<StorageComponent> entity, ref ComponentRemove args) + { + _ui.CloseUi(entity.Owner, StorageComponent.StorageUiKey.Key); + } + private void OnMapInit(Entity<StorageComponent> entity, ref MapInitEvent args) { UseDelay.SetLength(entity.Owner, entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID); @@ -154,7 +166,9 @@ private void OnStorageGetState(EntityUid uid, StorageComponent component, ref Co Grid = new List<Box2i>(component.Grid), MaxItemSize = component.MaxItemSize, StoredItems = storedItems, - SavedLocations = component.SavedLocations + SavedLocations = component.SavedLocations, + Whitelist = component.Whitelist, + Blacklist = component.Blacklist }; } @@ -166,6 +180,8 @@ private void OnStorageHandleState(EntityUid uid, StorageComponent component, ref component.Grid.Clear(); component.Grid.AddRange(state.Grid); component.MaxItemSize = state.MaxItemSize; + component.Whitelist = state.Whitelist; + component.Blacklist = state.Blacklist; component.StoredItems.Clear(); @@ -362,14 +378,18 @@ private void OnInteractUsing(EntityUid uid, StorageComponent storageComp, Intera /// </summary> private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args) { - if (args.Handled || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert)) + if (args.Handled || !args.Complex || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert)) return; // Toggle if (_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User)) + { _ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User); + } else + { OpenStorageUI(uid, args.User, storageComp, false); + } args.Handled = true; } @@ -429,7 +449,7 @@ private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInt var doAfterArgs = new DoAfterArgs(EntityManager, args.User, delay, new AreaPickupDoAfterEvent(GetNetEntityList(_entList)), uid, target: uid) { BreakOnDamage = true, - BreakOnUserMove = true, + BreakOnMove = true, NeedHand = true }; @@ -453,7 +473,7 @@ private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInt return; } - if (_xformQuery.TryGetComponent(uid, out var transformOwner) && TryComp<TransformComponent>(target, out var transformEnt)) + if (TryComp(uid, out TransformComponent? transformOwner) && TryComp(target, out TransformComponent? transformEnt)) { var parent = transformOwner.ParentUid; @@ -529,7 +549,7 @@ private void OnDoAfter(EntityUid uid, StorageComponent component, AreaPickupDoAf // If we picked up at least one thing, play a sound and do a cool animation! if (successfullyInserted.Count > 0) { - Audio.PlayPredicted(component.StorageInsertSound, uid, args.User); + Audio.PlayPredicted(component.StorageInsertSound, uid, args.User, _audioParams); EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent( GetNetEntity(uid), GetNetEntityList(successfullyInserted), @@ -560,151 +580,79 @@ private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionE /// </summary> private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not { } player) - return; - - var uid = GetEntity(msg.StorageUid); - var entity = GetEntity(msg.InteractedItemUid); - - if (!TryComp<StorageComponent>(uid, out var storageComp)) - return; - - if (!_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, player)) - return; - - if (!Exists(entity)) - { - Log.Error($"Player {args.SenderSession} interacted with non-existent item {msg.InteractedItemUid} stored in {ToPrettyString(uid)}"); - return; - } - - if (!ActionBlocker.CanInteract(player, entity) || !storageComp.Container.Contains(entity)) - return; - - // Does the player have hands? - if (!TryComp(player, out HandsComponent? hands) || hands.Count == 0) + if (!ValidateInput(args, msg.StorageUid, msg.InteractedItemUid, out var player, out var storage, out var item)) return; // If the user's active hand is empty, try pick up the item. - if (hands.ActiveHandEntity == null) + if (player.Comp.ActiveHandEntity == null) { - if (_sharedHandsSystem.TryPickupAnyHand(player, entity, handsComp: hands) - && storageComp.StorageRemoveSound != null) - Audio.PlayPredicted(storageComp.StorageRemoveSound, uid, player); + _adminLog.Add( + LogType.Storage, + LogImpact.Low, + $"{ToPrettyString(player):player} is attempting to take {ToPrettyString(item):item} out of {ToPrettyString(storage):storage}"); + + if (_sharedHandsSystem.TryPickupAnyHand(player, item, handsComp: player.Comp) + && storage.Comp.StorageRemoveSound != null) { - return; + Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams); } + + return; } + _adminLog.Add( + LogType.Storage, + LogImpact.Low, + $"{ToPrettyString(player):player} is interacting with {ToPrettyString(item):item} while it is stored in {ToPrettyString(storage):storage} using {ToPrettyString(player.Comp.ActiveHandEntity):used}"); + // Else, interact using the held item - _interactionSystem.InteractUsing(player, hands.ActiveHandEntity.Value, entity, Transform(entity).Coordinates, checkCanInteract: false); + _interactionSystem.InteractUsing(player, player.Comp.ActiveHandEntity.Value, item, Transform(item).Coordinates, checkCanInteract: false); } private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not { } player) + if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item)) return; - var storageEnt = GetEntity(msg.StorageEnt); - var itemEnt = GetEntity(msg.ItemEnt); - - if (!TryComp<StorageComponent>(storageEnt, out var storageComp)) - return; + _adminLog.Add( + LogType.Storage, + LogImpact.Low, + $"{ToPrettyString(player):player} is updating the location of {ToPrettyString(item):item} within {ToPrettyString(storage):storage}"); - if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player)) - return; - - if (!Exists(itemEnt)) - { - Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); - return; - } - - if (!ActionBlocker.CanInteract(player, itemEnt)) - return; - - TrySetItemStorageLocation((itemEnt, null), (storageEnt, storageComp), msg.Location); + TrySetItemStorageLocation(item!, storage!, msg.Location); } private void OnRemoveItem(StorageRemoveItemEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not { } player) - return; - - var storageEnt = GetEntity(msg.StorageEnt); - var itemEnt = GetEntity(msg.ItemEnt); - - if (!TryComp<StorageComponent>(storageEnt, out var storageComp)) + if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item)) return; - if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player)) - return; - - if (!Exists(itemEnt)) - { - Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); - return; - } - - if (!ActionBlocker.CanInteract(player, itemEnt)) - return; - - TransformSystem.DropNextTo(itemEnt, player); - Audio.PlayPredicted(storageComp.StorageRemoveSound, storageEnt, player); + _adminLog.Add( + LogType.Storage, + LogImpact.Low, + $"{ToPrettyString(player):player} is removing {ToPrettyString(item):item} from {ToPrettyString(storage):storage}"); + TransformSystem.DropNextTo(item.Owner, player.Owner); + Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams); } private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not { } player) - return; - - var storageEnt = GetEntity(msg.StorageEnt); - var itemEnt = GetEntity(msg.ItemEnt); - - if (!TryComp<StorageComponent>(storageEnt, out var storageComp)) - return; - - if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player)) - return; - - if (!Exists(itemEnt)) - { - Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); + if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true)) return; - } - if (!ActionBlocker.CanInteract(player, itemEnt) || !_sharedHandsSystem.IsHolding(player, itemEnt, out _)) - return; - - InsertAt((storageEnt, storageComp), (itemEnt, null), msg.Location, out _, player, stackAutomatically: false); + _adminLog.Add( + LogType.Storage, + LogImpact.Low, + $"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}"); + InsertAt(storage!, item!, msg.Location, out _, player, stackAutomatically: false); } - // TODO: if/when someone cleans up this shitcode please make all these - // handlers use a shared helper for checking that the ui is open etc, thanks private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not {} player) - return; - - var storage = GetEntity(msg.Storage); - var item = GetEntity(msg.Item); - - if (!HasComp<StorageComponent>(storage)) - return; - - if (!_ui.IsUiOpen(storage, StorageComponent.StorageUiKey.Key, player)) - return; - - if (!Exists(item)) - { - Log.Error($"Player {args.SenderSession} saved location of non-existent item {msg.Item} stored in {ToPrettyString(storage)}"); + if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item, held: true)) return; - } - if (!ActionBlocker.CanInteract(player, item)) - return; - - SaveItemLocation(storage, item); + SaveItemLocation(storage!, item.Owner); } private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args) @@ -793,7 +741,11 @@ public void UpdateAppearance(Entity<StorageComponent?, AppearanceComponent?> ent _appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance); _appearance.SetData(uid, StorageVisuals.Open, isOpen, appearance); _appearance.SetData(uid, SharedBagOpenVisuals.BagState, isOpen ? SharedBagState.Open : SharedBagState.Closed, appearance); - _appearance.SetData(uid, StackVisuals.Hide, !isOpen, appearance); + + // HideClosedStackVisuals true sets the StackVisuals.Hide to the open state of the storage. + // This is for containers that only show their contents when open. (e.g. donut boxes) + if (storage.HideStackVisualsWhenClosed) + _appearance.SetData(uid, StackVisuals.Hide, !isOpen, appearance); } /// <summary> @@ -816,13 +768,10 @@ public void TransferEntities(EntityUid source, EntityUid target, EntityUid? user foreach (var entity in entities.ToArray()) { - if (HasComp<PseudoItemComponent>(entity)) // Nyanotrasen - They dont transfer properly - continue; - Insert(target, entity, out _, user: user, targetComp, playSound: false); } - Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user); + Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user, _audioParams); } /// <summary> @@ -1001,7 +950,7 @@ public bool Insert( return false; if (playSound) - Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user); + Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams); return true; } @@ -1031,7 +980,7 @@ public bool Insert( } if (playSound) - Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user); + Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams); return true; } @@ -1380,7 +1329,12 @@ public ItemSizePrototype GetMaxItemSize(Entity<StorageComponent?> uid) // If we specify a max item size, use that if (uid.Comp.MaxItemSize != null) - return _prototype.Index(uid.Comp.MaxItemSize.Value); + { + if (_prototype.TryIndex(uid.Comp.MaxItemSize.Value, out var proto)) + return proto; + + Log.Error($"{ToPrettyString(uid.Owner)} tried to get invalid item size prototype: {uid.Comp.MaxItemSize.Value}. Stack trace:\\n{Environment.StackTrace}"); + } if (!_itemQuery.TryGetComponent(uid, out var item)) return _defaultStorageMaxItemSize; @@ -1408,7 +1362,7 @@ private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockTo private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args) { - if (_containerSystem.TryGetContainingContainer(uid, out var container, component) && + if (_containerSystem.TryGetContainingContainer((uid, null, component), out var container) && container.ID == StorageComponent.ContainerId) { UpdateAppearance(container.Owner); @@ -1418,15 +1372,15 @@ private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, Sta private void HandleOpenBackpack(ICommonSession? session) { - HandleOpenSlotUI(session, "back"); + HandleToggleSlotUI(session, "back"); } private void HandleOpenBelt(ICommonSession? session) { - HandleOpenSlotUI(session, "belt"); + HandleToggleSlotUI(session, "belt"); } - private void HandleOpenSlotUI(ICommonSession? session, string slot) + private void HandleToggleSlotUI(ICommonSession? session, string slot) { if (session is not { } playerSession) return; @@ -1441,9 +1395,13 @@ private void HandleOpenSlotUI(ICommonSession? session, string slot) return; if (!_ui.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt)) + { OpenStorageUI(storageEnt.Value, playerEnt, silent: false); + } else + { _ui.CloseUi(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt); + } } protected void ClearCantFillReasons() @@ -1473,6 +1431,79 @@ private bool CanInteract(EntityUid user, Entity<StorageComponent> storage, bool public abstract void PlayPickupAnimation(EntityUid uid, EntityCoordinates initialCoordinates, EntityCoordinates finalCoordinates, Angle initialRotation, EntityUid? user = null); + private bool ValidateInput( + EntitySessionEventArgs args, + NetEntity netStorage, + out Entity<HandsComponent> player, + out Entity<StorageComponent> storage) + { + player = default; + storage = default; + + if (args.SenderSession.AttachedEntity is not { } playerUid) + return false; + + if (!TryComp(playerUid, out HandsComponent? hands) || hands.Count == 0) + return false; + + if (!TryGetEntity(netStorage, out var storageUid)) + return false; + + if (!TryComp(storageUid, out StorageComponent? storageComp)) + return false; + + // TODO STORAGE use BUI events + // This would automatically validate that the UI is open & that the user can interact. + // However, we still need to manually validate that items being used are in the users hands or in the storage. + if (!_ui.IsUiOpen(storageUid.Value, StorageComponent.StorageUiKey.Key, playerUid)) + return false; + + if (!ActionBlocker.CanInteract(playerUid, storageUid)) + return false; + + player = new(playerUid, hands); + storage = new(storageUid.Value, storageComp); + return true; + } + + private bool ValidateInput(EntitySessionEventArgs args, + NetEntity netStorage, + NetEntity netItem, + out Entity<HandsComponent> player, + out Entity<StorageComponent> storage, + out Entity<ItemComponent> item, + bool held = false) + { + item = default!; + if (!ValidateInput(args, netStorage, out player, out storage)) + return false; + + if (!TryGetEntity(netItem, out var itemUid)) + return false; + + if (held) + { + if (!_sharedHandsSystem.IsHolding(player, itemUid, out _)) + return false; + } + else + { + if (!storage.Comp.Container.Contains(itemUid.Value)) + return false; + + DebugTools.Assert(storage.Comp.StoredItems.ContainsKey(itemUid.Value)); + } + + if (!TryComp(itemUid, out ItemComponent? itemComp)) + return false; + + if (!ActionBlocker.CanInteract(player, itemUid)) + return false; + + item = new(itemUid.Value, itemComp); + return true; + } + [Serializable, NetSerializable] protected sealed class StorageComponentState : ComponentState { @@ -1483,5 +1514,9 @@ protected sealed class StorageComponentState : ComponentState public List<Box2i> Grid = new(); public ProtoId<ItemSizePrototype>? MaxItemSize; + + public EntityWhitelist? Whitelist; + + public EntityWhitelist? Blacklist; } } diff --git a/Content.Shared/Storage/EntitySystems/StoreAfterFailedInteractSystem.cs b/Content.Shared/Storage/EntitySystems/StoreAfterFailedInteractSystem.cs new file mode 100644 index 00000000000..77f72806f4b --- /dev/null +++ b/Content.Shared/Storage/EntitySystems/StoreAfterFailedInteractSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Storage.Components; +using Content.Shared.Storage.Events; + +namespace Content.Shared.Storage.EntitySystems; + +public sealed class StoreAfterFailedInteractSystem : EntitySystem +{ + [Dependency] private readonly SharedStorageSystem _storage = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent<StoreAfterFailedInteractComponent, StorageInsertFailedEvent>(OnStorageInsertFailed); + } + + private void OnStorageInsertFailed(Entity<StoreAfterFailedInteractComponent> ent, ref StorageInsertFailedEvent args) + { + _storage.PlayerInsertHeldEntity(args.Storage, args.Player); + } +} diff --git a/Content.Shared/Storage/Events/StorageInsertFailedEvent.cs b/Content.Shared/Storage/Events/StorageInsertFailedEvent.cs new file mode 100644 index 00000000000..50f759e43fd --- /dev/null +++ b/Content.Shared/Storage/Events/StorageInsertFailedEvent.cs @@ -0,0 +1,6 @@ +using Content.Shared.Hands.Components; + +namespace Content.Shared.Storage.Events; + +[ByRefEvent] +public record struct StorageInsertFailedEvent(Entity<StorageComponent?> Storage, Entity<HandsComponent?> Player); diff --git a/Content.Shared/Storage/StorageComponent.cs b/Content.Shared/Storage/StorageComponent.cs index 2860f8dacfe..d2c607e57f7 100644 --- a/Content.Shared/Storage/StorageComponent.cs +++ b/Content.Shared/Storage/StorageComponent.cs @@ -123,6 +123,14 @@ public sealed partial class StorageComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public StorageDefaultOrientation? DefaultStorageOrientation; + /// <summary> + /// If true, sets StackVisuals.Hide to true when the container is closed + /// Used in cases where there are sprites that are shown when the container is open but not + /// when it is closed + /// </summary> + [DataField] + public bool HideStackVisualsWhenClosed = true; + [Serializable, NetSerializable] public enum StorageUiKey : byte { @@ -230,6 +238,9 @@ public AnimateInsertingEntitiesEvent(NetEntity storage, List<NetEntity> storedEn [ByRefEvent] public record struct StorageInteractAttemptEvent(bool Silent, bool Cancelled = false); + [ByRefEvent] + public record struct StorageInteractUsingAttemptEvent(bool Cancelled = false); + [NetSerializable] [Serializable] public enum StorageVisuals : byte diff --git a/Content.Server/Store/Components/StoreComponent.cs b/Content.Shared/Store/Components/StoreComponent.cs similarity index 66% rename from Content.Server/Store/Components/StoreComponent.cs rename to Content.Shared/Store/Components/StoreComponent.cs index 0b7dbbea094..e5171dec418 100644 --- a/Content.Server/Store/Components/StoreComponent.cs +++ b/Content.Shared/Store/Components/StoreComponent.cs @@ -1,46 +1,40 @@ using Content.Shared.FixedPoint; -using Content.Shared.Store; using Robust.Shared.Audio; -using Robust.Shared.Map; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; -namespace Content.Server.Store.Components; +namespace Content.Shared.Store.Components; /// <summary> /// This component manages a store which players can use to purchase different listings /// through the ui. The currency, listings, and categories are defined in yaml. /// </summary> -[RegisterComponent] +[RegisterComponent, NetworkedComponent] public sealed partial class StoreComponent : Component { - /// <summary> - /// The default preset for the store. Is overriden by default values specified on the component. - /// </summary> - [DataField("preset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))] - public string? Preset; + [DataField] + public LocId Name = "store-ui-default-title"; /// <summary> /// All the listing categories that are available on this store. /// The available listings are partially based on the categories. /// </summary> - [DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StoreCategoryPrototype>))] - public HashSet<string> Categories = new(); + [DataField] + public HashSet<ProtoId<StoreCategoryPrototype>> Categories = new(); /// <summary> /// The total amount of currency that can be used in the store. /// The string represents the ID of te currency prototype, where the /// float is that amount. /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("balance", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))] - public Dictionary<string, FixedPoint2> Balance = new(); + [DataField] + public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Balance = new(); /// <summary> /// The list of currencies that can be inserted into this store. /// </summary> - [ViewVariables(VVAccess.ReadOnly), DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<CurrencyPrototype>))] - public HashSet<string> CurrencyWhitelist = new(); + [DataField] + public HashSet<ProtoId<CurrencyPrototype>> CurrencyWhitelist = new(); /// <summary> /// The person who "owns" the store/account. Used if you want the listings to be fixed @@ -50,15 +44,16 @@ public sealed partial class StoreComponent : Component public EntityUid? AccountOwner = null; /// <summary> - /// All listings, including those that aren't available to the buyer + /// Cached list of listings items with modifiers. /// </summary> - public HashSet<ListingData> Listings = new(); + [DataField] + public HashSet<ListingDataWithCostModifiers> FullListingsCatalog = new(); /// <summary> /// All available listings from the last time that it was checked. /// </summary> [ViewVariables] - public HashSet<ListingData> LastAvailableListings = new(); + public HashSet<ListingDataWithCostModifiers> LastAvailableListings = new(); /// <summary> /// All current entities bought from this shop. Useful for keeping track of refunds and upgrades. @@ -70,7 +65,7 @@ public sealed partial class StoreComponent : Component /// The total balance spent in this store. Used for refunds. /// </summary> [ViewVariables, DataField] - public Dictionary<string, FixedPoint2> BalanceSpent = new(); + public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> BalanceSpent = new(); /// <summary> /// Controls if the store allows refunds @@ -95,7 +90,7 @@ public sealed partial class StoreComponent : Component /// <summary> /// The sound played to the buyer when a purchase is succesfully made. /// </summary> - [DataField("buySuccessSound")] + [DataField] public SoundSpecifier BuySuccessSound = new SoundPathSpecifier("/Audio/Effects/kaching.ogg"); #endregion } diff --git a/Content.Shared/Store/ListingLocalisationHelpers.cs b/Content.Shared/Store/ListingLocalisationHelpers.cs index 19cd029488f..882300109ce 100644 --- a/Content.Shared/Store/ListingLocalisationHelpers.cs +++ b/Content.Shared/Store/ListingLocalisationHelpers.cs @@ -18,11 +18,6 @@ public static string GetLocalisedNameOrEntityName(ListingData listingData, IProt else if (listingData.ProductEntity != null) name = prototypeManager.Index(listingData.ProductEntity.Value).Name; - if (listingData.DiscountValue > 0) - name += " " + Loc.GetString("store-sales-amount", ("amount", listingData.DiscountValue)); - else if (listingData.OldCost.Count > 0) - name += " " + Loc.GetString("store-sales-over"); - return name; } diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs index 445b5742dd1..05ac5cc4cd5 100644 --- a/Content.Shared/Store/ListingPrototype.cs +++ b/Content.Shared/Store/ListingPrototype.cs @@ -1,5 +1,7 @@ using System.Linq; using Content.Shared.FixedPoint; +using Content.Shared.Store.Components; +using Content.Shared.StoreDiscount.Components; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -13,8 +15,77 @@ namespace Content.Shared.Store; /// </summary> [Serializable, NetSerializable] [Virtual, DataDefinition] -public partial class ListingData : IEquatable<ListingData>, ICloneable +public partial class ListingData : IEquatable<ListingData> { + public ListingData() + { + } + + public ListingData(ListingData other) : this( + other.Name, + other.DiscountCategory, + other.Description, + other.Conditions, + other.Icon, + other.Priority, + other.ProductEntity, + other.ProductAction, + other.ProductUpgradeId, + other.ProductActionEntity, + other.ProductEvent, + other.RaiseProductEventOnUser, + other.PurchaseAmount, + other.ID, + other.Categories, + other.OriginalCost, + other.RestockTime, + other.DiscountDownTo + ) + { + + } + + public ListingData( + string? name, + ProtoId<DiscountCategoryPrototype>? discountCategory, + string? description, + List<ListingCondition>? conditions, + SpriteSpecifier? icon, + int priority, + EntProtoId? productEntity, + EntProtoId? productAction, + ProtoId<ListingPrototype>? productUpgradeId, + EntityUid? productActionEntity, + object? productEvent, + bool raiseProductEventOnUser, + int purchaseAmount, + string id, + HashSet<ProtoId<StoreCategoryPrototype>> categories, + IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> originalCost, + TimeSpan restockTime, + Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> dataDiscountDownTo + ) + { + Name = name; + DiscountCategory = discountCategory; + Description = description; + Conditions = conditions?.ToList(); + Icon = icon; + Priority = priority; + ProductEntity = productEntity; + ProductAction = productAction; + ProductUpgradeId = productUpgradeId; + ProductActionEntity = productActionEntity; + ProductEvent = productEvent; + RaiseProductEventOnUser = raiseProductEventOnUser; + PurchaseAmount = purchaseAmount; + ID = id; + Categories = categories.ToHashSet(); + OriginalCost = originalCost; + RestockTime = restockTime; + DiscountDownTo = new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(dataDiscountDownTo); + } + [ViewVariables] [IdDataField] public string ID { get; private set; } = default!; @@ -25,6 +96,12 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable [DataField] public string? Name; + /// <summary> + /// Discount category for listing item. This marker describes chance of how often will item be discounted. + /// </summary> + [DataField] + public ProtoId<DiscountCategoryPrototype>? DiscountCategory; + /// <summary> /// The description of the listing. If empty, uses the entity's description (if present) /// </summary> @@ -35,13 +112,15 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable /// The categories that this listing applies to. Used for filtering a listing for a store. /// </summary> [DataField] - public List<ProtoId<StoreCategoryPrototype>> Categories = new(); + public HashSet<ProtoId<StoreCategoryPrototype>> Categories = new(); /// <summary> - /// The cost of the listing. String represents the currency type while the FixedPoint2 represents the amount of that currency. + /// The original cost of the listing. FixedPoint2 represents the amount of that currency. + /// This fields should not be used for getting actual cost of item, as there could be + /// cost modifiers (due to discounts or surplus). Use Cost property on derived class instead. /// </summary> [DataField] - public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Cost = new(); + public IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> OriginalCost = new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(); /// <summary> /// Specific customizable conditions that determine whether or not the listing can be purchased. @@ -109,18 +188,11 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable [DataField] public TimeSpan RestockTime = TimeSpan.Zero; + /// <summary> + /// Options for discount - from max amount down to how much item costs can be cut by discount, absolute value. + /// </summary> [DataField] - public int SaleLimit = 3; - - [DataField] - public bool SaleBlacklist; - - public int DiscountValue; - - public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> OldCost = new(); - - [DataField] - public List<string> Components = new(); + public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> DiscountDownTo = new(); public bool Equals(ListingData? listing) { @@ -145,7 +217,7 @@ public bool Equals(ListingData? listing) if (!Categories.OrderBy(x => x).SequenceEqual(listing.Categories.OrderBy(x => x))) return false; - if (!Cost.OrderBy(x => x).SequenceEqual(listing.Cost.OrderBy(x => x))) + if (!OriginalCost.OrderBy(x => x).SequenceEqual(listing.OriginalCost.OrderBy(x => x))) return false; if ((Conditions != null && listing.Conditions != null) && @@ -155,43 +227,208 @@ public bool Equals(ListingData? listing) return true; } +} + +/// <summary> +/// Defines a set item listing that is available in a store +/// </summary> +[Prototype("listing")] +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class ListingPrototype : ListingData, IPrototype +{ + /// <summary> Setter/getter for item cost from prototype. </summary> + [DataField] + public IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Cost + { + get => OriginalCost; + set => OriginalCost = value; + } +} + +/// <summary> Wrapper around <see cref="ListingData"/> that enables controller and centralized cost modification. </summary> +/// <remarks> +/// Server lifecycle of those objects is bound to <see cref="StoreComponent.FullListingsCatalog"/>, which is their local cache. To fix +/// cost changes after server side change (for example, when all items with set discount are bought up) <see cref="ApplyAllModifiers"/> is called +/// on changes. +/// Client side lifecycle is possible due to modifiers and original cost being transferred fields and cost being calculated when needed. Modifiers changes +/// should not (are not expected) be happening on client. +/// </remarks> +[Serializable, NetSerializable, DataDefinition] +public sealed partial class ListingDataWithCostModifiers : ListingData +{ + private IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2>? _costModified; + + /// <summary> + /// Map of values, by which calculated cost should be modified, with modification sourceId. + /// Instead of modifying this field - use <see cref="RemoveCostModifier"/> and <see cref="AddCostModifier"/> + /// when possible. + /// </summary> + [DataField] + public Dictionary<string, Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>> CostModifiersBySourceId = new(); + + /// <inheritdoc /> + public ListingDataWithCostModifiers(ListingData listingData) + : base( + listingData.Name, + listingData.DiscountCategory, + listingData.Description, + listingData.Conditions, + listingData.Icon, + listingData.Priority, + listingData.ProductEntity, + listingData.ProductAction, + listingData.ProductUpgradeId, + listingData.ProductActionEntity, + listingData.ProductEvent, + listingData.RaiseProductEventOnUser, + listingData.PurchaseAmount, + listingData.ID, + listingData.Categories, + listingData.OriginalCost, + listingData.RestockTime, + listingData.DiscountDownTo + ) + { + } + + /// <summary> Marker, if cost of listing item have any modifiers. </summary> + public bool IsCostModified => CostModifiersBySourceId.Count > 0; + + /// <summary> Cost of listing item after applying all available modifiers. </summary> + public IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Cost + { + get + { + return _costModified ??= CostModifiersBySourceId.Count == 0 + ? new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(OriginalCost) + : ApplyAllModifiers(); + } + } + + /// <summary> Add map with currencies and value by which cost should be modified when final value is calculated. </summary> + /// <param name="modifierSourceId">Id of modifier source. Can be used for removing modifier later.</param> + /// <param name="modifiers">Values for cost modification.</param> + public void AddCostModifier(string modifierSourceId, Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> modifiers) + { + CostModifiersBySourceId.Add(modifierSourceId, modifiers); + if (_costModified != null) + { + _costModified = ApplyAllModifiers(); + } + } + + /// <summary> Remove cost modifier with passed sourceId. </summary> + public void RemoveCostModifier(string modifierSourceId) + { + CostModifiersBySourceId.Remove(modifierSourceId); + if (_costModified != null) + { + _costModified = ApplyAllModifiers(); + } + } + + /// <summary> Check if listing item can be bought with passed balance. </summary> + public bool CanBuyWith(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance) + { + foreach (var (currency, amount) in Cost) + { + if (!balance.ContainsKey(currency)) + return false; + + if (balance[currency] < amount) + return false; + } + + return true; + } + /// <summary> - /// Creates a unique instance of a listing. ALWAWYS USE THIS WHEN ENUMERATING LISTING PROTOTYPES - /// DON'T BE DUMB AND MODIFY THE PROTOTYPES + /// Gets percent of reduced/increased cost that modifiers give respective to <see cref="ListingData.OriginalCost"/>. + /// Percent values are numbers between 0 and 1. /// </summary> - /// <returns>A unique copy of the listing data.</returns> - public object Clone() + public IReadOnlyDictionary<ProtoId<CurrencyPrototype>, float> GetModifiersSummaryRelative() { - return new ListingData + var modifiersSummaryAbsoluteValues = CostModifiersBySourceId.Aggregate( + new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(), + (accumulator, x) => + { + foreach (var (currency, amount) in x.Value) + { + accumulator.TryGetValue(currency, out var accumulatedAmount); + accumulator[currency] = accumulatedAmount + amount; + } + + return accumulator; + } + ); + var relativeModifiedPercent = new Dictionary<ProtoId<CurrencyPrototype>, float>(); + foreach (var (currency, discountAmount) in modifiersSummaryAbsoluteValues) { - ID = ID, - Name = Name, - Description = Description, - Categories = Categories, - Cost = Cost, - Conditions = Conditions, - Icon = Icon, - Priority = Priority, - ProductEntity = ProductEntity, - ProductAction = ProductAction, - ProductUpgradeId = ProductUpgradeId, - ProductActionEntity = ProductActionEntity, - ProductEvent = ProductEvent, - PurchaseAmount = PurchaseAmount, - RestockTime = RestockTime, - SaleLimit = SaleLimit, - SaleBlacklist = SaleBlacklist, - DiscountValue = DiscountValue, - OldCost = OldCost, - Components = Components, - }; + if (OriginalCost.TryGetValue(currency, out var originalAmount)) + { + var discountPercent = (float)discountAmount.Value / originalAmount.Value; + relativeModifiedPercent.Add(currency, discountPercent); + } + } + + return relativeModifiedPercent; + + } + + private Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> ApplyAllModifiers() + { + var dictionary = new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(OriginalCost); + foreach (var (_, modifier) in CostModifiersBySourceId) + { + ApplyModifier(dictionary, modifier); + } + + return dictionary; + } + + private void ApplyModifier( + Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> applyTo, + IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> modifier + ) + { + foreach (var (currency, modifyBy) in modifier) + { + if (applyTo.TryGetValue(currency, out var currentAmount)) + { + var modifiedAmount = currentAmount + modifyBy; + if (modifiedAmount < 0) + { + modifiedAmount = 0; + // no negative cost allowed + } + applyTo[currency] = modifiedAmount; + } + } } } /// <summary> -/// Defines a set item listing that is available in a store +/// Defines set of rules for category of discounts - +/// how <see cref="StoreDiscountComponent"/> will be filled by respective system. /// </summary> -[Prototype("listing")] -[Serializable, NetSerializable] -[DataDefinition] -public sealed partial class ListingPrototype : ListingData, IPrototype; +[Prototype("discountCategory")] +[DataDefinition, Serializable, NetSerializable] +public sealed partial class DiscountCategoryPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + /// <summary> + /// Weight that sets chance to roll discount of that category. + /// </summary> + [DataField] + public int Weight { get; private set; } + + /// <summary> + /// Maximum amount of items that are allowed to be picked from this category. + /// </summary> + [DataField] + public int? MaxItems { get; private set; } +} diff --git a/Content.Shared/Store/StoreBuyFinishedEvent.cs b/Content.Shared/Store/StoreBuyFinishedEvent.cs new file mode 100644 index 00000000000..2661d7f20a8 --- /dev/null +++ b/Content.Shared/Store/StoreBuyFinishedEvent.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.Store; + + +/// <summary> +/// Event of successfully finishing purchase in store (<see cref="StoreSystem"/>. +/// </summary> +/// <param name="StoreUid">EntityUid on which store is placed.</param> +/// <param name="PurchasedItem">ListingItem that was purchased.</param> +[ByRefEvent] +public readonly record struct StoreBuyFinishedEvent( + EntityUid Buyer, + EntityUid StoreUid, + ListingDataWithCostModifiers PurchasedItem +); diff --git a/Content.Shared/Store/StoreUi.cs b/Content.Shared/Store/StoreUi.cs index ee4da6991f6..d8cb9e6ca88 100644 --- a/Content.Shared/Store/StoreUi.cs +++ b/Content.Shared/Store/StoreUi.cs @@ -13,7 +13,7 @@ public enum StoreUiKey : byte [Serializable, NetSerializable] public sealed class StoreUpdateState : BoundUserInterfaceState { - public readonly HashSet<ListingData> Listings; + public readonly HashSet<ListingDataWithCostModifiers> Listings; public readonly Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Balance; @@ -21,7 +21,7 @@ public sealed class StoreUpdateState : BoundUserInterfaceState public readonly bool AllowRefund; - public StoreUpdateState(HashSet<ListingData> listings, Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance, bool showFooter, bool allowRefund) + public StoreUpdateState(HashSet<ListingDataWithCostModifiers> listings, Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance, bool showFooter, bool allowRefund) { Listings = listings; Balance = balance; @@ -30,20 +30,6 @@ public StoreUpdateState(HashSet<ListingData> listings, Dictionary<ProtoId<Curren } } -/// <summary> -/// initializes miscellaneous data about the store. -/// </summary> -[Serializable, NetSerializable] -public sealed class StoreInitializeState : BoundUserInterfaceState -{ - public readonly string Name; - - public StoreInitializeState(string name) - { - Name = name; - } -} - [Serializable, NetSerializable] public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessage { @@ -51,14 +37,9 @@ public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessa } [Serializable, NetSerializable] -public sealed class StoreBuyListingMessage : BoundUserInterfaceMessage +public sealed class StoreBuyListingMessage(ProtoId<ListingPrototype> listing) : BoundUserInterfaceMessage { - public ListingData Listing; - - public StoreBuyListingMessage(ListingData listing) - { - Listing = listing; - } + public ProtoId<ListingPrototype> Listing = listing; } [Serializable, NetSerializable] diff --git a/Content.Shared/StoreDiscount/Components/StoreDiscountComponent.cs b/Content.Shared/StoreDiscount/Components/StoreDiscountComponent.cs new file mode 100644 index 00000000000..3736b4d4234 --- /dev/null +++ b/Content.Shared/StoreDiscount/Components/StoreDiscountComponent.cs @@ -0,0 +1,51 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Store; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.StoreDiscount.Components; + +/// <summary> +/// Partner-component for adding discounts functionality to StoreSystem using StoreDiscountSystem. +/// </summary> +[RegisterComponent, NetworkedComponent] +public sealed partial class StoreDiscountComponent : Component +{ + /// <summary> + /// Discounts for items in <see cref="ListingData"/>. + /// </summary> + [ViewVariables, DataField] + public IReadOnlyList<StoreDiscountData> Discounts = Array.Empty<StoreDiscountData>(); +} + +/// <summary> +/// Container for listing item discount state. +/// </summary> +[Serializable, NetSerializable, DataDefinition] +public sealed partial class StoreDiscountData +{ + /// <summary> + /// Id of listing item to be discounted. + /// </summary> + [DataField(required: true)] + public ProtoId<ListingPrototype> ListingId; + + /// <summary> + /// Amount of discounted items. Each buy will decrement this counter. + /// </summary> + [DataField] + public int Count; + + /// <summary> + /// Discount category that provided this discount. + /// </summary> + [DataField(required: true)] + public ProtoId<DiscountCategoryPrototype> DiscountCategory; + + /// <summary> + /// Map of currencies to flat amount of discount. + /// </summary> + [DataField] + public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> DiscountAmountByCurrency = new(); +} diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index a801e5ee467..dd0b2355858 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -1,5 +1,12 @@ using Content.Shared.DragDrop; using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Inventory.VirtualItem; using Content.Shared.Popups; using Content.Shared.Strip.Components; @@ -66,10 +73,25 @@ public void StripPopup(string messageId, ThievingStealth stealth, EntityUid targ PopupType? popupSize = _thieving.GetPopupTypeFromStealth(stealth); if (popupSize.HasValue) // We should always have a value if we're not hidden - _popup.PopupEntity(Loc.GetString(messageId, - ("user", subtle ? Loc.GetString("thieving-component-user") : user ?? EntityUid.Invalid), - ("item", subtle ? Loc.GetString("thieving-component-item") : item ?? EntityUid.Invalid), - ("slot", slot)), - target, target, popupSize.Value); + _popup.PopupEntity( + Loc.GetString( + messageId, + ("user", subtle ? Loc.GetString("thieving-component-user") : user ?? EntityUid.Invalid), + ("item", subtle ? Loc.GetString("thieving-component-item") : item ?? EntityUid.Invalid), + ("slot", slot)), + target, + target, + popupSize.Value); + } + + public bool IsStripHidden(SlotDefinition definition, EntityUid? viewer) + { + if (!definition.StripHidden) + return false; + + if (viewer == null) + return true; + + return !(HasComp<BypassInteractionChecksComponent>(viewer) || HasComp<ThievingComponent>(viewer)); } } diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index 989af647dc8..9d2e8d3dde2 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -14,10 +14,12 @@ using Content.Shared.Standing; using Content.Shared.StatusEffect; using Content.Shared.Throwing; +using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Player; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; namespace Content.Shared.Stunnable; @@ -26,9 +28,11 @@ public abstract class SharedStunSystem : EntitySystem [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; + [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; [Dependency] private readonly StandingStateSystem _standingState = default!; [Dependency] private readonly StatusEffectsSystem _statusEffect = default!; [Dependency] private readonly SharedLayingDownSystem _layingDown = default!; @@ -52,6 +56,9 @@ public override void Initialize() SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove); SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(UpdateCanMove); + SubscribeLocalEvent<StunOnContactComponent, ComponentStartup>(OnStunOnContactStartup); + SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide); + // helping people up if they're knocked down SubscribeLocalEvent<KnockedDownComponent, InteractHandEvent>(OnInteractHand); SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed); @@ -105,6 +112,27 @@ private void UpdateCanMove(EntityUid uid, StunnedComponent component, EntityEven _blocker.UpdateCanMove(uid); } + private void OnStunOnContactStartup(Entity<StunOnContactComponent> ent, ref ComponentStartup args) + { + if (TryComp<PhysicsComponent>(ent, out var body)) + _broadphase.RegenerateContacts(ent, body); + } + + private void OnStunOnContactCollide(Entity<StunOnContactComponent> ent, ref StartCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (_entityWhitelist.IsBlacklistPass(ent.Comp.Blacklist, args.OtherEntity)) + return; + + if (!TryComp<StatusEffectsComponent>(args.OtherEntity, out var status)) + return; + + TryStun(args.OtherEntity, ent.Comp.Duration, true, status); + TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, status); + } + private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args) { RaiseNetworkEvent(new CheckAutoGetUpEvent(GetNetEntity(uid))); diff --git a/Content.Shared/Stunnable/StunOnContactComponent.cs b/Content.Shared/Stunnable/StunOnContactComponent.cs new file mode 100644 index 00000000000..cc4af53b3b3 --- /dev/null +++ b/Content.Shared/Stunnable/StunOnContactComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared.Stunnable; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedStunSystem))] +public sealed partial class StunOnContactComponent : Component +{ + /// <summary> + /// The fixture the entity must collide with to be stunned + /// </summary> + [DataField] + public string FixtureId = "fix"; + + /// <summary> + /// The duration of the stun. + /// </summary> + [DataField] + public TimeSpan Duration = TimeSpan.FromSeconds(5); + + [DataField] + public EntityWhitelist Blacklist = new(); +} diff --git a/Content.Shared/Tabletop/SharedTabletopSystem.cs b/Content.Shared/Tabletop/SharedTabletopSystem.cs index 7bfd9d34572..afa77a643a0 100644 --- a/Content.Shared/Tabletop/SharedTabletopSystem.cs +++ b/Content.Shared/Tabletop/SharedTabletopSystem.cs @@ -16,7 +16,7 @@ public abstract class SharedTabletopSystem : EntitySystem [Dependency] protected readonly ActionBlockerSystem ActionBlockerSystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedTransformSystem _transforms = default!; + [Dependency] protected readonly SharedTransformSystem Transforms = default!; [Dependency] private readonly IMapManager _mapMan = default!; public override void Initialize() @@ -41,8 +41,8 @@ protected virtual void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventA // Move the entity and dirty it (we use the map ID from the entity so noone can try to be funny and move the item to another map) var transform = EntityManager.GetComponent<TransformComponent>(moved); - _transforms.SetParent(moved, transform, _mapMan.GetMapEntityId(transform.MapID)); - _transforms.SetLocalPositionNoLerp(transform, msg.Coordinates.Position); + Transforms.SetParent(moved, transform, _mapMan.GetMapEntityId(transform.MapID)); + Transforms.SetLocalPositionNoLerp(transform, msg.Coordinates.Position); } private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args) diff --git a/Content.Shared/Tag/TagComponent.cs b/Content.Shared/Tag/TagComponent.cs index b5b8a48a441..ad4240ba06c 100644 --- a/Content.Shared/Tag/TagComponent.cs +++ b/Content.Shared/Tag/TagComponent.cs @@ -1,13 +1,11 @@ using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; +using Robust.Shared.Prototypes; -namespace Content.Shared.Tag +namespace Content.Shared.Tag; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(TagSystem))] +public sealed partial class TagComponent : Component { - [RegisterComponent, NetworkedComponent, Access(typeof(TagSystem))] - public sealed partial class TagComponent : Component - { - [DataField("tags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<TagPrototype>))] - [Access(typeof(TagSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends - public HashSet<string> Tags = new(); - } + [DataField, ViewVariables, AutoNetworkedField] + public HashSet<ProtoId<TagPrototype>> Tags = new(); } diff --git a/Content.Shared/Tag/TagComponentState.cs b/Content.Shared/Tag/TagComponentState.cs deleted file mode 100644 index 9919aba108d..00000000000 --- a/Content.Shared/Tag/TagComponentState.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Tag -{ - [Serializable, NetSerializable] - public sealed class TagComponentState : ComponentState - { - public TagComponentState(string[] tags) - { - Tags = tags; - } - - public string[] Tags { get; } - } -} diff --git a/Content.Shared/Tag/TagPrototype.cs b/Content.Shared/Tag/TagPrototype.cs index 2a06e22cf90..97f8c1af7d4 100644 --- a/Content.Shared/Tag/TagPrototype.cs +++ b/Content.Shared/Tag/TagPrototype.cs @@ -1,17 +1,15 @@ using Robust.Shared.Prototypes; -namespace Content.Shared.Tag +namespace Content.Shared.Tag; + +/// <summary> +/// Prototype representing a tag in YAML. +/// Meant to only have an ID property, as that is the only thing that +/// gets saved in TagComponent. +/// </summary> +[Prototype("Tag")] +public sealed partial class TagPrototype : IPrototype { - /// <summary> - /// Prototype representing a tag in YAML. - /// Meant to only have an ID property, as that is the only thing that - /// gets saved in TagComponent. - /// </summary> - [Prototype("Tag")] - public sealed partial class TagPrototype : IPrototype - { - [ViewVariables] - [IdDataField] - public string ID { get; private set; } = default!; - } + [IdDataField, ViewVariables] + public string ID { get; } = string.Empty; } diff --git a/Content.Shared/Tag/TagSystem.cs b/Content.Shared/Tag/TagSystem.cs index b9fef076c88..f1f620a6949 100644 --- a/Content.Shared/Tag/TagSystem.cs +++ b/Content.Shared/Tag/TagSystem.cs @@ -1,11 +1,19 @@ -using System.Diagnostics; -using System.Linq; -using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Tag; +/// <summary> +/// The system that is responsible for working with tags. +/// Checking the existence of the <see cref="TagPrototype"/> only happens in DEBUG builds, +/// to improve performance, so don't forget to check it. +/// </summary> +/// <summary> +/// The methods to add or remove a list of tags have only an implementation with the <see cref="IEnumerable{T}"/> type, +/// it's not much, but it takes away performance, +/// if you need to use them often, it's better to make a proper implementation, +/// you can read more <a href="https://github.com/space-wizards/space-station-14/pull/28272">HERE</a>. +/// </summary> public sealed class TagSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; @@ -15,541 +23,514 @@ public sealed class TagSystem : EntitySystem public override void Initialize() { base.Initialize(); + _tagQuery = GetEntityQuery<TagComponent>(); - SubscribeLocalEvent<TagComponent, ComponentGetState>(OnTagGetState); - SubscribeLocalEvent<TagComponent, ComponentHandleState>(OnTagHandleState); #if DEBUG SubscribeLocalEvent<TagComponent, ComponentInit>(OnTagInit); +#endif } +#if DEBUG private void OnTagInit(EntityUid uid, TagComponent component, ComponentInit args) { foreach (var tag in component.Tags) { AssertValidTag(tag); } -#endif - } - - - private void OnTagHandleState(EntityUid uid, TagComponent component, ref ComponentHandleState args) - { - if (args.Current is not TagComponentState state) - return; - - component.Tags.Clear(); - - foreach (var tag in state.Tags) - { - AssertValidTag(tag); - component.Tags.Add(tag); - } - } - - private static void OnTagGetState(EntityUid uid, TagComponent component, ref ComponentGetState args) - { - var tags = new string[component.Tags.Count]; - var i = 0; - - foreach (var tag in component.Tags) - { - tags[i] = tag; - i++; - } - - args.State = new TagComponentState(tags); - } - - private void AssertValidTag(string id) - { - DebugTools.Assert(_proto.HasIndex<TagPrototype>(id), $"Unknown tag: {id}"); } +#endif /// <summary> - /// Tries to add a tag to an entity if the tag doesn't already exist. + /// Tries to add a tag to an entity if the tag doesn't already exist. /// </summary> - /// <param name="entity">The entity to add the tag to.</param> - /// <param name="id">The tag to add.</param> /// <returns> - /// true if it was added, false otherwise even if it already existed. + /// true if it was added, false otherwise even if it already existed. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool AddTag(EntityUid entity, string id) + public bool AddTag(EntityUid entityUid, ProtoId<TagPrototype> tag) { - return AddTag(entity, EnsureComp<TagComponent>(entity), id); + return AddTag((entityUid, EnsureComp<TagComponent>(entityUid)), tag); } /// <summary> - /// Tries to add the given tags to an entity if the tags don't already exist. + /// Tries to add the given tags to an entity if the tags don't already exist. /// </summary> - /// <param name="entity">The entity to add the tag to.</param> - /// <param name="ids">The tags to add.</param> /// <returns> - /// true if any tags were added, false otherwise even if they all already existed. + /// true if any tags were added, false otherwise even if they all already existed. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool AddTags(EntityUid entity, params string[] ids) + public bool AddTags(EntityUid entityUid, params ProtoId<TagPrototype>[] tags) { - return AddTags(entity, EnsureComp<TagComponent>(entity), ids); + return AddTags(entityUid, (IEnumerable<ProtoId<TagPrototype>>)tags); } /// <summary> - /// Tries to add the given tags to an entity if the tags don't already exist. + /// Tries to add the given tags to an entity if the tags don't already exist. /// </summary> - /// <param name="entity">The entity to add the tag to.</param> - /// <param name="ids">The tags to add.</param> /// <returns> - /// true if any tags were added, false otherwise even if they all already existed. + /// true if any tags were added, false otherwise even if they all already existed. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool AddTags(EntityUid entity, IEnumerable<string> ids) + public bool AddTags(EntityUid entityUid, IEnumerable<ProtoId<TagPrototype>> tags) { - return AddTags(entity, EnsureComp<TagComponent>(entity), ids); + return AddTags((entityUid, EnsureComp<TagComponent>(entityUid)), tags); } /// <summary> - /// Tries to add a tag to an entity if it has a <see cref="TagComponent"/> - /// and the tag doesn't already exist. + /// Tries to add a tag to an entity if it has a <see cref="TagComponent"/> + /// and the tag doesn't already exist. /// </summary> - /// <param name="entity">The entity to add the tag to.</param> - /// <param name="id">The tag to add.</param> /// <returns> - /// true if it was added, false otherwise even if it already existed. + /// true if it was added, false otherwise even if it already existed. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool TryAddTag(EntityUid entity, string id) + public bool TryAddTag(EntityUid entityUid, ProtoId<TagPrototype> tag) { - return TryComp<TagComponent>(entity, out var component) && - AddTag(entity, component, id); + return _tagQuery.TryComp(entityUid, out var component) && + AddTag((entityUid, component), tag); } /// <summary> - /// Tries to add the given tags to an entity if it has a - /// <see cref="TagComponent"/> and the tags don't already exist. + /// Tries to add the given tags to an entity if it has a + /// <see cref="TagComponent"/> and the tags don't already exist. /// </summary> - /// <param name="entity">The entity to add the tag to.</param> - /// <param name="ids">The tags to add.</param> /// <returns> - /// true if any tags were added, false otherwise even if they all already existed. + /// true if any tags were added, false otherwise even if they all already existed. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool TryAddTags(EntityUid entity, params string[] ids) + public bool TryAddTags(EntityUid entityUid, params ProtoId<TagPrototype>[] tags) { - return TryComp<TagComponent>(entity, out var component) && - AddTags(entity, component, ids); + return TryAddTags(entityUid, (IEnumerable<ProtoId<TagPrototype>>)tags); } /// <summary> - /// Tries to add the given tags to an entity if it has a - /// <see cref="TagComponent"/> and the tags don't already exist. + /// Tries to add the given tags to an entity if it has a + /// <see cref="TagComponent"/> and the tags don't already exist. /// </summary> - /// <param name="entity">The entity to add the tag to.</param> - /// <param name="ids">The tags to add.</param> /// <returns> - /// true if any tags were added, false otherwise even if they all already existed. + /// true if any tags were added, false otherwise even if they all already existed. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool TryAddTags(EntityUid entity, IEnumerable<string> ids) + public bool TryAddTags(EntityUid entityUid, IEnumerable<ProtoId<TagPrototype>> tags) { - return TryComp<TagComponent>(entity, out var component) && - AddTags(entity, component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + AddTags((entityUid, component), tags); } /// <summary> - /// Checks if a tag has been added to an entity. + /// Checks if a tag has been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="id">The tag to check for.</param> - /// <returns>true if it exists, false otherwise.</returns> + /// <returns> + /// true if it exists, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool HasTag(EntityUid entity, string id) - { - return _tagQuery.TryComp(entity, out var component) && - HasTag(component, id); - } - - /// <summary> - /// Checks if a tag has been added to an entity. - /// </summary> - [Obsolete] - public bool HasTag(EntityUid entity, string id, EntityQuery<TagComponent> tagQuery) + public bool HasTag(EntityUid entityUid, ProtoId<TagPrototype> tag) { - return tagQuery.TryGetComponent(entity, out var component) && - HasTag(component, id); + return _tagQuery.TryComp(entityUid, out var component) && + HasTag(component, tag); } /// <summary> - /// Checks if all of the given tags have been added to an entity. + /// Checks if a tag has been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="id">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if it exists, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool HasAllTags(EntityUid entity, string id) => HasTag(entity, id); + public bool HasAllTags(EntityUid entityUid, ProtoId<TagPrototype> tag) => + HasTag(entityUid, tag); /// <summary> - /// Checks if all of the given tags have been added to an entity. + /// Checks if all of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(EntityUid entity, List<string> ids) + public bool HasAllTags(EntityUid entityUid, params ProtoId<TagPrototype>[] tags) { - return _tagQuery.TryComp(entity, out var component) && - HasAllTags(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAllTags(component, tags); } /// <summary> - /// Checks if all of the given tags have been added to an entity. + /// Checks if all of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(EntityUid entity, IEnumerable<string> ids) + public bool HasAllTags(EntityUid entityUid, HashSet<ProtoId<TagPrototype>> tags) { - return _tagQuery.TryComp(entity, out var component) && - HasAllTags(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAllTags(component, tags); } /// <summary> - /// Checks if all of the given tags have been added to an entity. + /// Checks if all of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(EntityUid entity, List<ProtoId<TagPrototype>> ids) + public bool HasAllTags(EntityUid entityUid, List<ProtoId<TagPrototype>> tags) { - return _tagQuery.TryComp(entity, out var component) && - HasAllTags(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAllTags(component, tags); } /// <summary> - /// Checks if any of the given tags have been added to an entity. + /// Checks if all of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(EntityUid entity, params string[] ids) + public bool HasAllTags(EntityUid entityUid, IEnumerable<ProtoId<TagPrototype>> tags) { - return _tagQuery.TryComp(entity, out var component) && - HasAnyTag(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAllTags(component, tags); } /// <summary> - /// Checks if any of the given tags have been added to an entity. + /// Checks if a tag has been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="id">The tag to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if it exists, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool HasAnyTag(EntityUid entity, string id) => HasTag(entity, id); + public bool HasAnyTag(EntityUid entityUid, ProtoId<TagPrototype> tag) => + HasTag(entityUid, tag); /// <summary> - /// Checks if any of the given tags have been added to an entity. + /// Checks if any of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(EntityUid entity, List<string> ids) + public bool HasAnyTag(EntityUid entityUid, params ProtoId<TagPrototype>[] tags) { - return _tagQuery.TryComp(entity, out var component) && - HasAnyTag(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAnyTag(component, tags); } /// <summary> - /// Checks if any of the given tags have been added to an entity. + /// Checks if any of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(EntityUid entity, List<ProtoId<TagPrototype>> ids) + public bool HasAnyTag(EntityUid entityUid, HashSet<ProtoId<TagPrototype>> tags) { - return TryComp<TagComponent>(entity, out var component) && - HasAnyTag(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAnyTag(component, tags); } /// <summary> - /// Checks if any of the given tags have been added to an entity. + /// Checks if any of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to check.</param> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(EntityUid entity, IEnumerable<string> ids) + public bool HasAnyTag(EntityUid entityUid, List<ProtoId<TagPrototype>> tags) { - return _tagQuery.TryComp(entity, out var component) && - HasAnyTag(component, ids); + return _tagQuery.TryComp(entityUid, out var component) && + HasAnyTag(component, tags); } /// <summary> - /// Tries to remove a tag from an entity if it exists. + /// Checks if any of the given tags have been added to an entity. /// </summary> - /// <param name="entity">The entity to remove the tag from.</param> - /// <param name="id">The tag to remove.</param> /// <returns> - /// true if it was removed, false otherwise even if it didn't exist. + /// true if any of them exist, false otherwise. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool RemoveTag(EntityUid entity, string id) + public bool HasAnyTag(EntityUid entityUid, IEnumerable<ProtoId<TagPrototype>> tags) { - return TryComp<TagComponent>(entity, out var component) && - RemoveTag(entity, component, id); + return _tagQuery.TryComp(entityUid, out var component) && + HasAnyTag(component, tags); } /// <summary> - /// Tries to remove a tag from an entity if it exists. + /// Checks if a tag has been added to an component. /// </summary> - /// <param name="entity">The entity to remove the tag from.</param> - /// <param name="ids">The tag to remove.</param> /// <returns> - /// true if it was removed, false otherwise even if it didn't exist. + /// true if it exists, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - /// </returns> - public bool RemoveTags(EntityUid entity, params string[] ids) + public bool HasTag(TagComponent component, ProtoId<TagPrototype> tag) { - return TryComp<TagComponent>(entity, out var component) && - RemoveTags(entity, component, ids); +#if DEBUG + AssertValidTag(tag); +#endif + return component.Tags.Contains(tag); } /// <summary> - /// Tries to remove a tag from an entity if it exists. + /// Checks if a tag has been added to an component. /// </summary> - /// <param name="entity">The entity to remove the tag from.</param> - /// <param name="ids">The tag to remove.</param> /// <returns> - /// true if it was removed, false otherwise even if it didn't exist. + /// true if it exists, false otherwise. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool RemoveTags(EntityUid entity, IEnumerable<string> ids) - { - return TryComp<TagComponent>(entity, out var component) && - RemoveTags(entity, component, ids); - } + public bool HasAllTags(TagComponent component, ProtoId<TagPrototype> tag) => + HasTag(component, tag); /// <summary> - /// Tries to add a tag if it doesn't already exist. + /// Checks if all of the given tags have been added to an component. /// </summary> - /// <param name="id">The tag to add.</param> - /// <returns>true if it was added, false if it already existed.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool AddTag(EntityUid uid, TagComponent component, string id) + public bool HasAllTags(TagComponent component, params ProtoId<TagPrototype>[] tags) { - AssertValidTag(id); - var added = component.Tags.Add(id); - - if (added) + foreach (var tag in tags) { - Dirty(uid, component); - return true; +#if DEBUG + AssertValidTag(tag); +#endif + if (!component.Tags.Contains(tag)) + return false; } - return false; + return true; } /// <summary> - /// Tries to add the given tags if they don't already exist. + /// Checks if all of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to add.</param> - /// <returns>true if any tags were added, false if they all already existed.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool AddTags(EntityUid uid, TagComponent component, params string[] ids) + public bool HasAllTagsArray(TagComponent component, ProtoId<TagPrototype>[] tags) { - return AddTags(uid, component, ids.AsEnumerable()); + foreach (var tag in tags) + { +#if DEBUG + AssertValidTag(tag); +#endif + if (!component.Tags.Contains(tag)) + return false; + } + + return true; } /// <summary> - /// Tries to add the given tags if they don't already exist. + /// Checks if all of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to add.</param> - /// <returns>true if any tags were added, false if they all already existed.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool AddTags(EntityUid uid, TagComponent component, IEnumerable<string> ids) + public bool HasAllTags(TagComponent component, List<ProtoId<TagPrototype>> tags) { - var count = component.Tags.Count; - - foreach (var id in ids) - { - AssertValidTag(id); - component.Tags.Add(id); - } - - if (component.Tags.Count > count) + foreach (var tag in tags) { - Dirty(uid, component); - return true; +#if DEBUG + AssertValidTag(tag); +#endif + if (!component.Tags.Contains(tag)) + return false; } - return false; + return true; } /// <summary> - /// Checks if a tag has been added. + /// Checks if all of the given tags have been added to an component. /// </summary> - /// <param name="id">The tag to check for.</param> - /// <returns>true if it exists, false otherwise.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasTag(TagComponent component, string id) + public bool HasAllTags(TagComponent component, HashSet<ProtoId<TagPrototype>> tags) { - AssertValidTag(id); - return component.Tags.Contains(id); + foreach (var tag in tags) + { +#if DEBUG + AssertValidTag(tag); +#endif + if (!component.Tags.Contains(tag)) + return false; + } + + return true; } /// <summary> - /// Checks if all of the given tags have been added. + /// Checks if all of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if they all exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(TagComponent component, params string[] ids) + public bool HasAllTags(TagComponent component, IEnumerable<ProtoId<TagPrototype>> tags) { - return HasAllTags(component, ids.AsEnumerable()); + foreach (var tag in tags) + { +#if DEBUG + AssertValidTag(tag); +#endif + if (!component.Tags.Contains(tag)) + return false; + } + + return true; } /// <summary> - /// Checks if all of the given tags have been added. + /// Checks if a tag has been added to an component. /// </summary> - /// <param name="id">The tag to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if it exists, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool HasAllTags(TagComponent component, string id) => HasTag(component, id); + public bool HasAnyTag(TagComponent component, ProtoId<TagPrototype> tag) => + HasTag(component, tag); /// <summary> - /// Checks if all of the given tags have been added. + /// Checks if any of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(TagComponent component, List<string> ids) + public bool HasAnyTag(TagComponent component, params ProtoId<TagPrototype>[] tags) { - foreach (var id in ids) + foreach (var tag in tags) { - AssertValidTag(id); - - if (!component.Tags.Contains(id)) - return false; +#if DEBUG + AssertValidTag(tag); +#endif + if (component.Tags.Contains(tag)) + return true; } - return true; + return false; } /// <summary> - /// Checks if all of the given tags have been added. + /// Checks if any of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(TagComponent component, IEnumerable<string> ids) + public bool HasAnyTag(TagComponent component, HashSet<ProtoId<TagPrototype>> tags) { - foreach (var id in ids) + foreach (var tag in tags) { - AssertValidTag(id); - - if (!component.Tags.Contains(id)) - return false; - +#if DEBUG + AssertValidTag(tag); +#endif + if (component.Tags.Contains(tag)) + return true; } - return true; + return false; } /// <summary> - /// Checks if all of the given tags have been added. + /// Checks if any of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if they all exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAllTags(TagComponent component, List<ProtoId<TagPrototype>> ids) + public bool HasAnyTag(TagComponent component, List<ProtoId<TagPrototype>> tags) { - foreach (var id in ids) + foreach (var tag in tags) { - AssertValidTag(id); - - if (!component.Tags.Contains(id)) - return false; - +#if DEBUG + AssertValidTag(tag); +#endif + if (component.Tags.Contains(tag)) + return true; } - return true; + return false; } /// <summary> - /// Checks if any of the given tags have been added. + /// Checks if any of the given tags have been added to an component. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if any of them exist, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(TagComponent component, params string[] ids) + public bool HasAnyTag(TagComponent component, IEnumerable<ProtoId<TagPrototype>> tags) { - foreach (var id in ids) + foreach (var tag in tags) { - AssertValidTag(id); - - if (component.Tags.Contains(id)) +#if DEBUG + AssertValidTag(tag); +#endif + if (component.Tags.Contains(tag)) return true; } @@ -557,143 +538,178 @@ public bool HasAnyTag(TagComponent component, params string[] ids) } /// <summary> - /// Checks if any of the given tags have been added. + /// Tries to remove a tag from an entity if it exists. /// </summary> - /// <param name="id">The tag to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if it was removed, false otherwise even if it didn't exist. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool HasAnyTag(TagComponent component, string id) => HasTag(component, id); + public bool RemoveTag(EntityUid entityUid, ProtoId<TagPrototype> tag) + { + return _tagQuery.TryComp(entityUid, out var component) && + RemoveTag((entityUid, component), tag); + } /// <summary> - /// Checks if any of the given tags have been added. + /// Tries to remove a tag from an entity if it exists. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if it was removed, false otherwise even if it didn't exist. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(TagComponent component, List<string> ids) + public bool RemoveTags(EntityUid entityUid, params ProtoId<TagPrototype>[] tags) { - foreach (var id in ids) - { - AssertValidTag(id); - - if (component.Tags.Contains(id)) - { - return true; - } - } + return RemoveTags(entityUid, (IEnumerable<ProtoId<TagPrototype>>)tags); + } - return false; + /// <summary> + /// Tries to remove a tag from an entity if it exists. + /// </summary> + /// <returns> + /// true if it was removed, false otherwise even if it didn't exist. + /// </returns> + /// <exception cref="UnknownPrototypeException"> + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// </exception> + public bool RemoveTags(EntityUid entityUid, IEnumerable<ProtoId<TagPrototype>> tags) + { + return _tagQuery.TryComp(entityUid, out var component) && + RemoveTags((entityUid, component), tags); } /// <summary> - /// Checks if any of the given tags have been added. + /// Tries to add a tag if it doesn't already exist. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if it was added, false if it already existed. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool HasAnyTag(TagComponent component, IEnumerable<string> ids) + public bool AddTag(Entity<TagComponent> entity, ProtoId<TagPrototype> tag) { - foreach (var id in ids) - { - AssertValidTag(id); +#if DEBUG + AssertValidTag(tag); +#endif + if (!entity.Comp.Tags.Add(tag)) + return false; - if (component.Tags.Contains(id)) - { - return true; - } - } + Dirty(entity); + return true; + } - return false; + /// <summary> + /// Tries to add the given tags if they don't already exist. + /// </summary> + /// <returns> + /// true if any tags were added, false if they all already existed. + /// </returns> + /// <exception cref="UnknownPrototypeException"> + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// </exception> + public bool AddTags(Entity<TagComponent> entity, params ProtoId<TagPrototype>[] tags) + { + return AddTags(entity, (IEnumerable<ProtoId<TagPrototype>>)tags); } /// <summary> - /// Checks if any of the given tags have been added. + /// Tries to add the given tags if they don't already exist. /// </summary> - /// <param name="ids">The tags to check for.</param> - /// <returns>true if any of them exist, false otherwise.</returns> + /// <returns> + /// true if any tags were added, false if they all already existed. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool HasAnyTag(TagComponent comp, List<ProtoId<TagPrototype>> ids) + public bool AddTags(Entity<TagComponent> entity, IEnumerable<ProtoId<TagPrototype>> tags) { - foreach (var id in ids) + var update = false; + foreach (var tag in tags) { - AssertValidTag(id); - - if (comp.Tags.Contains(id)) - return true; +#if DEBUG + AssertValidTag(tag); +#endif + if (entity.Comp.Tags.Add(tag) && !update) + update = true; } - return false; + if (!update) + return false; + + Dirty(entity); + return true; } /// <summary> - /// Tries to remove a tag if it exists. + /// Tries to remove a tag if it exists. /// </summary> /// <returns> - /// true if it was removed, false otherwise even if it didn't exist. + /// true if it was removed, false otherwise even if it didn't exist. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if no <see cref="TagPrototype"/> exists with the given id. + /// Thrown if no <see cref="TagPrototype"/> exists with the given id. /// </exception> - public bool RemoveTag(EntityUid uid, TagComponent component, string id) + public bool RemoveTag(Entity<TagComponent> entity, ProtoId<TagPrototype> tag) { - AssertValidTag(id); +#if DEBUG + AssertValidTag(tag); +#endif - if (component.Tags.Remove(id)) - { - Dirty(uid, component); - return true; - } + if (!entity.Comp.Tags.Remove(tag)) + return false; - return false; + Dirty(entity); + return true; } /// <summary> - /// Tries to remove all of the given tags if they exist. + /// Tries to remove all of the given tags if they exist. /// </summary> - /// <param name="ids">The tags to remove.</param> /// <returns> - /// true if it was removed, false otherwise even if they didn't exist. + /// true if any tag was removed, false otherwise. /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool RemoveTags(EntityUid uid, TagComponent component, params string[] ids) + public bool RemoveTags(Entity<TagComponent> entity, params ProtoId<TagPrototype>[] tags) { - return RemoveTags(uid, component, ids.AsEnumerable()); + return RemoveTags(entity, (IEnumerable<ProtoId<TagPrototype>>)tags); } /// <summary> - /// Tries to remove all of the given tags if they exist. + /// Tries to remove all of the given tags if they exist. /// </summary> - /// <param name="ids">The tags to remove.</param> - /// <returns>true if any tag was removed, false otherwise.</returns> + /// <returns> + /// true if any tag was removed, false otherwise. + /// </returns> /// <exception cref="UnknownPrototypeException"> - /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. + /// Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>. /// </exception> - public bool RemoveTags(EntityUid uid, TagComponent component, IEnumerable<string> ids) + public bool RemoveTags(Entity<TagComponent> entity, IEnumerable<ProtoId<TagPrototype>> tags) { - var count = component.Tags.Count; - - foreach (var id in ids) + var update = false; + foreach (var tag in tags) { - AssertValidTag(id); - component.Tags.Remove(id); +#if DEBUG + AssertValidTag(tag); +#endif + if (entity.Comp.Tags.Remove(tag) && !update) + update = true; } - if (component.Tags.Count < count) - { - Dirty(uid, component); - return true; - } + if (!update) + return false; - return false; + Dirty(entity); + return true; + } + + private void AssertValidTag(string id) + { + DebugTools.Assert(_proto.HasIndex<TagPrototype>(id), $"Unknown tag: {id}"); } } diff --git a/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs b/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs index bc73baa61ad..58c249fec54 100644 --- a/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs +++ b/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Popups; using Content.Shared.Teleportation.Components; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map.Components; @@ -24,6 +25,7 @@ public sealed class SwapTeleporterSystem : EntitySystem [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; private EntityQuery<TransformComponent> _xformQuery; @@ -51,8 +53,8 @@ private void OnInteract(Entity<SwapTeleporterComponent> ent, ref AfterInteractEv if (!TryComp<SwapTeleporterComponent>(target, out var targetComp)) return; - if (!comp.TeleporterWhitelist.IsValid(target, EntityManager) || - !targetComp.TeleporterWhitelist.IsValid(uid, EntityManager)) + if (_whitelistSystem.IsWhitelistFail(comp.TeleporterWhitelist, target) || + _whitelistSystem.IsWhitelistFail(targetComp.TeleporterWhitelist, uid)) { return; } diff --git a/Content.Shared/Throwing/BeforeThrowEvent.cs b/Content.Shared/Throwing/BeforeThrowEvent.cs index f949c16b0ea..5c6689d46d9 100644 --- a/Content.Shared/Throwing/BeforeThrowEvent.cs +++ b/Content.Shared/Throwing/BeforeThrowEvent.cs @@ -5,17 +5,17 @@ namespace Content.Shared.Throwing; [ByRefEvent] public struct BeforeThrowEvent { - public BeforeThrowEvent(EntityUid itemUid, Vector2 direction, float throwStrength, EntityUid playerUid) + public BeforeThrowEvent(EntityUid itemUid, Vector2 direction, float throwSpeed, EntityUid playerUid) { ItemUid = itemUid; Direction = direction; - ThrowStrength = throwStrength; + ThrowSpeed = throwSpeed; PlayerUid = playerUid; } public EntityUid ItemUid { get; set; } public Vector2 Direction { get; } - public float ThrowStrength { get; set;} + public float ThrowSpeed { get; set;} public EntityUid PlayerUid { get; } public bool Cancelled = false; @@ -24,17 +24,17 @@ public BeforeThrowEvent(EntityUid itemUid, Vector2 direction, float throwStrengt [ByRefEvent] public struct BeforeGettingThrownEvent { - public BeforeGettingThrownEvent(EntityUid itemUid, Vector2 direction, float throwStrength, EntityUid playerUid) + public BeforeGettingThrownEvent(EntityUid itemUid, Vector2 direction, float throwSpeed, EntityUid playerUid) { ItemUid = itemUid; Direction = direction; - ThrowStrength = throwStrength; + ThrowSpeed = throwSpeed; PlayerUid = playerUid; } public EntityUid ItemUid { get; set; } public Vector2 Direction { get; } - public float ThrowStrength { get; set;} + public float ThrowSpeed { get; set;} public EntityUid PlayerUid { get; } public bool Cancelled = false; diff --git a/Content.Shared/Throwing/LandAtCursorComponent.cs b/Content.Shared/Throwing/LandAtCursorComponent.cs new file mode 100644 index 00000000000..f8e99e2fc87 --- /dev/null +++ b/Content.Shared/Throwing/LandAtCursorComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Throwing +{ + /// <summary> + /// Makes an item land at the cursor when thrown and slide a little further. + /// Without it the item lands slightly in front and stops moving at the cursor. + /// Use this for throwing weapons that should pierce the opponent, for example spears. + /// </summary> + [RegisterComponent, NetworkedComponent] + public sealed partial class LandAtCursorComponent : Component { } +} diff --git a/Content.Shared/Throwing/ThrowingSystem.cs b/Content.Shared/Throwing/ThrowingSystem.cs index 7d94ada924d..56bbf4c2bf3 100644 --- a/Content.Shared/Throwing/ThrowingSystem.cs +++ b/Content.Shared/Throwing/ThrowingSystem.cs @@ -1,13 +1,12 @@ using System.Numerics; using Content.Shared.Administration.Logs; using Content.Shared.Camera; +using Content.Shared.CCVar; using Content.Shared.Database; +using Content.Shared.Friction; using Content.Shared.Gravity; -using Content.Shared.Hands.Components; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Interaction; using Content.Shared.Projectiles; -using Content.Shared.Tag; +using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; @@ -31,7 +30,10 @@ public sealed class ThrowingSystem : EntitySystem /// The minimum amount of time an entity needs to be thrown before the timer can be run. /// Anything below this threshold never enters the air. /// </summary> - public const float FlyTime = 0.15f; + public const float MinFlyTime = 0.15f; + public const float FlyTimePercentage = 0.8f; + + private float _frictionModifier; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; @@ -40,13 +42,23 @@ public sealed class ThrowingSystem : EntitySystem [Dependency] private readonly ThrownItemSystem _thrownSystem = default!; [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; + + public override void Initialize() + { + base.Initialize(); + + Subs.CVar(_configManager, CCVars.TileFrictionModifier, value => _frictionModifier = value, true); + } public void TryThrow( EntityUid uid, EntityCoordinates coordinates, - float strength = 1.0f, + float baseThrowSpeed = 10.0f, EntityUid? user = null, float pushbackRatio = PushbackDefault, + float? friction = null, + bool compensateFriction = false, bool recoil = true, bool animated = true, bool playSound = true, @@ -58,7 +70,7 @@ public void TryThrow( if (mapPos.MapId != thrownPos.MapId) return; - TryThrow(uid, mapPos.Position - thrownPos.Position, strength, user, pushbackRatio, recoil: recoil, animated: animated, playSound: playSound, doSpin: doSpin); + TryThrow(uid, mapPos.Position - thrownPos.Position, baseThrowSpeed, user, pushbackRatio, friction, compensateFriction: compensateFriction, recoil: recoil, animated: animated, playSound: playSound, doSpin: doSpin); } /// <summary> @@ -66,14 +78,18 @@ public void TryThrow( /// </summary> /// <param name="uid">The entity being thrown.</param> /// <param name="direction">A vector pointing from the entity to its destination.</param> - /// <param name="strength">How much the direction vector should be multiplied for velocity.</param> + /// <param name="baseThrowSpeed">Throw velocity. Gets modified if compensateFriction is true.</param> /// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param> + /// <param name="friction">friction value used for the distance calculation. If set to null this defaults to the standard tile values</param> + /// <param name="compensateFriction">True will adjust the throw so the item stops at the target coordinates. False means it will land at the target and keep sliding.</param> /// <param name="doSpin">Whether spin will be applied to the thrown entity.</param> public void TryThrow(EntityUid uid, Vector2 direction, - float strength = 1.0f, + float baseThrowSpeed = 10.0f, EntityUid? user = null, float pushbackRatio = PushbackDefault, + float? friction = null, + bool compensateFriction = false, bool recoil = true, bool animated = true, bool playSound = true, @@ -91,9 +107,10 @@ public void TryThrow(EntityUid uid, physics, Transform(uid), projectileQuery, - strength, + baseThrowSpeed, user, - pushbackRatio, recoil: recoil, animated: animated, playSound: playSound, doSpin: doSpin); + pushbackRatio, + friction, compensateFriction: compensateFriction, recoil: recoil, animated: animated, playSound: playSound, doSpin: doSpin); } /// <summary> @@ -101,23 +118,27 @@ public void TryThrow(EntityUid uid, /// </summary> /// <param name="uid">The entity being thrown.</param> /// <param name="direction">A vector pointing from the entity to its destination.</param> - /// <param name="strength">How much the direction vector should be multiplied for velocity.</param> + /// <param name="baseThrowSpeed">Throw velocity. Gets modified if compensateFriction is true.</param> /// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param> + /// <param name="friction">friction value used for the distance calculation. If set to null this defaults to the standard tile values</param> + /// <param name="compensateFriction">True will adjust the throw so the item stops at the target coordinates. False means it will land at the target and keep sliding.</param> /// <param name="doSpin">Whether spin will be applied to the thrown entity.</param> public void TryThrow(EntityUid uid, Vector2 direction, PhysicsComponent physics, TransformComponent transform, EntityQuery<ProjectileComponent> projectileQuery, - float strength = 1.0f, + float baseThrowSpeed = 10.0f, EntityUid? user = null, float pushbackRatio = PushbackDefault, + float? friction = null, + bool compensateFriction = false, bool recoil = true, bool animated = true, bool playSound = true, bool doSpin = true) { - if (strength <= 0 || direction == Vector2Helpers.Infinity || direction == Vector2Helpers.NaN || direction == Vector2.Zero) + if (baseThrowSpeed <= 0 || direction == Vector2Helpers.Infinity || direction == Vector2Helpers.NaN || direction == Vector2.Zero || friction < 0) return; if ((physics.BodyType & (BodyType.Dynamic | BodyType.KinematicController)) == 0x0) @@ -136,16 +157,22 @@ public void TryThrow(EntityUid uid, Animate = animated, }; - // Estimate time to arrival so we can apply OnGround status and slow it much faster. - var time = direction.Length() / strength; + // if not given, get the default friction value for distance calculation + var tileFriction = friction ?? _frictionModifier * TileFrictionController.DefaultFriction; + + if (tileFriction == 0f) + compensateFriction = false; // cannot calculate this if there is no friction + + // Set the time the item is supposed to be in the air so we can apply OnGround status. + // This is a free parameter, but we should set it to something reasonable. + var flyTime = direction.Length() / baseThrowSpeed; + if (compensateFriction) + flyTime *= FlyTimePercentage; + + if (flyTime < MinFlyTime) + flyTime = 0f; comp.ThrownTime = _gameTiming.CurTime; - // TODO: This is a bandaid, don't do this. - // if you want to force landtime have the caller handle it or add a new method. - // did we launch this with something stronger than our hands? - if (TryComp<HandsComponent>(comp.Thrower, out var hands) && strength > hands.ThrowForceMultiplier) - comp.LandTime = comp.ThrownTime + TimeSpan.FromSeconds(time); - else - comp.LandTime = time < FlyTime ? default : comp.ThrownTime + TimeSpan.FromSeconds(time - FlyTime); + comp.LandTime = comp.ThrownTime + TimeSpan.FromSeconds(flyTime); comp.PlayLandSound = playSound; AddComp(uid, comp, true); @@ -173,7 +200,12 @@ public void TryThrow(EntityUid uid, if (user != null) _adminLogger.Add(LogType.Throw, LogImpact.Low, $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity}"); - var impulseVector = direction.Normalized() * strength * physics.Mass; + // if compensateFriction==true compensate for the distance the item will slide over the floor after landing by reducing the throw speed accordingly. + // else let the item land on the cursor and from where it slides a little further. + // This is an exact formula we get from exponentially decaying velocity after landing. + // If someone changes how tile friction works at some point, this will have to be adjusted. + var throwSpeed = compensateFriction ? direction.Length() / (flyTime + 1 / tileFriction) : baseThrowSpeed; + var impulseVector = direction.Normalized() * throwSpeed * physics.Mass; _physics.ApplyLinearImpulse(uid, impulseVector, body: physics); if (comp.LandTime == null || comp.LandTime <= TimeSpan.Zero) diff --git a/Content.Shared/Throwing/ThrownItemSystem.cs b/Content.Shared/Throwing/ThrownItemSystem.cs index f607a2dc934..5f6dbbab35e 100644 --- a/Content.Shared/Throwing/ThrownItemSystem.cs +++ b/Content.Shared/Throwing/ThrownItemSystem.cs @@ -163,7 +163,7 @@ public override void Update(float frameTime) LandComponent(uid, thrown, physics, thrown.PlayLandSound); } - var stopThrowTime = (thrown.LandTime ?? thrown.ThrownTime) + TimeSpan.FromSeconds(ThrowingSystem.FlyTime); + var stopThrowTime = thrown.LandTime ?? thrown.ThrownTime; if (stopThrowTime <= _gameTiming.CurTime) { StopThrow(uid, thrown); diff --git a/Content.Shared/Toggleable/ToggleActionEvent.cs b/Content.Shared/Toggleable/ToggleActionEvent.cs index 1283b6699bf..f28e62e7dd1 100644 --- a/Content.Shared/Toggleable/ToggleActionEvent.cs +++ b/Content.Shared/Toggleable/ToggleActionEvent.cs @@ -4,9 +4,12 @@ namespace Content.Shared.Toggleable; /// <summary> -/// Generic action-event for toggle-able components. +/// Generic action-event for toggle-able components. /// </summary> -public sealed partial class ToggleActionEvent : InstantActionEvent { } +/// <remarks> +/// If you are using <c>ItemToggleComponent</c> subscribe to <c>ItemToggledEvent</c> instead. +/// </remarks> +public sealed partial class ToggleActionEvent : InstantActionEvent; /// <summary> /// Generic enum keys for toggle-visualizer appearance data & sprite layers. diff --git a/Content.Shared/Tools/Components/SharedHandheldGPSComponent.cs b/Content.Shared/Tools/Components/SharedHandheldGPSComponent.cs deleted file mode 100644 index 3c30738857b..00000000000 --- a/Content.Shared/Tools/Components/SharedHandheldGPSComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ - -namespace Content.Shared.GPS -{ - public abstract partial class SharedHandheldGPSComponent : Component - { - [DataField("updateRate")] - public float UpdateRate = 1.5f; - } -} diff --git a/Content.Shared/Tools/Components/ToolComponent.cs b/Content.Shared/Tools/Components/ToolComponent.cs index 92857ab9054..a7210c6fa07 100644 --- a/Content.Shared/Tools/Components/ToolComponent.cs +++ b/Content.Shared/Tools/Components/ToolComponent.cs @@ -1,52 +1,43 @@ +using Content.Shared.Tools.Systems; using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Utility; -namespace Content.Shared.Tools.Components -{ - [RegisterComponent, NetworkedComponent] // TODO move tool system to shared, and make it a friend. - public sealed partial class ToolComponent : Component - { - [DataField("qualities")] - public PrototypeFlags<ToolQualityPrototype> Qualities { get; set; } = new(); - - /// <summary> - /// For tool interactions that have a delay before action this will modify the rate, time to wait is divided by this value - /// </summary> - [ViewVariables(VVAccess.ReadWrite)] - [DataField("speed")] - public float SpeedModifier { get; set; } = 1; +namespace Content.Shared.Tools.Components; - [DataField("useSound")] - public SoundSpecifier? UseSound { get; set; } - } +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedToolSystem))] +public sealed partial class ToolComponent : Component +{ + [DataField] + public PrototypeFlags<ToolQualityPrototype> Qualities = []; /// <summary> - /// Attempt event called *before* any do afters to see if the tool usage should succeed or not. - /// Raised on both the tool and then target. + /// For tool interactions that have a delay before action this will modify the rate, time to wait is divided by this value /// </summary> - public sealed class ToolUseAttemptEvent : CancellableEntityEventArgs - { - public EntityUid User { get; } + [DataField] + public float SpeedModifier = 1; - public ToolUseAttemptEvent(EntityUid user) - { - User = user; - } - } + [DataField] + public SoundSpecifier? UseSound; +} - /// <summary> - /// Event raised on the user of a tool to see if they can actually use it. - /// </summary> - [ByRefEvent] - public struct ToolUserAttemptUseEvent - { - public EntityUid? Target; - public bool Cancelled = false; +/// <summary> +/// Attempt event called *before* any do afters to see if the tool usage should succeed or not. +/// Raised on both the tool and then target. +/// </summary> +public sealed class ToolUseAttemptEvent(EntityUid user, float fuel) : CancellableEntityEventArgs +{ + public EntityUid User { get; } = user; + public float Fuel = fuel; +} - public ToolUserAttemptUseEvent(EntityUid? target) - { - Target = target; - } - } +/// <summary> +/// Event raised on the user of a tool to see if they can actually use it. +/// </summary> +[ByRefEvent] +public struct ToolUserAttemptUseEvent(EntityUid? target) +{ + public EntityUid? Target = target; + public bool Cancelled = false; } diff --git a/Content.Server/Construction/Components/WelderRefinableComponent.cs b/Content.Shared/Tools/Components/ToolRefinableComponent.cs similarity index 51% rename from Content.Server/Construction/Components/WelderRefinableComponent.cs rename to Content.Shared/Tools/Components/ToolRefinableComponent.cs index 31b029e2415..8adf0443510 100644 --- a/Content.Server/Construction/Components/WelderRefinableComponent.cs +++ b/Content.Shared/Tools/Components/ToolRefinableComponent.cs @@ -1,24 +1,26 @@ using Content.Shared.Tools; using Content.Shared.Storage; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; +using Content.Shared.Tools.Systems; -namespace Content.Server.Construction.Components; +namespace Content.Shared.Tools.Components; /// <summary> /// Used for something that can be refined by welder. /// For example, glass shard can be refined to glass sheet. /// </summary> -[RegisterComponent] -public sealed partial class WelderRefinableComponent : Component +[RegisterComponent, NetworkedComponent, Access(typeof(ToolRefinableSystem))] +public sealed partial class ToolRefinableComponent : Component { - [DataField] - public HashSet<EntitySpawnEntry>? RefineResult; + [DataField(required: true)] + public HashSet<EntitySpawnEntry> RefineResult; [DataField] public float RefineTime = 2f; [DataField] - public float RefineFuel; + public float RefineFuel = 3f; [DataField] public ProtoId<ToolQualityPrototype> QualityNeeded = "Welding"; diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.Welder.cs b/Content.Shared/Tools/Systems/SharedToolSystem.Welder.cs index e790b59cd12..60eafce4742 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.Welder.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.Welder.cs @@ -16,8 +16,15 @@ public void InitializeWelder() { SubscribeLocalEvent<WelderComponent, ExaminedEvent>(OnWelderExamine); SubscribeLocalEvent<WelderComponent, AfterInteractEvent>(OnWelderAfterInteract); - SubscribeLocalEvent<WelderComponent, DoAfterAttemptEvent<ToolDoAfterEvent>>(OnWelderToolUseAttempt); + + SubscribeLocalEvent<WelderComponent, ToolUseAttemptEvent>((uid, comp, ev) => { + CanCancelWelderUse((uid, comp), ev.User, ev.Fuel, ev); + }); + SubscribeLocalEvent<WelderComponent, DoAfterAttemptEvent<ToolDoAfterEvent>>((uid, comp, ev) => { + CanCancelWelderUse((uid, comp), ev.Event.User, ev.Event.Fuel, ev); + }); SubscribeLocalEvent<WelderComponent, ToolDoAfterEvent>(OnWelderDoAfter); + SubscribeLocalEvent<WelderComponent, ItemToggledEvent>(OnToggle); SubscribeLocalEvent<WelderComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt); } @@ -120,23 +127,20 @@ private void OnWelderAfterInteract(Entity<WelderComponent> entity, ref AfterInte } } - private void OnWelderToolUseAttempt(Entity<WelderComponent> entity, ref DoAfterAttemptEvent<ToolDoAfterEvent> args) + private void CanCancelWelderUse(Entity<WelderComponent> entity, EntityUid user, float requiredFuel, CancellableEntityEventArgs ev) { - var user = args.DoAfter.Args.User; - if (!ItemToggle.IsActivated(entity.Owner)) { _popup.PopupClient(Loc.GetString("welder-component-welder-not-lit-message"), entity, user); - args.Cancel(); - return; + ev.Cancel(); } - var (fuel, _) = GetWelderFuelAndCapacity(entity); + var (currentFuel, _) = GetWelderFuelAndCapacity(entity); - if (args.Event.Fuel > fuel) + if (requiredFuel > currentFuel) { _popup.PopupClient(Loc.GetString("welder-component-cannot-weld-message"), entity, user); - args.Cancel(); + ev.Cancel(); } } diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.cs b/Content.Shared/Tools/Systems/SharedToolSystem.cs index 0d7972a8de6..1aac1561656 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.cs @@ -24,7 +24,7 @@ public abstract partial class SharedToolSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] protected readonly SharedInteractionSystem InteractionSystem = default!; - [Dependency] protected readonly SharedItemToggleSystem ItemToggle = default!; + [Dependency] protected readonly ItemToggleSystem ItemToggle = default!; [Dependency] private readonly SharedMapSystem _maps = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainerSystem = default!; @@ -33,6 +33,9 @@ public abstract partial class SharedToolSystem : EntitySystem [Dependency] private readonly TurfSystem _turfs = default!; [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!; + public const string CutQuality = "Cutting"; + public const string PulseQuality = "Pulsing"; + public override void Initialize() { InitializeMultipleTool(); @@ -138,8 +141,8 @@ public bool UseTool( var doAfterArgs = new DoAfterArgs(EntityManager, user, delay / toolComponent.SpeedModifier, toolEvent, tool, target: target, used: tool) { BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, + BreakOnWeightlessMove = false, NeedHand = tool != user, AttemptFrequency = fuel > 0 ? AttemptFrequency.EveryTick : AttemptFrequency.Never }; @@ -217,7 +220,7 @@ private bool CanStartToolUse(EntityUid tool, EntityUid user, EntityUid? target, return false; // check if the tool allows being used - var beforeAttempt = new ToolUseAttemptEvent(user); + var beforeAttempt = new ToolUseAttemptEvent(user, fuel); RaiseLocalEvent(tool, beforeAttempt); if (beforeAttempt.Cancelled) return false; @@ -231,6 +234,9 @@ private bool CanStartToolUse(EntityUid tool, EntityUid user, EntityUid? target, return !beforeAttempt.Cancelled; } + public void SetSpeedModifier(Entity<ToolComponent> ent, float value) => + ent.Comp.SpeedModifier = value; + #region DoAfterEvents [Serializable, NetSerializable] @@ -279,9 +285,7 @@ protected sealed partial class LatticeCuttingCompleteEvent : DoAfterEvent [DataField(required:true)] public NetCoordinates Coordinates; - private LatticeCuttingCompleteEvent() - { - } + private LatticeCuttingCompleteEvent() { } public LatticeCuttingCompleteEvent(NetCoordinates coordinates) { @@ -296,4 +300,3 @@ public LatticeCuttingCompleteEvent(NetCoordinates coordinates) public sealed partial class CableCuttingFinishedEvent : SimpleDoAfterEvent; #endregion - diff --git a/Content.Shared/Tools/Systems/ToolRefinableSystem.cs b/Content.Shared/Tools/Systems/ToolRefinableSystem.cs new file mode 100644 index 00000000000..7dd82136da6 --- /dev/null +++ b/Content.Shared/Tools/Systems/ToolRefinableSystem.cs @@ -0,0 +1,55 @@ +using Content.Shared.Construction; +using Content.Shared.Interaction; +using Content.Shared.Storage; +using Content.Shared.Tools.Components; +using Robust.Shared.Network; +using Robust.Shared.Random; + +namespace Content.Shared.Tools.Systems; + +public sealed class ToolRefinableSystem : EntitySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent<ToolRefinableComponent, InteractUsingEvent>(OnInteractUsing); + SubscribeLocalEvent<ToolRefinableComponent, WelderRefineDoAfterEvent>(OnDoAfter); + } + + private void OnInteractUsing(EntityUid uid, ToolRefinableComponent component, InteractUsingEvent args) + { + if (args.Handled) + return; + + args.Handled = _toolSystem.UseTool( + args.Used, + args.User, + uid, + component.RefineTime, + component.QualityNeeded, + new WelderRefineDoAfterEvent(), + fuel: component.RefineFuel); + } + + private void OnDoAfter(EntityUid uid, ToolRefinableComponent component, WelderRefineDoAfterEvent args) + { + if (args.Cancelled) + return; + + if (_net.IsClient) + return; + + var xform = Transform(uid); + var spawns = EntitySpawnCollection.GetSpawns(component.RefineResult, _random); + foreach (var spawn in spawns) + { + SpawnNextToOrDrop(spawn, uid, xform); + } + + Del(uid); + } +} diff --git a/Content.Shared/UserInterface/ActivatableUISystem.Power.cs b/Content.Shared/UserInterface/ActivatableUISystem.Power.cs index b8a815c7a81..e494253c832 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.Power.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.Power.cs @@ -1,3 +1,5 @@ +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; using Content.Shared.PowerCell; using Robust.Shared.Containers; @@ -5,6 +7,7 @@ namespace Content.Shared.UserInterface; public sealed partial class ActivatableUISystem { + [Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly SharedPowerCellSystem _cell = default!; private void InitializePower() @@ -12,27 +15,22 @@ private void InitializePower() SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt); SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened); SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIClosedEvent>(OnBatteryClosed); - - SubscribeLocalEvent<PowerCellDrawComponent, EntRemovedFromContainerMessage>(OnPowerCellRemoved); + SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled); } - private void OnPowerCellRemoved(EntityUid uid, PowerCellDrawComponent component, EntRemovedFromContainerMessage args) + private void OnToggled(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref ItemToggledEvent args) { - _cell.SetPowerCellDrawEnabled(uid, false); - - if (!HasComp<ActivatableUIRequiresPowerCellComponent>(uid) || - !TryComp(uid, out ActivatableUIComponent? activatable)) - { + // only close ui when losing power + if (!TryComp<ActivatableUIComponent>(ent, out var activatable) || args.Activated) return; - } if (activatable.Key == null) { - Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}"); + Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(ent)}"); return; } - _uiSystem.CloseUi(uid, activatable.Key); + _uiSystem.CloseUi(ent.Owner, activatable.Key); } private void OnBatteryOpened(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIOpenedEvent args) @@ -42,7 +40,7 @@ private void OnBatteryOpened(EntityUid uid, ActivatableUIRequiresPowerCellCompon if (!args.UiKey.Equals(activatable.Key)) return; - _cell.SetPowerCellDrawEnabled(uid, true); + _toggle.TryActivate(uid); } private void OnBatteryClosed(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIClosedEvent args) @@ -54,7 +52,7 @@ private void OnBatteryClosed(EntityUid uid, ActivatableUIRequiresPowerCellCompon // Stop drawing power if this was the last person with the UI open. if (!_uiSystem.IsUiOpen(uid, activatable.Key)) - _cell.SetPowerCellDrawEnabled(uid, false); + _toggle.TryDeactivate(uid); } /// <summary> diff --git a/Content.Shared/UserInterface/ActivatableUISystem.cs b/Content.Shared/UserInterface/ActivatableUISystem.cs index c620097961d..f32d6c98a89 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Interaction.Events; using Content.Shared.Popups; using Content.Shared.Verbs; +using Content.Shared.Whitelist; using Robust.Shared.Utility; using Content.Shared.Whitelist; using Robust.Shared.Containers; @@ -22,6 +23,7 @@ public sealed partial class ActivatableUISystem : EntitySystem [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; public override void Initialize() { @@ -99,8 +101,8 @@ private bool ShouldAddVerb<T>(EntityUid uid, ActivatableUIComponent component, G if (!args.CanAccess) return false; - if (component.RequiredItems is not null && !_entityWhitelist.IsValid(component.RequiredItems, args.Using ?? default) || - component.UserWhitelist is not null && !_entityWhitelist.IsValid(component.UserWhitelist, args.User)) + if (_whitelistSystem.IsWhitelistFail(component.RequiredItems, args.Using ?? default) + || _whitelistSystem.IsWhitelistFail(component.UserWhitelist, args.User)) return false; if (component.RequireHands) @@ -157,8 +159,8 @@ private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, In if (component.VerbOnly) return; - if (component.RequiredItems is null || !_entityWhitelist.IsValid(component.RequiredItems, args.Used) || - component.UserWhitelist is not null && !_entityWhitelist.IsValid(component.UserWhitelist, args.User)) + if (_whitelistSystem.IsWhitelistFailOrNull(component.RequiredItems, args.Used) || + !_whitelistSystem.IsWhitelistFail(component.UserWhitelist, args.User)) return; args.Handled = InteractUI(args.User, uid, component); diff --git a/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs b/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs index 87e2f0890a2..6aef4d0949a 100644 --- a/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs +++ b/Content.Shared/VendingMachines/SharedVendingMachineSystem.Restock.cs @@ -70,8 +70,7 @@ private void OnAfterInteract(EntityUid uid, VendingMachineRestockComponent compo var doAfterArgs = new DoAfterArgs(EntityManager, args.User, (float) component.RestockDelay.TotalSeconds, new RestockDoAfterEvent(), target, target: target, used: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = true, NeedHand = true }; diff --git a/Content.Shared/VendingMachines/VendingMachineComponent.cs b/Content.Shared/VendingMachines/VendingMachineComponent.cs index 23130bb8f39..ab9bdc14f6b 100644 --- a/Content.Shared/VendingMachines/VendingMachineComponent.cs +++ b/Content.Shared/VendingMachines/VendingMachineComponent.cs @@ -87,7 +87,7 @@ public sealed partial class VendingMachineComponent : Component /// </summary> [DataField("soundVend")] // Grabbed from: https://github.com/discordia-space/CEV-Eris/blob/f702afa271136d093ddeb415423240a2ceb212f0/sound/machines/vending_drop.ogg - public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg") + public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/SimpleStation14/Machines/machine_vend.ogg") { Params = new AudioParams { diff --git a/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs b/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs index 55c01c1d6f4..75c85790deb 100644 --- a/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs +++ b/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs @@ -80,7 +80,7 @@ public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, EntityUid weap /// Raised on a melee weapon to calculate potential damage bonuses or decreases. /// </summary> [ByRefEvent] -public record struct GetMeleeDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid User); +public record struct GetMeleeDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid User, bool ResistanceBypass = false); /// <summary> /// Raised on a melee weapon to calculate the attack rate. diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs index b698728193f..851699c9815 100644 --- a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs +++ b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs @@ -11,7 +11,7 @@ namespace Content.Shared.Weapons.Melee; /// <summary> /// When given to a mob lets them do unarmed attacks, or when given to an item lets someone wield it to do attacks. /// </summary> -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] public sealed partial class MeleeWeaponComponent : Component { // TODO: This is becoming bloated as shit. @@ -91,6 +91,12 @@ public sealed partial class MeleeWeaponComponent : Component [DataField, AutoNetworkedField] public bool AutoAttack; + /// <summary> + /// If true, attacks will bypass armor resistances. + /// </summary> + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public bool ResistanceBypass = false; + /// <summary> /// Base damage for this weapon. Can be modified via heavy damage or other means. /// </summary> diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index aa15ecfb286..edca18e5f76 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Inventory; +using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Physics; using Content.Shared.Popups; @@ -32,26 +33,31 @@ namespace Content.Shared.Weapons.Melee; -public abstract partial class SharedMeleeWeaponSystem : EntitySystem +public abstract class SharedMeleeWeaponSystem : EntitySystem { - [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; - [Dependency] protected readonly ActionBlockerSystem Blocker = default!; - [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!; - [Dependency] protected readonly DamageableSystem Damageable = default!; - [Dependency] protected readonly SharedInteractionSystem Interaction = default!; - [Dependency] protected readonly IMapManager MapManager = default!; - [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; - [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly MeleeSoundSystem _meleeSound = default!; - [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly StaminaSystem _stamina = default!; - [Dependency] private readonly ContestsSystem _contests = default!; + [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; + [Dependency] protected readonly ActionBlockerSystem Blocker = default!; + [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!; + [Dependency] protected readonly DamageableSystem Damageable = default!; + [Dependency] protected readonly SharedInteractionSystem Interaction = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly MeleeSoundSystem _meleeSound = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly ContestsSystem _contests = default!; private const int AttackMask = (int) (CollisionGroup.MobMask | CollisionGroup.Opaque); + /// <summary> + /// Maximum amount of targets allowed for a wide-attack. + /// </summary> + public const int MaxTargets = 5; + /// <summary> /// If an attack is released within this buffer it's assumed to be full damage. /// </summary> @@ -62,6 +68,8 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<MeleeWeaponComponent, HandSelectedEvent>(OnMeleeSelected); + SubscribeLocalEvent<MeleeWeaponComponent, ShotAttemptedEvent>(OnMeleeShotAttempted); + SubscribeLocalEvent<MeleeWeaponComponent, GunShotEvent>(OnMeleeShot); SubscribeLocalEvent<BonusMeleeDamageComponent, GetMeleeDamageEvent>(OnGetBonusMeleeDamage); SubscribeLocalEvent<BonusMeleeDamageComponent, GetHeavyDamageModifierEvent>(OnGetBonusHeavyDamageModifier); SubscribeLocalEvent<BonusMeleeAttackRateComponent, GetMeleeAttackRateEvent>(OnGetBonusMeleeAttackRate); @@ -74,7 +82,8 @@ public override void Initialize() SubscribeAllEvent<StopAttackEvent>(OnStopAttack); #if DEBUG - SubscribeLocalEvent<MeleeWeaponComponent, MapInitEvent>(OnMapInit); + SubscribeLocalEvent<MeleeWeaponComponent, + MapInitEvent> (OnMapInit); } private void OnMapInit(EntityUid uid, MeleeWeaponComponent component, MapInitEvent args) @@ -84,6 +93,24 @@ private void OnMapInit(EntityUid uid, MeleeWeaponComponent component, MapInitEve #endif } + private void OnMeleeShotAttempted(EntityUid uid, MeleeWeaponComponent comp, ref ShotAttemptedEvent args) + { + if (comp.NextAttack > Timing.CurTime) + args.Cancel(); + } + + private void OnMeleeShot(EntityUid uid, MeleeWeaponComponent component, ref GunShotEvent args) + { + if (!TryComp<GunComponent>(uid, out var gun)) + return; + + if (gun.NextFire > component.NextAttack) + { + component.NextAttack = gun.NextFire; + DirtyField(uid, component, nameof(MeleeWeaponComponent.NextAttack)); + } + } + private void OnMeleeSelected(EntityUid uid, MeleeWeaponComponent component, HandSelectedEvent args) { var attackRate = GetAttackRate(uid, args.User, component); @@ -104,7 +131,7 @@ private void OnMeleeSelected(EntityUid uid, MeleeWeaponComponent component, Hand return; component.NextAttack = minimum; - Dirty(uid, component); + DirtyField(uid, component, nameof(MeleeWeaponComponent.NextAttack)); } private void OnGetBonusMeleeDamage(EntityUid uid, BonusMeleeDamageComponent component, ref GetMeleeDamageEvent args) @@ -144,28 +171,34 @@ private void OnStopAttack(StopAttackEvent msg, EntitySessionEventArgs args) return; weapon.Attacking = false; - Dirty(weaponUid, weapon); + DirtyField(weaponUid, weapon, nameof(MeleeWeaponComponent.Attacking)); } private void OnLightAttack(LightAttackEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not {} user || - !TryGetWeapon(user, out var weaponUid, out var weapon) || - weaponUid != GetEntity(msg.Weapon) || - weapon.DisableClick) + if (args.SenderSession.AttachedEntity is not {} user) return; + if (!TryGetWeapon(user, out var weaponUid, out var weapon) || + weaponUid != GetEntity(msg.Weapon)) + { + return; + } + AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } private void OnHeavyAttack(HeavyAttackEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity is not {} user || - !TryGetWeapon(user, out var weaponUid, out var weapon) || - weaponUid != GetEntity(msg.Weapon) || - weapon.DisableHeavy) + if (args.SenderSession.AttachedEntity is not {} user) return; + if (!TryGetWeapon(user, out var weaponUid, out var weapon) || + weaponUid != GetEntity(msg.Weapon)) + { + return; + } + AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } @@ -186,10 +219,11 @@ public DamageSpecifier GetDamage(EntityUid uid, EntityUid user, MeleeWeaponCompo if (!Resolve(uid, ref component, false)) return new DamageSpecifier(); - var ev = new GetMeleeDamageEvent(uid, new (component.Damage), new(), user); + var ev = new GetMeleeDamageEvent(uid, new(component.Damage), new(), user, component.ResistanceBypass); RaiseLocalEvent(uid, ref ev); if (component.ContestArgs is not null) + ev.Damage *= _contests.ContestConstructor(user, component.ContestArgs); return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers); @@ -214,9 +248,18 @@ public FixedPoint2 GetHeavyDamageModifier(EntityUid uid, EntityUid user, MeleeWe var ev = new GetHeavyDamageModifierEvent(uid, component.ClickDamageModifier, 1, user); RaiseLocalEvent(uid, ref ev); - return ev.DamageModifier - * ev.Multipliers - * component.HeavyDamageBaseModifier; + return ev.DamageModifier * ev.Multipliers * component.HeavyDamageBaseModifier; + } + + public bool GetResistanceBypass(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + var ev = new GetMeleeDamageEvent(uid, new(component.Damage), new(), user, component.ResistanceBypass); + RaiseLocalEvent(uid, ref ev); + + return ev.ResistanceBypass; } public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen(true)] out MeleeWeaponComponent? melee) @@ -250,7 +293,8 @@ public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen return true; } - return false; + if (!HasComp<VirtualItemComponent>(held)) + return false; } // Use hands clothing if applicable. @@ -279,7 +323,7 @@ public void AttemptLightAttackMiss(EntityUid user, EntityUid weaponUid, MeleeWea public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { - if (!TryComp<TransformComponent>(target, out var targetXform)) + if (!TryComp(target, out TransformComponent? targetXform)) return false; return AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(targetXform.Coordinates)), null); @@ -287,7 +331,7 @@ public bool AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponC public bool AttemptDisarmAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { - if (!TryComp<TransformComponent>(target, out var targetXform)) + if (!TryComp(target, out TransformComponent? targetXform)) return false; return AttemptAttack(user, weaponUid, weapon, new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(targetXform.Coordinates)), null); @@ -307,8 +351,6 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo if (!CombatMode.IsInCombatMode(user)) return false; - var fireRateSwingModifier = 1f; - EntityUid? target = null; switch (attack) { @@ -337,9 +379,6 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo if (!Blocker.CanAttack(user, target, (weaponUid, weapon), true)) return false; break; - case HeavyAttackEvent: - fireRateSwingModifier = weapon.HeavyRateModifier; - break; default: if (!Blocker.CanAttack(user, weapon: (weaponUid, weapon))) return false; @@ -347,7 +386,7 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo } // Windup time checked elsewhere. - var fireRate = TimeSpan.FromSeconds(GetAttackRate(weaponUid, user, weapon) * fireRateSwingModifier); + var fireRate = TimeSpan.FromSeconds(1f / GetAttackRate(weaponUid, user, weapon)); var swings = 0; // TODO: If we get autoattacks then probably need a shotcounter like guns so we can do timing properly. @@ -360,15 +399,13 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo swings++; } - Dirty(weaponUid, weapon); + DirtyField(weaponUid, weapon, nameof(MeleeWeaponComponent.NextAttack)); // Do this AFTER attack so it doesn't spam every tick - // White Dream: Added PlayerUid var ev = new AttemptMeleeEvent { PlayerUid = user }; - RaiseLocalEvent(weaponUid, ref ev); if (ev.Cancelled) @@ -408,7 +445,7 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo throw new NotImplementedException(); } - DoLungeAnimation(user, weaponUid, weapon.Angle, GetCoordinates(attack.Coordinates).ToMap(EntityManager, TransformSystem), weapon.Range, animation); + DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation); } var attackEv = new MeleeAttackEvent(weaponUid); @@ -422,13 +459,15 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { - var damage = GetDamage(meleeUid, user, component); + // If I do not come back later to fix Light Attacks being Heavy Attacks you can throw me in the spider pit -Errant + var damage = GetDamage(meleeUid, user, component) * GetHeavyDamageModifier(meleeUid, user, component); var target = GetEntity(ev.Target); + var resistanceBypass = GetResistanceBypass(meleeUid, user, component); // For consistency with wide attacks stuff needs damageable. if (Deleted(target) || !HasComp<DamageableComponent>(target) || - !TryComp<TransformComponent>(target, out var targetXform) || + !TryComp(target, out TransformComponent? targetXform) || // Not in LOS. !InRange(user, target.Value, component.Range, session)) { @@ -438,12 +477,14 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity // TODO: This needs fixing if (meleeUid == user) { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (light) using their hands and missed"); } else { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed"); } var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, null); @@ -468,7 +509,7 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity var weapon = GetEntity(ev.Weapon); - Interaction.DoContactInteraction(weapon, target); + // We skip weapon -> target interaction, as forensics system applies DNA on hit Interaction.DoContactInteraction(user, weapon); // If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a @@ -480,9 +521,9 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity RaiseLocalEvent(target.Value, attackedEvent); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList); - var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin: user, partMultiplier: component.ClickPartDamageMultiplier); + var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user, ignoreResistances: resistanceBypass, partMultiplier: component.ClickPartDamageMultiplier); - if (damageResult != null && damageResult.Any()) + if (damageResult is {Empty: false}) { // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage)) @@ -492,12 +533,14 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity if (meleeUid == user) { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using their hands and dealt {damageResult.GetTotal():damage} damage"); } else { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage"); } @@ -511,15 +554,15 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity } } - protected abstract void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform); + protected abstract void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform); private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { // TODO: This is copy-paste as fuck with DoPreciseAttack - if (!TryComp<TransformComponent>(user, out var userXform)) + if (!TryComp(user, out TransformComponent? userXform)) return false; - var targetMap = GetCoordinates(ev.Coordinates).ToMap(EntityManager, TransformSystem); + var targetMap = TransformSystem.ToMapCoordinates(GetCoordinates(ev.Coordinates)); if (targetMap.MapId != userXform.MapID) return false; @@ -537,21 +580,23 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU var userPos = TransformSystem.GetWorldPosition(userXform); var direction = targetMap.Position - userPos; - var distance = Math.Min(component.Range * component.HeavyRangeModifier, direction.Length()); + var distance = Math.Min(component.Range, direction.Length()); - var damage = GetDamage(meleeUid, user, component) * GetHeavyDamageModifier(meleeUid, user, component); + var damage = GetDamage(meleeUid, user, component); var entities = GetEntityList(ev.Entities); if (entities.Count == 0) { if (meleeUid == user) { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (heavy) using their hands and missed"); } else { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Low, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Low, $"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed"); } var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, direction); @@ -563,15 +608,23 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU return true; } - var maxTargets = component.MaxTargets; - if (entities.Count > maxTargets) - entities.RemoveRange(maxTargets, entities.Count - maxTargets); + // Naughty input + if (entities.Count > component.MaxTargets) + { + entities.RemoveRange(component.MaxTargets, entities.Count - component.MaxTargets); + } // Validate client for (var i = entities.Count - 1; i >= 0; i--) { - if (ArcRaySuccessful(entities[i], userPos, direction.ToWorldAngle(), component.Angle, distance, - userXform.MapID, user, session)) + if (ArcRaySuccessful(entities[i], + userPos, + direction.ToWorldAngle(), + component.Angle, + distance, + userXform.MapID, + user, + session)) { continue; } @@ -608,7 +661,7 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU // For stuff that cares about it being attacked. foreach (var target in targets) { - Interaction.DoContactInteraction(weapon, target); + // We skip weapon -> target interaction, as forensics system applies DNA on hit // If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a // somewhat messy scuffle. See also, light attacks. @@ -617,14 +670,18 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU var appliedDamage = new DamageSpecifier(); - foreach (var entity in targets) + for (var i = targets.Count - 1; i >= 0; i--) { + var entity = targets[i]; // We raise an attack attempt here as well, // primarily because this was an untargeted wideswing: if a subscriber to that event cared about // the potential target (such as for pacifism), they need to be made aware of the target here. // In that case, just continue. if (!Blocker.CanAttack(user, entity, (weapon, component))) + { + targets.RemoveAt(i); continue; + } var attackedEvent = new AttackedEvent(meleeUid, user, GetCoordinates(ev.Coordinates)); RaiseLocalEvent(entity, attackedEvent); @@ -634,16 +691,24 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero) { + // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor + if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage)) + { + _stamina.TakeStaminaDamage(entity, (bluntDamage * component.BluntStaminaDamageFactor).Float(), visual: false, source: user, with: meleeUid == user ? null : meleeUid); + } + appliedDamage += damageResult; if (meleeUid == user) { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.GetTotal():damage} damage"); } else { - AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium, + AdminLogger.Add(LogType.MeleeHit, + LogImpact.Medium, $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage"); } } @@ -677,8 +742,13 @@ protected HashSet<EntityUid> ArcRayCast(Vector2 position, Angle angle, Angle arc { var castAngle = new Angle(baseAngle + increment * i); var res = _physics.IntersectRay(mapId, - new CollisionRay(position, castAngle.ToWorldVec(), - AttackMask), range, ignore, false).ToList(); + new CollisionRay(position, + castAngle.ToWorldVec(), + AttackMask), + range, + ignore, + false) + .ToList(); if (res.Count != 0) { @@ -689,8 +759,14 @@ protected HashSet<EntityUid> ArcRayCast(Vector2 position, Angle angle, Angle arc return resSet; } - protected virtual bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range, - MapId mapId, EntityUid ignore, ICommonSession? session) + protected virtual bool ArcRaySuccessful(EntityUid targetUid, + Vector2 position, + Angle angle, + Angle arcWidth, + float range, + MapId mapId, + EntityUid ignore, + ICommonSession? session) { // Only matters for server. return true; @@ -739,7 +815,7 @@ protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid private void DoLungeAnimation(EntityUid user, EntityUid weapon, Angle angle, MapCoordinates coordinates, float length, string? animation) { // TODO: Assert that offset eyes are still okay. - if (!TryComp<TransformComponent>(user, out var userXform)) + if (!TryComp(user, out TransformComponent? userXform)) return; var invMatrix = TransformSystem.GetInvWorldMatrix(userXform); @@ -777,6 +853,7 @@ private void OnItemToggle(EntityUid uid, ItemToggleMeleeWeaponComponent itemTogg //Setting deactivated damage to the weapon's regular value before changing it. itemToggleMelee.DeactivatedDamage ??= meleeWeapon.Damage; meleeWeapon.Damage = itemToggleMelee.ActivatedDamage; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.Damage)); } meleeWeapon.SoundHit = itemToggleMelee.ActivatedSoundOnHit; @@ -786,6 +863,7 @@ private void OnItemToggle(EntityUid uid, ItemToggleMeleeWeaponComponent itemTogg //Setting the deactivated sound on no damage hit to the weapon's regular value before changing it. itemToggleMelee.DeactivatedSoundOnHitNoDamage ??= meleeWeapon.SoundNoDamage; meleeWeapon.SoundNoDamage = itemToggleMelee.ActivatedSoundOnHitNoDamage; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.SoundNoDamage)); } if (itemToggleMelee.ActivatedSoundOnSwing != null) @@ -793,28 +871,41 @@ private void OnItemToggle(EntityUid uid, ItemToggleMeleeWeaponComponent itemTogg //Setting the deactivated sound on no damage hit to the weapon's regular value before changing it. itemToggleMelee.DeactivatedSoundOnSwing ??= meleeWeapon.SoundSwing; meleeWeapon.SoundSwing = itemToggleMelee.ActivatedSoundOnSwing; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.SoundSwing)); } if (itemToggleMelee.DeactivatedSecret) + { meleeWeapon.Hidden = false; + } } else { if (itemToggleMelee.DeactivatedDamage != null) + { meleeWeapon.Damage = itemToggleMelee.DeactivatedDamage; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.Damage)); + } meleeWeapon.SoundHit = itemToggleMelee.DeactivatedSoundOnHit; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.SoundHit)); if (itemToggleMelee.DeactivatedSoundOnHitNoDamage != null) + { meleeWeapon.SoundNoDamage = itemToggleMelee.DeactivatedSoundOnHitNoDamage; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.SoundNoDamage)); + } if (itemToggleMelee.DeactivatedSoundOnSwing != null) + { meleeWeapon.SoundSwing = itemToggleMelee.DeactivatedSoundOnSwing; + DirtyField(uid, meleeWeapon, nameof(MeleeWeaponComponent.SoundSwing)); + } if (itemToggleMelee.DeactivatedSecret) + { meleeWeapon.Hidden = true; + } } - - Dirty(uid, meleeWeapon); } } diff --git a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs index 21da0a3ca45..3d7f9df458e 100644 --- a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs +++ b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs @@ -223,7 +223,7 @@ protected virtual void StartTether(EntityUid gunUid, BaseForceGunComponent compo _blocker.UpdateCanMove(target); // Invisible tether entity - var tether = Spawn("TetherEntity", Transform(target).MapPosition); + var tether = Spawn("TetherEntity", TransformSystem.GetMapCoordinates(target)); var tetherPhysics = Comp<PhysicsComponent>(tether); component.TetherEntity = tether; _physics.WakeBody(tether); diff --git a/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs b/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs index 62af124252b..3e1111a97d1 100644 --- a/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs @@ -30,18 +30,6 @@ public sealed partial class CartridgeAmmoComponent : AmmoComponent [AutoNetworkedField] public bool Spent = false; - /// <summary> - /// How much the ammo spreads when shot, in degrees. Does nothing if count is 0. - /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("spread")] - public Angle Spread = Angle.FromDegrees(5); - - /// <summary> - /// How many prototypes are spawned when shot. - /// </summary> - [ViewVariables(VVAccess.ReadWrite), DataField("count")] - public int Count = 1; - /// <summary> /// Caseless ammunition. /// </summary> diff --git a/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs index e64ec0d68de..1c74cff7fae 100644 --- a/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs @@ -20,6 +20,12 @@ public sealed partial class ChamberMagazineAmmoProviderComponent : MagazineAmmoP [ViewVariables(VVAccess.ReadWrite), DataField("autoCycle"), AutoNetworkedField] public bool AutoCycle = true; + /// <summary> + /// Can the gun be racked, which opens and then instantly closes the bolt to cycle a round. + /// </summary> + [ViewVariables(VVAccess.ReadWrite), DataField("canRack"), AutoNetworkedField] + public bool CanRack = true; + [ViewVariables(VVAccess.ReadWrite), DataField("soundBoltClosed"), AutoNetworkedField] public SoundSpecifier? BoltClosedSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg"); diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index d522df5395e..056b14f6924 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -1,6 +1,5 @@ -using Content.Shared.Damage; +using System.Numerics; using Content.Shared.Nyanotrasen.Abilities.Oni; -using Content.Shared.Tag; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio; @@ -159,6 +158,30 @@ public sealed partial class GunComponent : Component [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] public int ShotsPerBurstModified = 3; + /// <summary> + /// How long time must pass between burstfire shots. + /// </summary> + [DataField, AutoNetworkedField] + public float BurstCooldown = 0.25f; + + /// <summary> + /// The fire rate of the weapon in burst fire mode. + /// </summary> + [DataField, AutoNetworkedField] + public float BurstFireRate = 8f; + + /// <summary> + /// Whether the burst fire mode has been activated. + /// </summary> + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public bool BurstActivated = false; + + /// <summary> + /// The burst fire bullet count. + /// </summary> + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int BurstShotsCount = 0; + /// <summary> /// Used for tracking semi-auto / burst /// </summary> @@ -243,16 +266,16 @@ public sealed partial class GunComponent : Component public bool ClumsyProof = false; /// <summary> - /// The percentage chance of a given gun to accidentally discharge if violently thrown into a wall or person + /// Firing direction for an item not being held (e.g. shuttle cannons, thrown guns still firing). /// </summary> [DataField] - public float FireOnDropChance = 0.1f; + public Vector2 DefaultDirection = new Vector2(0, -1); /// <summary> - /// Whether or not this gun is truly Recoilless, such as Lasers, and therefore shouldn't move the user. + /// The percentage chance of a given gun to accidentally discharge if violently thrown into a wall or person /// </summary> [DataField] - public bool DoRecoil = true; + public float FireOnDropChance = 0.1f; } [Flags] diff --git a/Content.Shared/Weapons/Ranged/Components/GunRequiresWieldComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunRequiresWieldComponent.cs index fa3732209f5..2016459b9d5 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunRequiresWieldComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunRequiresWieldComponent.cs @@ -15,4 +15,7 @@ public sealed partial class GunRequiresWieldComponent : Component [DataField, AutoNetworkedField] public TimeSpan PopupCooldown = TimeSpan.FromSeconds(1); + + [DataField] + public LocId? WieldRequiresExamineMessage = "gunrequireswield-component-examine"; } diff --git a/Content.Shared/Weapons/Ranged/Events/BeforeGunShootEvent.cs b/Content.Shared/Weapons/Ranged/Events/BeforeGunShootEvent.cs new file mode 100644 index 00000000000..1d3317c840f --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/BeforeGunShootEvent.cs @@ -0,0 +1,20 @@ +using Content.Shared.Inventory; +using Content.Shared.Weapons.Ranged.Components; + +namespace Content.Shared.Weapons.Ranged.Events; +/// <summary> +/// This event is triggered on an entity right before they shoot a gun. +/// </summary> +public sealed partial class SelfBeforeGunShotEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public readonly EntityUid Shooter; + public readonly Entity<GunComponent> Gun; + public readonly List<(EntityUid? Entity, IShootable Shootable)> Ammo; + public SelfBeforeGunShotEvent(EntityUid shooter, Entity<GunComponent> gun, List<(EntityUid? Entity, IShootable Shootable)> ammo) + { + Shooter = shooter; + Gun = gun; + Ammo = ammo; + } +} diff --git a/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs b/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs index d61862bf1a0..1626c5a63fb 100644 --- a/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs +++ b/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs @@ -21,7 +21,7 @@ public record struct ShotAttemptedEvent public bool Cancelled { get; private set; } - /// </summary> + /// <summary> /// Prevent the gun from shooting /// </summary> public void Cancel() @@ -29,7 +29,7 @@ public void Cancel() Cancelled = true; } - /// </summary> + /// <summary> /// Allow the gun to shoot again, only use if you know what you are doing /// </summary> public void Uncancel() diff --git a/Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs b/Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs new file mode 100644 index 00000000000..57f731889a4 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared.Weapons.Ranged.Events; + +[ByRefEvent] +public readonly record struct UpdateClientAmmoEvent(); \ No newline at end of file diff --git a/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs b/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs index 8e44576d283..bae5b95a193 100644 --- a/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Popups; using Content.Shared.Verbs; using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Prototypes; namespace Content.Shared.Weapons.Ranged.Systems; @@ -99,13 +100,21 @@ private void SetFireMode(EntityUid uid, BatteryWeaponFireModesComponent componen component.CurrentFireMode = index; Dirty(uid, component); - if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProvider)) + if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProviderComponent)) { if (!_prototypeManager.TryIndex<EntityPrototype>(fireMode.Prototype, out var prototype)) return; - projectileBatteryAmmoProvider.Prototype = fireMode.Prototype; - projectileBatteryAmmoProvider.FireCost = fireMode.FireCost; + // TODO: Have this get the info directly from the batteryComponent when power is moved to shared. + var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost; + projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype; + projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost; + float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost; + projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots/FireCostDiff); + projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity/FireCostDiff); + Dirty(uid, projectileBatteryAmmoProviderComponent); + var updateClientAmmoEvent = new UpdateClientAmmoEvent(); + RaiseLocalEvent(uid, ref updateClientAmmoEvent); if (user != null) { diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index 88acd81d5bd..639ae31d0e8 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -15,6 +15,7 @@ public abstract partial class SharedGunSystem { [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + protected virtual void InitializeBallistic() { SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentInit>(OnBallisticInit); @@ -35,7 +36,7 @@ private void OnBallisticUse(EntityUid uid, BallisticAmmoProviderComponent compon if (args.Handled) return; - ManualCycle(uid, component, Transform(uid).MapPosition, args.User); + ManualCycle(uid, component, TransformSystem.GetMapCoordinates(uid), args.User); args.Handled = true; } @@ -77,8 +78,7 @@ private void OnBallisticAfterInteract(EntityUid uid, BallisticAmmoProviderCompon _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.FillDelay, new AmmoFillDoAfterEvent(), used: uid, target: args.Target, eventTarget: uid) { - BreakOnTargetMove = true, - BreakOnUserMove = true, + BreakOnMove = true, BreakOnDamage = false, NeedHand = true }); @@ -126,7 +126,7 @@ void SimulateInsertAmmo(EntityUid ammo, EntityUid ammoProvider, EntityCoordinate if (ent == null) continue; - if (!target.Whitelist.IsValid(ent.Value)) + if (_whitelistSystem.IsWhitelistFail(target.Whitelist, ent.Value)) { Popup( Loc.GetString("gun-ballistic-transfer-invalid", @@ -165,7 +165,7 @@ private void OnBallisticVerb(EntityUid uid, BallisticAmmoProviderComponent compo { Text = Loc.GetString("gun-ballistic-cycle"), Disabled = GetBallisticShots(component) == 0, - Act = () => ManualCycle(uid, component, Transform(uid).MapPosition, args.User), + Act = () => ManualCycle(uid, component, TransformSystem.GetMapCoordinates(uid), args.User), }); } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs index adae26a223a..3060e2c1a93 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs @@ -67,7 +67,10 @@ private void OnChamberUse(EntityUid uid, ChamberMagazineAmmoProviderComponent co return; args.Handled = true; - UseChambered(uid, component, args.User); + if (component.CanRack) + UseChambered(uid, component, args.User); + else + ToggleBolt(uid, component, args.User); } /// <summary> @@ -75,7 +78,7 @@ private void OnChamberUse(EntityUid uid, ChamberMagazineAmmoProviderComponent co /// </summary> private void OnChamberActivationVerb(EntityUid uid, ChamberMagazineAmmoProviderComponent component, GetVerbsEvent<ActivationVerb> args) { - if (!args.CanAccess || !args.CanInteract || component.BoltClosed == null) + if (!args.CanAccess || !args.CanInteract || component.BoltClosed == null || !component.CanRack) return; args.Verbs.Add(new ActivationVerb() @@ -108,7 +111,7 @@ private void UseChambered(EntityUid uid, ChamberMagazineAmmoProviderComponent co else { // Similar to below just due to prediction. - TransformSystem.DetachParentToNull(chamberEnt.Value, Transform(chamberEnt.Value)); + TransformSystem.DetachEntity(chamberEnt.Value, Transform(chamberEnt.Value)); } } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs index 77ee419ac3b..7ef57df5391 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs @@ -33,7 +33,7 @@ private bool TryGetClothingSlotEntity(EntityUid uid, ClothingSlotAmmoProviderCom { slotEntity = null; - if (!Containers.TryGetContainingContainer(uid, out var container)) + if (!Containers.TryGetContainingContainer((uid, null, null), out var container)) return false; var user = container.Owner; @@ -42,7 +42,7 @@ private bool TryGetClothingSlotEntity(EntityUid uid, ClothingSlotAmmoProviderCom while (enumerator.NextItem(out var item)) { - if (component.ProviderWhitelist == null || !component.ProviderWhitelist.IsValid(item, EntityManager)) + if (_whitelistSystem.IsWhitelistFailOrNull(component.ProviderWhitelist, item)) continue; slotEntity = item; diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 7afb41239c6..37e6451f92d 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -11,7 +11,6 @@ using Content.Shared.Gravity; using Content.Shared.Hands; using Content.Shared.Hands.Components; -using Content.Shared.Item; // Delta-V: Felinids in duffelbags can't shoot. using Content.Shared.Popups; using Content.Shared.Projectiles; using Content.Shared.Tag; @@ -114,18 +113,14 @@ private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args) private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args) { - var curTime = Timing.CurTime; - - if (component.NextFire < curTime) - component.NextFire = curTime; - - var meleeCooldown = TimeSpan.FromSeconds(component.MeleeCooldown); - - component.NextFire += meleeCooldown; - while (component.NextFire <= curTime) - component.NextFire += meleeCooldown; + if (!TryComp<MeleeWeaponComponent>(uid, out var melee)) + return; - Dirty(uid, component); + if (melee.NextAttack > component.NextFire) + { + component.NextFire = melee.NextAttack; + Dirty(uid, component); + } } private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args) @@ -134,8 +129,7 @@ private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args) if (user == null || !_combatMode.IsInCombatMode(user) || - !TryGetGun(user.Value, out var ent, out var gun) || - HasComp<ItemComponent>(user)) // Delta-V: Felinids in duffelbags can't shoot. + !TryGetGun(user.Value, out var ent, out var gun)) { return; } @@ -209,14 +203,6 @@ private void StopShooting(EntityUid uid, GunComponent gun) Dirty(uid, gun); } - /// <summary> - /// Sets the targeted entity of the gun. Should be called before attempting to shoot to avoid shooting over the target. - /// </summary> - public void SetTarget(GunComponent gun, EntityUid target) - { - gun.Target = target; - } - /// <summary> /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot. /// </summary> @@ -232,7 +218,7 @@ public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, Ent /// </summary> public void AttemptShoot(EntityUid gunUid, GunComponent gun) { - var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1)); + var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection); gun.ShootCoordinates = coordinates; AttemptShoot(gunUid, gunUid, gun); gun.ShotCounter = 0; @@ -272,6 +258,9 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified); + if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated) + fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate); + // First shot // Previously we checked shotcounter but in some cases all the bullets got dumped at once // curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker. @@ -292,19 +281,23 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) // Get how many shots we're actually allowed to make, due to clip size or otherwise. // Don't do this in the loop so we still reset NextFire. - switch (gun.SelectedMode) + if (!gun.BurstActivated) { - case SelectiveFire.SemiAuto: - shots = Math.Min(shots, 1 - gun.ShotCounter); - break; - case SelectiveFire.Burst: - shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); - break; - case SelectiveFire.FullAuto: - break; - default: - throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!"); - } + switch (gun.SelectedMode) + { + case SelectiveFire.SemiAuto: + shots = Math.Min(shots, 1 - gun.ShotCounter); + break; + case SelectiveFire.Burst: + shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); + break; + case SelectiveFire.FullAuto: + break; + default: + throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!"); + } + } else + shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); var attemptEv = new AttemptShootEvent(user, null); RaiseLocalEvent(gunUid, ref attemptEv); @@ -312,10 +305,10 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) if (attemptEv.Cancelled) { if (attemptEv.Message != null) - { PopupSystem.PopupClient(attemptEv.Message, gunUid, user); - } + gun.BurstActivated = false; + gun.BurstShotsCount = 0; gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); return; } @@ -342,6 +335,10 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) var emptyGunShotEvent = new OnEmptyGunShotEvent(); RaiseLocalEvent(gunUid, ref emptyGunShotEvent); + gun.BurstActivated = false; + gun.BurstShotsCount = 0; + gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown); + // Play empty gun sounds if relevant // If they're firing an existing clip then don't play anything. if (shots > 0) @@ -361,16 +358,40 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) return; } + // Handle burstfire + if (gun.SelectedMode == SelectiveFire.Burst) + { + gun.BurstActivated = true; + } + if (gun.BurstActivated) + { + gun.BurstShotsCount += shots; + if (gun.BurstShotsCount >= gun.ShotsPerBurstModified) + { + gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown); + gun.BurstActivated = false; + gun.BurstShotsCount = 0; + } + } + // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). - Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems); + Shoot( + gunUid, + gun, + ev.Ammo, + fromCoordinates, + toCoordinates.Value, + out var userImpulse, + user, + throwItems: attemptEv.ThrowItems); var shotEv = new GunShotEvent(user, ev.Ammo); RaiseLocalEvent(gunUid, ref shotEv); - if (gun.DoRecoil - && userImpulse - && TryComp<PhysicsComponent>(user, out var userPhysics) - && _gravity.IsWeightless(user, userPhysics)) - CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); + if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics)) + { + if (_gravity.IsWeightless(user, userPhysics)) + CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); + } Dirty(gunUid, gun); } @@ -413,7 +434,7 @@ public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocit Projectiles.SetShooter(uid, projectile, user ?? gunUid); projectile.Weapon = gunUid; - TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle()); + TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle() + projectile.Angle); } protected abstract void Popup(string message, EntityUid? uid, EntityUid? user); @@ -455,7 +476,7 @@ protected void EjectCartridge( { Angle ejectAngle = angle.Value; ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun - ThrowingSystem.TryThrow(entity, ejectAngle.ToVec(), 625f); + ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f); } if (playSound && TryComp<CartridgeAmmoComponent>(entity, out var cartridge)) { diff --git a/Content.Shared/Weapons/Reflect/ReflectComponent.cs b/Content.Shared/Weapons/Reflect/ReflectComponent.cs index 5d8432ac776..5edd445afcb 100644 --- a/Content.Shared/Weapons/Reflect/ReflectComponent.cs +++ b/Content.Shared/Weapons/Reflect/ReflectComponent.cs @@ -5,16 +5,11 @@ namespace Content.Shared.Weapons.Reflect; /// <summary> /// Entities with this component have a chance to reflect projectiles and hitscan shots +/// Uses <c>ItemToggleComponent</c> to control reflection. /// </summary> [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ReflectComponent : Component { - /// <summary> - /// Can only reflect when enabled - /// </summary> - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public bool Enabled = true; - /// <summary> /// What we reflect. /// </summary> diff --git a/Content.Shared/Weapons/Reflect/ReflectSystem.cs b/Content.Shared/Weapons/Reflect/ReflectSystem.cs index 76c98a427f5..881b547f27f 100644 --- a/Content.Shared/Weapons/Reflect/ReflectSystem.cs +++ b/Content.Shared/Weapons/Reflect/ReflectSystem.cs @@ -3,20 +3,17 @@ using Content.Shared.Administration.Logs; using Content.Shared.Alert; using Content.Shared.Audio; -using Content.Shared.Damage.Components; using Content.Shared.Database; -using Content.Shared.Gravity; using Content.Shared.Hands; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; +using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Popups; using Content.Shared.Projectiles; -using Content.Shared.Standing; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; -using Content.Shared.WhiteDream.BloodCult.BloodCultist; -using Content.Shared.WhiteDream.BloodCult.Items; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; using Robust.Shared.Physics.Components; @@ -31,21 +28,16 @@ namespace Content.Shared.Weapons.Reflect; /// </summary> public sealed class ReflectSystem : EntitySystem { + [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedGravitySystem _gravity = default!; - [Dependency] private readonly StandingStateSystem _standing = default!; - [Dependency] private readonly AlertsSystem _alerts = default!; - - [ValidatePrototypeId<AlertPrototype>] - private const string DeflectingAlert = "Deflecting"; public override void Initialize() { @@ -68,7 +60,7 @@ private void OnReflectUserHitscan(EntityUid uid, ReflectUserComponent component, if (args.Reflected) return; - foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.WITHOUT_POCKET)) + foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET)) { if (!TryReflectHitscan(uid, ent, args.Shooter, args.SourceItem, args.Direction, out var dir)) continue; @@ -81,7 +73,7 @@ private void OnReflectUserHitscan(EntityUid uid, ReflectUserComponent component, private void OnReflectUserCollide(EntityUid uid, ReflectUserComponent component, ref ProjectileReflectAttemptEvent args) { - foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.WITHOUT_POCKET)) + foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET)) { if (!TryReflectProjectile(uid, ent, args.ProjUid)) continue; @@ -102,24 +94,15 @@ private void OnReflectCollide(EntityUid uid, ReflectComponent component, ref Pro private bool TryReflectProjectile(EntityUid user, EntityUid reflector, EntityUid projectile, ProjectileComponent? projectileComp = null, ReflectComponent? reflect = null) { - // Do we have the components needed to try a reflect at all? - if ( - !Resolve(reflector, ref reflect, false) || - !reflect.Enabled || + if (!Resolve(reflector, ref reflect, false) || + !_toggle.IsActivated(reflector) || !TryComp<ReflectiveComponent>(projectile, out var reflective) || (reflect.Reflects & reflective.Reflective) == 0x0 || - !TryComp<PhysicsComponent>(projectile, out var physics) || - TryComp<StaminaComponent>(reflector, out var staminaComponent) && staminaComponent.Critical || - _standing.IsDown(reflector) - ) - return false; - - // Non cultists can't use cult items to reflect anything. - if (HasComp<CultItemComponent>(reflector) && !HasComp<BloodCultistComponent>(user)) - return false; - - if (!_random.Prob(CalcReflectChance(reflector, reflect))) + !_random.Prob(reflect.ReflectProb) || + !TryComp<PhysicsComponent>(projectile, out var physics)) + { return false; + } var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite(); var existingVelocity = _physics.GetMapLinearVelocity(projectile, component: physics); @@ -157,34 +140,6 @@ private bool TryReflectProjectile(EntityUid user, EntityUid reflector, EntityUid return true; } - private float CalcReflectChance(EntityUid reflector, ReflectComponent reflect) - { - /* - * The rules of deflection are as follows: - * If you innately reflect things via magic, biology etc., you always have a full chance. - * If you are standing up and standing still, you're prepared to deflect and have full chance. - * If you have velocity, your deflection chance depends on your velocity, clamped. - * If you are floating, your chance is the minimum value possible. - * You cannot deflect if you are knocked down or stunned. - */ - - if (reflect.Innate) - return reflect.ReflectProb; - - if (_gravity.IsWeightless(reflector)) - return reflect.MinReflectProb; - - if (!TryComp<PhysicsComponent>(reflector, out var reflectorPhysics)) - return reflect.ReflectProb; - - return MathHelper.Lerp( - reflect.MinReflectProb, - reflect.ReflectProb, - // Inverse progression between velocities fed in as progression between probabilities. We go high -> low so the output here needs to be _inverted_. - 1 - Math.Clamp((reflectorPhysics.LinearVelocity.Length() - reflect.VelocityBeforeNotMaxProb) / (reflect.VelocityBeforeMinProb - reflect.VelocityBeforeNotMaxProb), 0, 1) - ); - } - private void OnReflectHitscan(EntityUid uid, ReflectComponent component, ref HitScanReflectAttemptEvent args) { if (args.Reflected || @@ -208,19 +163,13 @@ private bool TryReflectHitscan( Vector2 direction, [NotNullWhen(true)] out Vector2? newDirection) { - newDirection = null; if (!TryComp<ReflectComponent>(reflector, out var reflect) || - !reflect.Enabled || - TryComp<StaminaComponent>(reflector, out var staminaComponent) && staminaComponent.Critical || - _standing.IsDown(reflector)) - return false; - - // Non cultists can't use cult items to reflect anything. - if (HasComp<CultItemComponent>(reflector) && !HasComp<BloodCultistComponent>(user)) - return false; - - if (!_random.Prob(CalcReflectChance(reflector, reflect))) + !_toggle.IsActivated(reflector) || + !_random.Prob(reflect.ReflectProb)) + { + newDirection = null; return false; + } if (_netManager.IsServer) { @@ -245,9 +194,6 @@ private void OnReflectEquipped(EntityUid uid, ReflectComponent component, GotEqu return; EnsureComp<ReflectUserComponent>(args.Equipee); - - if (component.Enabled) - EnableAlert(args.Equipee); } private void OnReflectUnequipped(EntityUid uid, ReflectComponent comp, GotUnequippedEvent args) @@ -261,9 +207,6 @@ private void OnReflectHandEquipped(EntityUid uid, ReflectComponent component, Go return; EnsureComp<ReflectUserComponent>(args.User); - - if (component.Enabled) - EnableAlert(args.User); } private void OnReflectHandUnequipped(EntityUid uid, ReflectComponent component, GotUnequippedHandEvent args) @@ -273,13 +216,8 @@ private void OnReflectHandUnequipped(EntityUid uid, ReflectComponent component, private void OnToggleReflect(EntityUid uid, ReflectComponent comp, ref ItemToggledEvent args) { - comp.Enabled = args.Activated; - Dirty(uid, comp); - - if (comp.Enabled) - EnableAlert(uid); - else - DisableAlert(uid); + if (args.User is {} user) + RefreshReflectUser(user); } /// <summary> @@ -287,28 +225,15 @@ private void OnToggleReflect(EntityUid uid, ReflectComponent comp, ref ItemToggl /// </summary> private void RefreshReflectUser(EntityUid user) { - foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.WITHOUT_POCKET)) + foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.All & ~SlotFlags.POCKET)) { - if (!HasComp<ReflectComponent>(ent)) + if (!HasComp<ReflectComponent>(ent) || !_toggle.IsActivated(ent)) continue; EnsureComp<ReflectUserComponent>(user); - EnableAlert(user); - return; } RemCompDeferred<ReflectUserComponent>(user); - DisableAlert(user); - } - - private void EnableAlert(EntityUid alertee) - { - _alerts.ShowAlert(alertee, DeflectingAlert); - } - - private void DisableAlert(EntityUid alertee) - { - _alerts.ClearAlert(alertee, DeflectingAlert); } } diff --git a/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultLeaderComponent.cs b/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultLeaderComponent.cs index 6c0a118cac5..34e53efdb8d 100644 --- a/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultLeaderComponent.cs +++ b/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultLeaderComponent.cs @@ -6,10 +6,10 @@ namespace Content.Shared.WhiteDream.BloodCult.BloodCultist; [RegisterComponent, NetworkedComponent] -public sealed partial class BloodCultLeaderComponent : Component, IAntagStatusIconComponent +public sealed partial class BloodCultLeaderComponent : Component { [DataField] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "BloodCultLeader"; + public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "BloodCultLeader"; [DataField] public bool IconVisibleToGhost { get; set; } = true; diff --git a/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultistComponent.cs b/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultistComponent.cs index 288d496ee5a..d9ad12bbcea 100644 --- a/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultistComponent.cs +++ b/Content.Shared/WhiteDream/BloodCult/BloodCultist/BloodCultistComponent.cs @@ -10,7 +10,7 @@ namespace Content.Shared.WhiteDream.BloodCult.BloodCultist; [RegisterComponent, NetworkedComponent] -public sealed partial class BloodCultistComponent : Component, IAntagStatusIconComponent +public sealed partial class BloodCultistComponent : Component { [DataField] public float HolyConvertTime = 15f; @@ -19,7 +19,7 @@ public sealed partial class BloodCultistComponent : Component, IAntagStatusIconC public int MaximumAllowedEmpowers = 4; [DataField] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "BloodCultMember"; + public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "BloodCultMember"; [DataField] public bool IconVisibleToGhost { get; set; } = true; diff --git a/Content.Shared/WhiteDream/BloodCult/Constructs/ConstructComponent.cs b/Content.Shared/WhiteDream/BloodCult/Constructs/ConstructComponent.cs index 24d67ed2c8d..81921cbe79b 100644 --- a/Content.Shared/WhiteDream/BloodCult/Constructs/ConstructComponent.cs +++ b/Content.Shared/WhiteDream/BloodCult/Constructs/ConstructComponent.cs @@ -5,7 +5,7 @@ namespace Content.Shared.WhiteDream.BloodCult.Constructs; [RegisterComponent] -public sealed partial class ConstructComponent : Component, IAntagStatusIconComponent +public sealed partial class ConstructComponent : Component { [DataField] public List<EntProtoId> Actions = new(); @@ -17,7 +17,7 @@ public sealed partial class ConstructComponent : Component, IAntagStatusIconComp public float TransformDelay = 1; [DataField] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "BloodCultMember"; + public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "BloodCultMember"; [DataField] public bool IconVisibleToGhost { get; set; } = true; diff --git a/Content.Shared/Whitelist/EntityWhitelist.cs b/Content.Shared/Whitelist/EntityWhitelist.cs index 895759be958..3e4e2fecb2e 100644 --- a/Content.Shared/Whitelist/EntityWhitelist.cs +++ b/Content.Shared/Whitelist/EntityWhitelist.cs @@ -54,14 +54,4 @@ public sealed partial class EntityWhitelist /// </summary> [DataField] public bool RequireAll; - - [Obsolete("Use WhitelistSystem")] - public bool IsValid(EntityUid uid, IEntityManager? man = null) - { - var sys = man?.System<EntityWhitelistSystem>() ?? - IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<EntityWhitelistSystem>(); - - return sys.IsValid(this, uid); - - } } diff --git a/Content.Shared/Wieldable/WieldableSystem.cs b/Content.Shared/Wieldable/WieldableSystem.cs index 95fc69e0614..490636dbcea 100644 --- a/Content.Shared/Wieldable/WieldableSystem.cs +++ b/Content.Shared/Wieldable/WieldableSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item; @@ -17,6 +18,7 @@ using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Wieldable.Components; using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; using Robust.Shared.Timing; namespace Content.Shared.Wieldable; @@ -32,6 +34,7 @@ public sealed class WieldableSystem : EntitySystem [Dependency] private readonly UseDelaySystem _delay = default!; [Dependency] private readonly SharedGunSystem _gun = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _netManager = default!; public override void Initialize() { @@ -45,6 +48,7 @@ public override void Initialize() SubscribeLocalEvent<WieldableComponent, HandDeselectedEvent>(OnDeselectWieldable); SubscribeLocalEvent<MeleeRequiresWieldComponent, AttemptMeleeEvent>(OnMeleeAttempt); + SubscribeLocalEvent<GunRequiresWieldComponent, ExaminedEvent>(OnExamineRequires); SubscribeLocalEvent<GunRequiresWieldComponent, ShotAttemptedEvent>(OnShootAttempt); SubscribeLocalEvent<GunWieldBonusComponent, ItemWieldedEvent>(OnGunWielded); SubscribeLocalEvent<GunWieldBonusComponent, ItemUnwieldedEvent>(OnGunUnwielded); @@ -114,8 +118,17 @@ private void OnGunRefreshModifiers(Entity<GunWieldBonusComponent> bonus, ref Gun } } + private void OnExamineRequires(Entity<GunRequiresWieldComponent> entity, ref ExaminedEvent args) + { + if(entity.Comp.WieldRequiresExamineMessage != null) + args.PushText(Loc.GetString(entity.Comp.WieldRequiresExamineMessage)); + } + private void OnExamine(EntityUid uid, GunWieldBonusComponent component, ref ExaminedEvent args) { + if (HasComp<GunRequiresWieldComponent>(uid)) + return; + if (component.WieldBonusExamineMessage != null) args.PushText(Loc.GetString(component.WieldBonusExamineMessage)); } @@ -213,19 +226,25 @@ public bool TryWield(EntityUid used, WieldableComponent component, EntityUid use if (component.WieldSound != null) _audioSystem.PlayPredicted(component.WieldSound, used, user); - var virtuals = new List<EntityUid>(); - for (var i = 0; i < component.FreeHandsRequired; i++) + //This section handles spawning the virtual item(s) to occupy the required additional hand(s). + //Since the client can't currently predict entity spawning, only do this if this is running serverside. + //Remove this check if TrySpawnVirtualItem in SharedVirtualItemSystem is allowed to complete clientside. + if (_netManager.IsServer) { - if (_virtualItemSystem.TrySpawnVirtualItemInHand(used, user, out var virtualItem, true)) + var virtuals = new List<EntityUid>(); + for (var i = 0; i < component.FreeHandsRequired; i++) { - virtuals.Add(virtualItem.Value); - continue; - } + if (_virtualItemSystem.TrySpawnVirtualItemInHand(used, user, out var virtualItem, true)) + { + virtuals.Add(virtualItem.Value); + continue; + } - foreach (var existingVirtual in virtuals) - QueueDel(existingVirtual); + foreach (var existingVirtual in virtuals) + QueueDel(existingVirtual); - return false; + return false; + } } if (TryComp(used, out UseDelayComponent? useDelay) @@ -233,7 +252,7 @@ public bool TryWield(EntityUid used, WieldableComponent component, EntityUid use return false; var selfMessage = Loc.GetString("wieldable-component-successful-wield", ("item", used)); - var othersMessage = Loc.GetString("wieldable-component-successful-wield-other", ("user", user), ("item", used)); + var othersMessage = Loc.GetString("wieldable-component-successful-wield-other", ("user", Identity.Entity(user, EntityManager)), ("item", used)); _popupSystem.PopupPredicted(selfMessage, othersMessage, user, user); var targEv = new ItemWieldedEvent(); @@ -278,7 +297,7 @@ private void OnItemUnwielded(EntityUid uid, WieldableComponent component, ItemUn _audioSystem.PlayPredicted(component.UnwieldSound, uid, args.User); var selfMessage = Loc.GetString("wieldable-component-failed-wield", ("item", uid)); - var othersMessage = Loc.GetString("wieldable-component-failed-wield-other", ("user", args.User.Value), ("item", uid)); + var othersMessage = Loc.GetString("wieldable-component-failed-wield-other", ("user", Identity.Entity(args.User.Value, EntityManager)), ("item", uid)); _popupSystem.PopupPredicted(selfMessage, othersMessage, args.User.Value, args.User.Value); } diff --git a/Content.Shared/Wires/SharedWiresSystem.cs b/Content.Shared/Wires/SharedWiresSystem.cs index b4b0768e0f7..24f3ad8e764 100644 --- a/Content.Shared/Wires/SharedWiresSystem.cs +++ b/Content.Shared/Wires/SharedWiresSystem.cs @@ -18,11 +18,17 @@ public override void Initialize() { base.Initialize(); + SubscribeLocalEvent<WiresPanelComponent, ComponentStartup>(OnStartup); SubscribeLocalEvent<WiresPanelComponent, WirePanelDoAfterEvent>(OnPanelDoAfter); SubscribeLocalEvent<WiresPanelComponent, InteractUsingEvent>(OnInteractUsing); SubscribeLocalEvent<WiresPanelComponent, ExaminedEvent>(OnExamine); } + private void OnStartup(Entity<WiresPanelComponent> ent, ref ComponentStartup args) + { + UpdateAppearance(ent, ent); + } + private void OnPanelDoAfter(EntityUid uid, WiresPanelComponent panel, WirePanelDoAfterEvent args) { if (args.Cancelled) diff --git a/Content.Shared/Zombies/InitialInfectedComponent.cs b/Content.Shared/Zombies/InitialInfectedComponent.cs index 3200dd7f5ee..b69d1515f26 100644 --- a/Content.Shared/Zombies/InitialInfectedComponent.cs +++ b/Content.Shared/Zombies/InitialInfectedComponent.cs @@ -1,4 +1,3 @@ -using Content.Shared.Antag; using Content.Shared.StatusIcon; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -6,11 +5,8 @@ namespace Content.Shared.Zombies; [RegisterComponent, NetworkedComponent] -public sealed partial class InitialInfectedComponent : Component, IAntagStatusIconComponent +public sealed partial class InitialInfectedComponent : Component { - [DataField("initialInfectedStatusIcon")] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "InitialInfectedFaction"; - [DataField] - public bool IconVisibleToGhost { get; set; } = true; + public ProtoId<FactionIconPrototype> StatusIcon = "InitialInfectedFaction"; } diff --git a/Content.Shared/Zombies/SharedZombieSystem.cs b/Content.Shared/Zombies/SharedZombieSystem.cs index 6d9103639f6..0388450a8c4 100644 --- a/Content.Shared/Zombies/SharedZombieSystem.cs +++ b/Content.Shared/Zombies/SharedZombieSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Movement.Systems; +using Content.Shared.NameModifier.EntitySystems; namespace Content.Shared.Zombies; @@ -10,6 +11,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent<ZombieComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshSpeed); + SubscribeLocalEvent<ZombieComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers); } private void OnRefreshSpeed(EntityUid uid, ZombieComponent component, RefreshMovementSpeedModifiersEvent args) @@ -17,4 +19,9 @@ private void OnRefreshSpeed(EntityUid uid, ZombieComponent component, RefreshMov var mod = component.ZombieMovementSpeedDebuff; args.ModifySpeed(mod, mod); } + + private void OnRefreshNameModifiers(Entity<ZombieComponent> entity, ref RefreshNameModifiersEvent args) + { + args.AddModifier("zombie-name-prefix"); + } } diff --git a/Content.Shared/Zombies/ShowZombieIconsComponent.cs b/Content.Shared/Zombies/ShowZombieIconsComponent.cs deleted file mode 100644 index a2bc85c0746..00000000000 --- a/Content.Shared/Zombies/ShowZombieIconsComponent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Zombies; - -/// <summary> -/// Makes it so an entity can view ZombieAntagIcons. -/// </summary> -[RegisterComponent, NetworkedComponent] -public sealed partial class ShowZombieIconsComponent: Component -{ - -} diff --git a/Content.Shared/Zombies/ZombieComponent.cs b/Content.Shared/Zombies/ZombieComponent.cs index 3673a2c51d5..94e1e5afec4 100644 --- a/Content.Shared/Zombies/ZombieComponent.cs +++ b/Content.Shared/Zombies/ZombieComponent.cs @@ -14,7 +14,7 @@ namespace Content.Shared.Zombies; [RegisterComponent, NetworkedComponent] -public sealed partial class ZombieComponent : Component, IAntagStatusIconComponent +public sealed partial class ZombieComponent : Component { /// <summary> /// The baseline infection chance you have if you are completely nude @@ -62,12 +62,6 @@ public sealed partial class ZombieComponent : Component, IAntagStatusIconCompone [DataField("zombieRoleId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))] public string ZombieRoleId = "Zombie"; - /// <summary> - /// The EntityName of the humanoid to restore in case of cloning - /// </summary> - [DataField("beforeZombifiedEntityName"), ViewVariables(VVAccess.ReadOnly)] - public string BeforeZombifiedEntityName = string.Empty; - /// <summary> /// The CustomBaseLayers of the humanoid to restore in case of cloning /// </summary> @@ -95,10 +89,7 @@ public sealed partial class ZombieComponent : Component, IAntagStatusIconCompone public TimeSpan NextTick; [DataField("zombieStatusIcon")] - public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "ZombieFaction"; - - [DataField] - public bool IconVisibleToGhost { get; set; } = true; + public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "ZombieFaction"; /// <summary> /// Healing each second diff --git a/Content.Shared/_NF/Shuttles/Events/SetInertiaDampeningRequest.cs b/Content.Shared/_NF/Shuttles/Events/SetInertiaDampeningRequest.cs new file mode 100644 index 00000000000..70d7dddabb5 --- /dev/null +++ b/Content.Shared/_NF/Shuttles/Events/SetInertiaDampeningRequest.cs @@ -0,0 +1,27 @@ +// New Frontiers - This file is licensed under AGPLv3 +// Copyright (c) 2024 New Frontiers Contributors +// See AGPLv3.txt for details. +using Robust.Shared.Serialization; + +namespace Content.Shared._NF.Shuttles.Events +{ + /// <summary> + /// Raised on the client when it wishes to change the inertial dampening of a ship. + /// </summary> + [Serializable, NetSerializable] + public sealed class SetInertiaDampeningRequest : BoundUserInterfaceMessage + { + public NetEntity? ShuttleEntityUid { get; set; } + public InertiaDampeningMode Mode { get; set; } + } + + [Serializable, NetSerializable] + public enum InertiaDampeningMode : byte + { + Off = 0, + Dampen = 1, + Anchor = 2, + Station = 3, // Reserved for station status, should not be used in requests. + Query = 255 // Reserved for requests - does not set the mode, only returns its state. + } +} diff --git a/Content.Shared/_Shitmed/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/_Shitmed/Surgery/SharedSurgerySystem.Steps.cs index be84137a765..e6b9814f548 100644 --- a/Content.Shared/_Shitmed/Surgery/SharedSurgerySystem.Steps.cs +++ b/Content.Shared/_Shitmed/Surgery/SharedSurgerySystem.Steps.cs @@ -744,8 +744,7 @@ public bool TryDoSurgeryStep(EntityUid body, EntityUid targetPart, EntityUid use var doAfter = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(duration), ev, body, part) { - BreakOnUserMove = true, - BreakOnTargetMove = true, + BreakOnMove = true, CancelDuplicate = true, DuplicateCondition = DuplicateConditions.SameEvent, NeedHand = true, diff --git a/Content.Tests/AssemblyInfo.cs b/Content.Tests/AssemblyInfo.cs new file mode 100644 index 00000000000..8e01f41b183 --- /dev/null +++ b/Content.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using NUnit.Framework; + +[assembly: Parallelizable(ParallelScope.Fixtures)] diff --git a/Content.Tests/Server/Utility/IPAddressExtTest.cs b/Content.Tests/Server/Utility/IPAddressExtTest.cs index a57430267b8..dba8a0e6aae 100644 --- a/Content.Tests/Server/Utility/IPAddressExtTest.cs +++ b/Content.Tests/Server/Utility/IPAddressExtTest.cs @@ -26,6 +26,7 @@ public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress [TestCase("10.128.240.50/30", "10.128.240.52")] [TestCase("10.128.240.50/30", "10.128.239.50")] [TestCase("10.128.240.50/30", "10.127.240.51")] + [TestCase("10.128.240.50/30", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) { var ipAddressObj = IPAddress.Parse(ipAddress); @@ -51,6 +52,7 @@ public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress [TestCase("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")] [TestCase("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")] [TestCase("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] + [TestCase("2001:db8:abcd:0012::0/128", "10.128.239.50")] // ReSharper restore StringLiteralTypo public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) { diff --git a/Resources/Locale/en-US/_NF/shuttles/console.ftl b/Resources/Locale/en-US/_NF/shuttles/console.ftl new file mode 100644 index 00000000000..d518dc22130 --- /dev/null +++ b/Resources/Locale/en-US/_NF/shuttles/console.ftl @@ -0,0 +1,3 @@ +shuttle-console-inertia-dampener-off = Cruise +shuttle-console-inertia-dampener-dampen = Drive +shuttle-console-inertia-dampener-anchor = Park \ No newline at end of file diff --git a/Resources/Locale/en-US/abilities/goliath.ftl b/Resources/Locale/en-US/abilities/goliath.ftl new file mode 100644 index 00000000000..953d32a5c6c --- /dev/null +++ b/Resources/Locale/en-US/abilities/goliath.ftl @@ -0,0 +1 @@ +tentacle-ability-use-popup = {CAPITALIZE(THE($entity))} digs its tentacles under the ground! diff --git a/Resources/Locale/en-US/administration/admin-alerts.ftl b/Resources/Locale/en-US/administration/admin-alerts.ftl new file mode 100644 index 00000000000..40ab4737dcc --- /dev/null +++ b/Resources/Locale/en-US/administration/admin-alerts.ftl @@ -0,0 +1 @@ +admin-alert-shared-connection = {$player} is sharing a connection with {$otherCount} connected player(s): {$otherList} diff --git a/Resources/Locale/en-US/administration/admin-verbs.ftl b/Resources/Locale/en-US/administration/admin-verbs.ftl index 03f92f7c420..16715087ee4 100644 --- a/Resources/Locale/en-US/administration/admin-verbs.ftl +++ b/Resources/Locale/en-US/administration/admin-verbs.ftl @@ -6,6 +6,7 @@ admin-verbs-admin-logs-entity = Entity Logs admin-verbs-teleport-to = Teleport To admin-verbs-teleport-here = Teleport Here admin-verbs-freeze = Freeze +admin-verbs-freeze-and-mute = Freeze And Mute admin-verbs-unfreeze = Unfreeze admin-verbs-erase = Erase admin-verbs-erase-description = Removes the player from the round and crew manifest and deletes their chat messages. diff --git a/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl b/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl index 068f88dc5be..1a721c93f36 100644 --- a/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl +++ b/Resources/Locale/en-US/administration/commands/add-uplink-command.ftl @@ -3,5 +3,6 @@ add-uplink-command-help = Usage: adduplink [username] [item-id] add-uplink-command-completion-1 = Username (defaults to self) add-uplink-command-completion-2 = Uplink uid (default to PDA) +add-uplink-command-completion-3 = Is uplink discount enabled add-uplink-command-error-1 = Selected player doesn't control any entity add-uplink-command-error-2 = Failed to add uplink to the player \ No newline at end of file diff --git a/Resources/Locale/en-US/administration/commands/babyjail.ftl b/Resources/Locale/en-US/administration/commands/babyjail.ftl new file mode 100644 index 00000000000..bbe77e5826e --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/babyjail.ftl @@ -0,0 +1,19 @@ +cmd-babyjail-desc = Toggles the baby jail, which enables stricter restrictions on who's allowed to join the server. +cmd-babyjail-help = Usage: babyjail +babyjail-command-enabled = Baby jail has been enabled. +babyjail-command-disabled = Baby jail has been disabled. + +cmd-babyjail_show_reason-desc = Toggles whether or not to show connecting clients the reason why the baby jail blocked them from joining. +cmd-babyjail_show_reason-help = Usage: babyjail_show_reason +babyjail-command-show-reason-enabled = The baby jail will now show a reason to users it blocks from connecting. +babyjail-command-show-reason-disabled = The baby jail will no longer show a reason to users it blocks from connecting. + +cmd-babyjail_max_account_age-desc = Gets or sets the maximum account age in minutes that an account can have to be allowed to connect with the baby jail enabled. +cmd-babyjail_max_account_age-help = Usage: babyjail_max_account_age <hours> +babyjail-command-max-account-age-is = The maximum account age for the baby jail is {$hours} hours. +babyjail-command-max-account-age-set = Set the maximum account age for the baby jail to {$hours} hours. + +cmd-babyjail_max_overall_hours-desc = Gets or sets the maximum overall playtime in minutes that an account can have to be allowed to connect with the baby jail enabled. +cmd-babyjail_max_overall_hours-help = Usage: babyjail_max_overall_hours <hours> +babyjail-command-max-overall-hours-is = The maximum overall playtime for the baby jail is {$hours} hours. +babyjail-command-max-overall-hours-set = Set the maximum overall playtime for the baby jail to {$hours} hours. diff --git a/Resources/Locale/en-US/administration/ui/actions.ftl b/Resources/Locale/en-US/administration/ui/actions.ftl index b067774d0ae..498835b46c9 100644 --- a/Resources/Locale/en-US/administration/ui/actions.ftl +++ b/Resources/Locale/en-US/administration/ui/actions.ftl @@ -6,6 +6,7 @@ admin-player-actions-ahelp = AHelp admin-player-actions-respawn = Respawn admin-player-actions-spawn = Spawn here admin-player-spawn-failed = Failed to find valid coordinates +admin-player-actions-player-panel = Open Player Panel admin-player-actions-clone = Clone admin-player-actions-follow = Follow diff --git a/Resources/Locale/en-US/administration/ui/admin-menu-window.ftl b/Resources/Locale/en-US/administration/ui/admin-menu-window.ftl index c759e4c2cb1..03b2046a9e1 100644 --- a/Resources/Locale/en-US/administration/ui/admin-menu-window.ftl +++ b/Resources/Locale/en-US/administration/ui/admin-menu-window.ftl @@ -7,5 +7,6 @@ admin-menu-atmos-tab = Atmos admin-menu-round-tab = Round admin-menu-server-tab = Server admin-menu-panic-bunker-tab = Panic Bunker +admin-menu-baby-jail-tab = Baby Jail admin-menu-players-tab = Players admin-menu-objects-tab = Objects diff --git a/Resources/Locale/en-US/administration/ui/player-panel.ftl b/Resources/Locale/en-US/administration/ui/player-panel.ftl index 208268d5898..2c9d5086c0a 100644 --- a/Resources/Locale/en-US/administration/ui/player-panel.ftl +++ b/Resources/Locale/en-US/administration/ui/player-panel.ftl @@ -1 +1,23 @@ player-panel-job-whitelists = Job Whitelists +player-panel-title = information for {$player} +player-panel-username = Username: {$player} +player-panel-whitelisted = Whitelisted: +player-panel-bans = Total Bans: {$totalBans} +player-panel-rolebans = Total Role Bans: {$totalRoleBans} +player-panel-notes = Total Notes: {$totalNotes} +player-panel-playtime = Total Playtime: {$days}d:{$hours}h:{$minutes}m +player-panel-shared-connections = Shared Connections: {$sharedConnections} + +player-panel-copy-username = Copy +player-panel-show-notes = Notes +player-panel-show-bans = Show Bans +player-panel-help = Ahelp +player-panel-freeze-and-mute = Freeze & Mute +player-panel-freeze = Freeze +player-panel-unfreeze = Unfreeze +player-panel-kick = Kick +player-panel-ban = Ban +player-panel-logs = Logs +player-panel-delete = Delete +player-panel-rejuvenate = Rejuvenate +player-panel-false = False diff --git a/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl b/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl new file mode 100644 index 00000000000..0a68d08063a --- /dev/null +++ b/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl @@ -0,0 +1,10 @@ +silicon-law-ui-verb = Manage laws +silicon-law-ui-title = Silicon laws +silicon-law-ui-new-law = New law +silicon-law-ui-save = Save changes +silicon-law-ui-plus-one = +1 +silicon-law-ui-minus-one = -1 +silicon-law-ui-delete = Delete +silicon-law-ui-check-corrupted = Corrupted law +silicon-law-ui-check-corrupted-tooltip = If the law identifier should be set as 'corrupted', so symbols shuffling around. +silicon-law-ui-placeholder = Type here to change law text... diff --git a/Resources/Locale/en-US/administration/ui/tabs/babyjail-tab.ftl b/Resources/Locale/en-US/administration/ui/tabs/babyjail-tab.ftl new file mode 100644 index 00000000000..9800e7f9f10 --- /dev/null +++ b/Resources/Locale/en-US/administration/ui/tabs/babyjail-tab.ftl @@ -0,0 +1,16 @@ +admin-ui-baby-jail-window-title = Baby Jail + +admin-ui-baby-jail-enabled = Baby Jail Enabled +admin-ui-baby-jail-disabled = Baby Jail Disabled +admin-ui-baby-jail-tooltip = The baby jail restricts players from joining if their account is too old or they do have too much overall playtime on this server. + +admin-ui-baby-jail-show-reason = Show Reason +admin-ui-baby-jail-show-reason-tooltip = Show the user why they were blocked from connecting by the baby jail. + +admin-ui-baby-jail-max-account-age = Max. Account Age +admin-ui-baby-jail-max-overall-hours = Max. Overall Playtime + +admin-ui-baby-jail-is-enabled = [font size=20][bold]The baby jail is currently enabled.[/bold][/font] + +admin-ui-baby-jail-enabled-admin-alert = The baby jail has been enabled. +admin-ui-baby-jail-disabled-admin-alert = The baby jail has been disabled. diff --git a/Resources/Locale/en-US/administration/ui/tabs/object-tab.ftl b/Resources/Locale/en-US/administration/ui/tabs/object-tab.ftl new file mode 100644 index 00000000000..da84ea4f163 --- /dev/null +++ b/Resources/Locale/en-US/administration/ui/tabs/object-tab.ftl @@ -0,0 +1,10 @@ +object-tab-entity-id = Entity ID +object-tab-object-name = Object name + +object-tab-object-type = Object type: +object-tab-object-search = Search... + +object-tab-object-type-grids = Grids +object-tab-object-type-maps = Maps +object-tab-object-type-stations = Stations +object-tab-refresh-button = Refresh diff --git a/Resources/Locale/en-US/bonk/components/bonkable-component.ftl b/Resources/Locale/en-US/bonk/components/bonkable-component.ftl index 560b10c46ec..1a79da3509f 100644 --- a/Resources/Locale/en-US/bonk/components/bonkable-component.ftl +++ b/Resources/Locale/en-US/bonk/components/bonkable-component.ftl @@ -1,2 +1,4 @@ -bonkable-success-message-others = { CAPITALIZE(THE($user)) } bonks { POSS-ADJ($user) } head against { THE($bonkable) } -bonkable-success-message-user = You bonk your head against { THE($bonkable) } +forced-bonkable-success-message = { CAPITALIZE($bonker) } bonks {$victim}s head against { THE($bonkable) }! + +bonkable-success-message-user = You bonk your head against { THE($bonkable) }! +bonkable-success-message-others = {$victim} bonks their head against { THE($bonkable) }! diff --git a/Resources/Locale/en-US/borg/borg.ftl b/Resources/Locale/en-US/borg/borg.ftl index c9005eb7961..6f31ca781f9 100644 --- a/Resources/Locale/en-US/borg/borg.ftl +++ b/Resources/Locale/en-US/borg/borg.ftl @@ -20,5 +20,7 @@ borg-ui-module-counter = {$actual}/{$max} # Transponder borg-transponder-disabled-popup = A brain shoots out the top of {$name}! +borg-transponder-disabling-popup = Your transponder begins to lock you out of the chassis! +borg-transponder-destroying-popup = The self destruct of {$name} starts beeping! borg-transponder-emagged-disabled-popup = Your transponder's lights go out! borg-transponder-emagged-destroyed-popup = Your transponder's fuse blows! diff --git a/Resources/Locale/en-US/cargo/cargo-console-component.ftl b/Resources/Locale/en-US/cargo/cargo-console-component.ftl index 941e0fa1805..9d57f6c67b0 100644 --- a/Resources/Locale/en-US/cargo/cargo-console-component.ftl +++ b/Resources/Locale/en-US/cargo/cargo-console-component.ftl @@ -30,7 +30,7 @@ cargo-console-snip-snip = Order trimmed to capacity cargo-console-insufficient-funds = Insufficient funds (require {$cost}) cargo-console-unfulfilled = No room to fulfill order cargo-console-trade-station = Sent to {$destination} -cargo-console-unlock-approved-order-broadcast = [bold]{$productName} x{$orderAmount}[/bold], which cost [bold]{$cost}[/bold], was approved by [bold]{$approverName}, {$approverJob}[/bold] +cargo-console-unlock-approved-order-broadcast = [bold]{$productName} x{$orderAmount}[/bold], which cost [bold]{$cost}[/bold], was approved by [bold]{$approver}[/bold] cargo-console-paper-print-name = Order #{$orderNumber} cargo-console-paper-print-text = diff --git a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl index cfa1b1424d2..95e3a0114fe 100644 --- a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl @@ -24,3 +24,7 @@ glimmer-monitor-program-name = Glimmer Monitor glimmer-monitor-current-glimmer = Current Glimmer: {$glimmer}Ψ glimmer-monitor-interval = Interval glimmer-monitor-sync = Sync + +astro-nav-program-name = AstroNav + +med-tek-program-name = MedTek \ No newline at end of file diff --git a/Resources/Locale/en-US/chat/commands/ghost-command.ftl b/Resources/Locale/en-US/chat/commands/ghost-command.ftl new file mode 100644 index 00000000000..08e78d34ce5 --- /dev/null +++ b/Resources/Locale/en-US/chat/commands/ghost-command.ftl @@ -0,0 +1,5 @@ +ghost-command-description = Give up on life and become a ghost. +ghost-command-help-text = The ghost command turns you into a ghost and makes the character you played permanently catatonic. + Please note that you cannot return to your character's body after ghosting. +ghost-command-no-session = You have no session, you can't ghost. +ghost-command-denied = You cannot ghost right now. diff --git a/Resources/Locale/en-US/chat/commands/suicide-command.ftl b/Resources/Locale/en-US/chat/commands/suicide-command.ftl index 6748aa630cd..36e861169b8 100644 --- a/Resources/Locale/en-US/chat/commands/suicide-command.ftl +++ b/Resources/Locale/en-US/chat/commands/suicide-command.ftl @@ -6,3 +6,5 @@ suicide-command-help-text = The suicide command gives you a quick way out of a r suicide-command-default-text-others = {$name} is attempting to bite their own tongue! suicide-command-default-text-self = You attempt to bite your own tongue! suicide-command-already-dead = You can't suicide. You're dead. +suicide-command-no-mind = You have no mind! +suicide-command-denied = You cannot suicide right now. diff --git a/Resources/Locale/en-US/chemistry/components/transformable-container-component.ftl b/Resources/Locale/en-US/chemistry/components/transformable-container-component.ftl index 21f096273d1..ce43bd714ad 100644 --- a/Resources/Locale/en-US/chemistry/components/transformable-container-component.ftl +++ b/Resources/Locale/en-US/chemistry/components/transformable-container-component.ftl @@ -1 +1 @@ -transformable-container-component-glass = {$name} glass +transformable-container-component-glass = {$reagent} glass diff --git a/Resources/Locale/en-US/clothing/components/cursed-mask.ftl b/Resources/Locale/en-US/clothing/components/cursed-mask.ftl new file mode 100644 index 00000000000..c93a6cfb87f --- /dev/null +++ b/Resources/Locale/en-US/clothing/components/cursed-mask.ftl @@ -0,0 +1,5 @@ +cursed-mask-examine-Neutral = It depicts an entirely unremarkable visage. +cursed-mask-examine-Joy = It depicts a face basking in joy. +cursed-mask-examine-Despair = It depicts a face wraught with despair. +cursed-mask-examine-Anger = It depicts a furious expression locked in rage. +cursed-mask-takeover-popup = The mask seizes control over your body! diff --git a/Resources/Locale/en-US/cluwne/cluwne.ftl b/Resources/Locale/en-US/cluwne/cluwne.ftl index 206df8657dd..0ffd3f32dfb 100644 --- a/Resources/Locale/en-US/cluwne/cluwne.ftl +++ b/Resources/Locale/en-US/cluwne/cluwne.ftl @@ -1,2 +1,2 @@ cluwne-transform = {CAPITALIZE(THE($target))} turned into a cluwne! -cluwne-name-prefix = Cluwnified {$target} +cluwne-name-prefix = cluwnified {$baseName} diff --git a/Resources/Locale/en-US/communications/communications-console-component.ftl b/Resources/Locale/en-US/communications/communications-console-component.ftl index bead43df286..bf8a6c33c9c 100644 --- a/Resources/Locale/en-US/communications/communications-console-component.ftl +++ b/Resources/Locale/en-US/communications/communications-console-component.ftl @@ -5,6 +5,7 @@ comms-console-menu-announcement-button = Announce comms-console-menu-broadcast-button = Broadcast comms-console-menu-call-shuttle = Call emergency shuttle comms-console-menu-recall-shuttle = Recall emergency shuttle +comms-console-menu-time-remaining = Time remaining: {$time} # Popup comms-console-permission-denied = Permission denied @@ -18,4 +19,4 @@ comms-console-announcement-unknown-sender = Unknown # Comms console variant titles comms-console-announcement-title-station = Communications Console comms-console-announcement-title-centcom = Central Command -comms-console-announcement-title-nukie = Syndicate Nuclear Operative +comms-console-announcement-title-nukie = Syndicate Nuclear Operative \ No newline at end of file diff --git a/Resources/Locale/en-US/components/station-anchor-component.ftl b/Resources/Locale/en-US/components/station-anchor-component.ftl new file mode 100644 index 00000000000..fd9d5ea4aec --- /dev/null +++ b/Resources/Locale/en-US/components/station-anchor-component.ftl @@ -0,0 +1,2 @@ +station-anchor-unanchoring-failed = Can't unanchor an active station anchor +station-anchor-window-title = Station Anchor diff --git a/Resources/Locale/en-US/connection-messages.ftl b/Resources/Locale/en-US/connection-messages.ftl index 7f4afce8b96..475e8fdcbe3 100644 --- a/Resources/Locale/en-US/connection-messages.ftl +++ b/Resources/Locale/en-US/connection-messages.ftl @@ -1,24 +1,12 @@ -whitelist-not-whitelisted = You are not whitelisted. - -# proper handling for having a min/max or not -whitelist-playercount-invalid = {$min -> - [0] The whitelist for this server only applies below {$max} players. - *[other] The whitelist for this server only applies above {$min} {$max -> - [2147483647] -> players, so you may be able to join later. - *[other] -> players and below {$max} players, so you may be able to join later. - } -} -whitelist-not-whitelisted-rp = You are not whitelisted. To become whitelisted, visit our Discord (which can be found at https://spacestation14.io) and check the #rp-whitelist channel. - -cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist. -cmd-whitelistadd-help = Usage: whitelistadd <username> +cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist. +cmd-whitelistadd-help = Usage: whitelistadd <username or User ID> cmd-whitelistadd-existing = {$username} is already on the whitelist! cmd-whitelistadd-added = {$username} added to the whitelist cmd-whitelistadd-not-found = Unable to find '{$username}' cmd-whitelistadd-arg-player = [player] cmd-whitelistremove-desc = Removes the player with the given username from the server whitelist. -cmd-whitelistremove-help = Usage: whitelistremove <username> +cmd-whitelistremove-help = Usage: whitelistremove <username or User ID> cmd-whitelistremove-existing = {$username} is not on the whitelist! cmd-whitelistremove-removed = {$username} removed from the whitelist cmd-whitelistremove-not-found = Unable to find '{$username}' @@ -37,5 +25,33 @@ ban-banned-3 = Attempts to circumvent this ban such as creating a new account wi soft-player-cap-full = The server is full! panic-bunker-account-denied = This server is in panic bunker mode, often enabled as a precaution against raids. New connections by accounts not meeting certain requirements are temporarily not accepted. Try again later panic-bunker-account-denied-reason = This server is in panic bunker mode, often enabled as a precaution against raids. New connections by accounts not meeting certain requirements are temporarily not accepted. Try again later. Reason: "{$reason}" -panic-bunker-account-reason-account = Your Space Station 14 account is too new. It must be older than {$minutes} minutes -panic-bunker-account-reason-overall = Your overall playtime on the server must be greater than {$hours} hours +panic-bunker-account-reason-account = Your Space Station 14 account is too new. It must be older than {$hours} hours +panic-bunker-account-reason-overall = Your overall playtime on the server must be greater than {$hours} hours. + +whitelist-playtime = You do not have enough playtime to join this server. You need at least {$hours} minutes of playtime to join this server. +whitelist-player-count = This server is currently not accepting players. Please try again later. +whitelist-notes = You currently have too many admin notes to join this server. You can check your notes by typing /adminremarks in chat. +whitelist-manual = You are not whitelisted on this server. +whitelist-blacklisted = You are blacklisted from this server. +whitelist-always-deny = You are not allowed to join this server. +whitelist-fail-prefix = Not whitelisted: {$msg} +whitelist-misconfigured = The server is misconfigured and is not accepting players. Please contact the server owner and try again later. + +cmd-blacklistadd-desc = Adds the player with the given username to the server blacklist. +cmd-blacklistadd-help = Usage: blacklistadd <username> +cmd-blacklistadd-existing = {$username} is already on the blacklist! +cmd-blacklistadd-added = {$username} added to the blacklist +cmd-blacklistadd-not-found = Unable to find '{$username}' +cmd-blacklistadd-arg-player = [player] + +cmd-blacklistremove-desc = Removes the player with the given username from the server blacklist. +cmd-blacklistremove-help = Usage: blacklistremove <username> +cmd-blacklistremove-existing = {$username} is not on the blacklist! +cmd-blacklistremove-removed = {$username} removed from the blacklist +cmd-blacklistremove-not-found = Unable to find '{$username}' +cmd-blacklistremove-arg-player = [player] + +baby-jail-account-denied = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun! +baby-jail-account-denied-reason = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun! Reason: "{$reason}" +baby-jail-account-reason-account = Your Space Station 14 account is too old. It must be younger than {$hours} hours. +baby-jail-account-reason-overall = Your overall playtime on the server must be younger than {$hours} hours. diff --git a/Resources/Locale/en-US/deltav/access/components/agent-id-card-component.ftl b/Resources/Locale/en-US/deltav/access/components/agent-id-card-component.ftl new file mode 100644 index 00000000000..00b6312fbde --- /dev/null +++ b/Resources/Locale/en-US/deltav/access/components/agent-id-card-component.ftl @@ -0,0 +1 @@ +agent-id-card-current-number = NanoChat Number diff --git a/Resources/Locale/en-US/deltav/cargo/stocks-comapnies.ftl b/Resources/Locale/en-US/deltav/cargo/stocks-comapnies.ftl new file mode 100644 index 00000000000..996a538a7fc --- /dev/null +++ b/Resources/Locale/en-US/deltav/cargo/stocks-comapnies.ftl @@ -0,0 +1,7 @@ +# Company names used for stocks trading +stock-trading-company-nanotrasen = Nanotrasen [NT] +stock-trading-company-gorlex = Gorlex [GRX] +stock-trading-company-interdyne = Interdyne Pharmaceuticals [INTP] +stock-trading-company-fishinc = Fish Inc. [FIN] +stock-trading-company-donk = Donk Co. [DONK] +stock-trading-company-hydroco = HydroCo [HYD] \ No newline at end of file diff --git a/Resources/Locale/en-US/deltav/cargo/stocks-commands.ftl b/Resources/Locale/en-US/deltav/cargo/stocks-commands.ftl new file mode 100644 index 00000000000..8e0fe014999 --- /dev/null +++ b/Resources/Locale/en-US/deltav/cargo/stocks-commands.ftl @@ -0,0 +1,13 @@ +# changestockprice command +cmd-changestocksprice-desc = Changes a company's stock price to the specified number. +cmd-changestocksprice-help = changestockprice <Company index> <New price> [Station UID] +cmd-changestocksprice-invalid-company = Failed to execute command! Invalid company index or the new price exceeds the allowed limit. +cmd-changestocksprice-invalid-station = No stock market found for specified station +cmd-changestocksprice-no-stations = No stations with stock markets found + +# addstockscompany command +cmd-addstockscompany-desc = Adds a new company to the stocks market. +cmd-addstockscompany-help = addstockscompany <Display name> <Base price> [Station UID] +cmd-addstockscompany-failure = Failed to add company to the stock market. +cmd-addstockscompany-invalid-station = No stock market found for specified station +cmd-addstockscompany-no-stations = No stations with stock markets found diff --git a/Resources/Locale/en-US/deltav/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/deltav/cartridge-loader/cartridges.ftl index ede1a36b8ee..5c5a5c0a418 100644 --- a/Resources/Locale/en-US/deltav/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/deltav/cartridge-loader/cartridges.ftl @@ -1,3 +1,6 @@ +## CrimeAssist + +# General crime-assist-program-name = Crime Assist crime-assist-yes-button = Yes crime-assist-no-button = No @@ -6,6 +9,14 @@ crime-assist-crimetype-misdemeanour = Misdemeanour crime-assist-crimetype-felony = Felony crime-assist-crimetype-capital = Capital crime-assist-crime-innocent = No crime was committed +crime-assist-mainmenu = Welcome to Crime Assist! +crime-assist-sophont-explanation = A sophont is described as any entity with the capacity to display the following attributes: + • [bold]Sapience[/bold]: the entity possesses basic logic and problem-solving skills, or at a minimum some level of significant intelligence. + • [bold]Sentience[/bold]: the entity has the capacity to process an emotion or lack thereof, or at a minimum the ability to recognise its own pain. + • [bold]Self-awareness[/bold]: the entity is capable of altering its behaviour in a reasonable fashion as a result of stimuli, or at a minimum is capable of recognising its own sapience and sentience. + Any sophont is considered a legal person, regardless of origin or prior cognitive status. Much like any other intelligent organic, a sophont may press charges against crew and be tried for crimes. + +# Crimes crime-assist-crime-animalcruelty = Code 101: Animal Cruelty crime-assist-crime-theft = Code 102: Theft crime-assist-crime-trespass = Code 110: Trespass @@ -32,7 +43,8 @@ crime-assist-crime-decorporealisation = Code 305: Decorporealisation crime-assist-crime-kidnapping = Code 309: Kidnapping crime-assist-crime-sedition = Code 311: Sedition crime-assist-crime-sexualharassment = Code 314: Sexual Harassment -crime-assist-mainmenu = Welcome to Crime Assist! + +# Questions crime-assist-question-isitterrorism = Did the suspect hold hostages, cause many deaths or major destruction to force compliance from the crew? crime-assist-question-wassomeoneattacked = Was an entity attacked? crime-assist-question-wasitsophont = Was the victim in question a sophont? @@ -59,6 +71,8 @@ crime-assist-question-happenincourt = Was the suspect a nuisance in court? crime-assist-question-duringactiveinvestigation = Was the suspect a nuisance during an active investigation, and hindered the investigation as a result? crime-assist-question-tocommandstaff = Did the suspect overthrow or compromise a lawfully established Chain of Command, or attempt to do so? crime-assist-question-wasitcommanditself = Was a command staff or department head abusing authority over another sophont? + +# Crime details crime-assist-crimedetail-innocent = Crime could not be determined. Use your best judgement to resolve the situation. crime-assist-crimedetail-animalcruelty = To inflict unnecessary suffering on a sapient being with malicious intent. crime-assist-crimedetail-theft = To unlawfully take property or items without consent. @@ -86,6 +100,8 @@ crime-assist-crimedetail-decorporealisation = To unlawfully, maliciously, and pe crime-assist-crimedetail-kidnapping = To unlawfully confine or restrict the free movement of a sophont against their will. crime-assist-crimedetail-sedition = To act to overthrow a lawfully established Chain of Command or governing body without lawful or legitimate cause. crime-assist-crimedetail-sexualharassment = To sexually harass, attempt to coerce into sexual relations, or effect unwanted sexual contact with an unwilling sophont. + +# Punishments crime-assist-crimepunishment-innocent = No punishment may be necessary crime-assist-crimepunishment-animalcruelty = Punishment: 3 minutes crime-assist-crimepunishment-theft = Punishment: 2 minutes @@ -113,12 +129,10 @@ crime-assist-crimepunishment-decorporealisation = Punishment: Capital crime-assist-crimepunishment-kidnapping = Punishment: Capital crime-assist-crimepunishment-sedition = Punishment: Capital crime-assist-crimepunishment-sexualharassment = Punishment: Capital -crime-assist-sophont-explanation = A sophont is described as any entity with the capacity to display the following attributes: - • [bold]Sapience[/bold]: the entity possesses basic logic and problem-solving skills, or at a minimum some level of significant intelligence. - • [bold]Sentience[/bold]: the entity has the capacity to process an emotion or lack thereof, or at a minimum the ability to recognise its own pain. - • [bold]Self-awareness[/bold]: the entity is capable of altering its behaviour in a reasonable fashion as a result of stimuli, or at a minimum is capable of recognising its own sapience and sentience. - Any sophont is considered a legal person, regardless of origin or prior cognitive status. Much like any other intelligent organic, a sophont may press charges against crew and be tried for crimes. +## MailMetrics + +# General mail-metrics-program-name = MailMetrics mail-metrics-header = Income from Mail Deliveries mail-metrics-opened = Earnings (Opened) @@ -131,3 +145,60 @@ mail-metrics-money-header = Spesos mail-metrics-total = Total mail-metrics-progress = {$opened} out of {$total} packages opened! mail-metrics-progress-percent = Success rate: {$successRate}% + +## StockTrading + +# General +stock-trading-program-name = StockTrading +stock-trading-title = Intergalactic Stock Market +stock-trading-balance = Balance: {$balance} credits +stock-trading-no-entries = No entries +stock-trading-owned-shares = Owned: {$shares} +stock-trading-buy-button = Buy +stock-trading-sell-button = Sell +stock-trading-amount-placeholder = Amount +stock-trading-price-history = Price History + + +## NanoChat + +# General +nano-chat-program-name = NanoChat +nano-chat-title = NanoChat +nano-chat-new-chat = New Chat +nano-chat-contacts = CONTACTS +nano-chat-no-chats = No active chats +nano-chat-select-chat = Select a chat to begin +nano-chat-message-placeholder = Type a message... +nano-chat-send = Send +nano-chat-edit = Edit +nano-chat-delete = Delete +nano-chat-loading = Loading... +nano-chat-message-too-long = Message too long ({$current}/{$max} characters) +nano-chat-max-recipients = Maximum number of chats reached +nano-chat-new-message-title = Message from {$sender} +nano-chat-new-message-body = {$message} +nano-chat-toggle-mute = Mute notifications +nano-chat-delivery-failed = Failed to deliver + +# Create chat popup +nano-chat-new-title = Add a new chat +nano-chat-edit-title = Edit a contact +nano-chat-number-label = Number +nano-chat-name-label = Name +nano-chat-job-label = Job title +nano-chat-number-placeholder = Enter a number +nano-chat-name-placeholder = Enter a name +nano-chat-job-placeholder = Enter a job title (optional) +nano-chat-cancel = Cancel +nano-chat-create = Create + +# LogProbe additions +log-probe-scan-nanochat = Scanned {$card}'s NanoChat logs +log-probe-header-access = Access Log Scanner +log-probe-header-nanochat = NanoChat Log Scanner +log-probe-label-message = Message +log-probe-card-number = Card: {$number} +log-probe-recipients = {$count} Recipients +log-probe-recipient-list = Known Recipients: +log-probe-message-format = {$sender} → {$recipient}: {$content} diff --git a/Resources/Locale/en-US/deltav/job/captain-state.ftl b/Resources/Locale/en-US/deltav/job/captain-state.ftl new file mode 100644 index 00000000000..bbae626d7e5 --- /dev/null +++ b/Resources/Locale/en-US/deltav/job/captain-state.ftl @@ -0,0 +1,6 @@ +# Announcements related to captain presence and ACO state + +captain-arrived-revoke-aco-announcement = The Acting Commanding Officer's position is revoked due to the arrival of a NanoTrasen-appointed captain. All personnel are to return to the standard Chain of Command. +no-captain-request-aco-vote-with-aa-announcement = Station records indicate that no captain is currently present. Command personnel are requested to nominate an Acting Commanding Officer and report the results to Central Command in accordance with Standard Operating Procedure. Emergency AA will be unlocked in {$minutes} minutes to ensure continued operational efficiency. +no-captain-request-aco-vote-announcement = Station records indicate that no captain is currently present. Command personnel are requested to nominate an Acting Commanding Officer and report the results to Central Command in accordance with Standard Operating Procedure. +no-captain-aa-unlocked-announcement = Command access authority has been granted to the Spare ID cabinet for use by the Acting Commanding Officer. Unauthorized possession of Emergency AA is punishable under Felony Offense [202]: Grand Theft. diff --git a/Resources/Locale/en-US/deltav/job/job-description.ftl b/Resources/Locale/en-US/deltav/job/job-description.ftl index 9e99f5838e9..c35f56b3274 100644 --- a/Resources/Locale/en-US/deltav/job/job-description.ftl +++ b/Resources/Locale/en-US/deltav/job/job-description.ftl @@ -3,3 +3,4 @@ job-description-chief-justice = Manage the justice department, act as a judge, a job-description-clerk = Organize trials, notarize documents, review charges, and act as a judge if needed. job-description-prosecutor = Take statements from security and prepare cases against those accused of commiting crimes. job-description-courier = Deliver mail and other packages from and to logistics. Avoid dogs. +job-description-admin-assistant = Assist command in their day-to-day activities, grab the captain a coffee, answer faxes in the bridge. diff --git a/Resources/Locale/en-US/deltav/job/job-names.ftl b/Resources/Locale/en-US/deltav/job/job-names.ftl index 175da8ba693..8f257f6a4dc 100644 --- a/Resources/Locale/en-US/deltav/job/job-names.ftl +++ b/Resources/Locale/en-US/deltav/job/job-names.ftl @@ -4,3 +4,4 @@ job-name-clerk = Clerk job-name-prosecutor = Prosecutor job-name-lawyer = Attorney job-name-courier = Courier +job-name-admin-assistant = Administrative Assistant \ No newline at end of file diff --git a/Resources/Locale/en-US/deltav/job/job-supervisors.ftl b/Resources/Locale/en-US/deltav/job/job-supervisors.ftl index f1c0ade32a5..d911839af25 100644 --- a/Resources/Locale/en-US/deltav/job/job-supervisors.ftl +++ b/Resources/Locale/en-US/deltav/job/job-supervisors.ftl @@ -1,2 +1,2 @@ -job-supervisors-cj = the chief justice - +job-supervisors-cj = the Chief Justice +job-supervisors-command = all command staff diff --git a/Resources/Locale/en-US/deltav/lathe/ui/lathe-menu.ftl b/Resources/Locale/en-US/deltav/lathe/ui/lathe-menu.ftl new file mode 100644 index 00000000000..9f43b363f13 --- /dev/null +++ b/Resources/Locale/en-US/deltav/lathe/ui/lathe-menu.ftl @@ -0,0 +1,2 @@ +lathe-menu-mining-points = Mining Points: {$points} +lathe-menu-mining-points-claim-button = Claim Points diff --git a/Resources/Locale/en-US/deltav/nanochat/components/nanochat-card-component.ftl b/Resources/Locale/en-US/deltav/nanochat/components/nanochat-card-component.ftl new file mode 100644 index 00000000000..d04c3066d5d --- /dev/null +++ b/Resources/Locale/en-US/deltav/nanochat/components/nanochat-card-component.ftl @@ -0,0 +1,7 @@ +# Examine +nanochat-card-examine-no-number = The NanoChat card has not been assigned a number yet. +nanochat-card-examine-number = The NanoChat card displays #{$number}. + +# Microwave interactions +nanochat-card-microwave-erased = The {$card} emits a soft beep as all its message history vanishes into the ether! +nanochat-card-microwave-scrambled = The {$card} crackles as its messages become scrambled! diff --git a/Resources/Locale/en-US/deltav/paper/stamp-component.ftl b/Resources/Locale/en-US/deltav/paper/stamp-component.ftl index cfa5279baa0..37a38d69ca5 100644 --- a/Resources/Locale/en-US/deltav/paper/stamp-component.ftl +++ b/Resources/Locale/en-US/deltav/paper/stamp-component.ftl @@ -2,4 +2,4 @@ stamp-component-stamped-name-lawyer = Lawyer stamp-component-stamped-name-psychologist = Psychologist stamp-component-stamped-name-notary = NOTARY stamp-component-stamped-name-chiefjustice = Chief Justice - +stamp-component-stamped-name-admin-assistant = Administrative Assistant diff --git a/Resources/Locale/en-US/deltav/vending-machines/shop-vendor.ftl b/Resources/Locale/en-US/deltav/vending-machines/shop-vendor.ftl new file mode 100644 index 00000000000..361d1dc71cb --- /dev/null +++ b/Resources/Locale/en-US/deltav/vending-machines/shop-vendor.ftl @@ -0,0 +1,4 @@ +shop-vendor-balance = Balance: {$points} +shop-vendor-stack-suffix = x{$count} +shop-vendor-flavor-left = All payments are secure +shop-vendor-flavor-right = v1.2 diff --git a/Resources/Locale/en-US/execution/execution.ftl b/Resources/Locale/en-US/execution/execution.ftl index 8bdf3261666..08f9a06dd48 100644 --- a/Resources/Locale/en-US/execution/execution.ftl +++ b/Resources/Locale/en-US/execution/execution.ftl @@ -6,25 +6,12 @@ execution-verb-message = Use your weapon to execute someone. # victim (the person being executed) # weapon (the weapon used for the execution) -execution-popup-gun-initial-internal = You ready the muzzle of {THE($weapon)} against {$victim}'s head. -execution-popup-gun-initial-external = {$attacker} readies the muzzle of {THE($weapon)} against {$victim}'s head. -execution-popup-gun-complete-internal = You blast {$victim} in the head! -execution-popup-gun-complete-external = {$attacker} blasts {$victim} in the head! -execution-popup-gun-clumsy-internal = You miss {$victim}'s head and shoot your foot instead! -execution-popup-gun-clumsy-external = {$attacker} misses {$victim} and shoots {POSS-ADJ($attacker)} foot instead! -execution-popup-gun-empty = {CAPITALIZE(THE($weapon))} clicks. - -suicide-popup-gun-initial-internal = You place the muzzle of {THE($weapon)} in your mouth. -suicide-popup-gun-initial-external = {$attacker} places the muzzle of {THE($weapon)} in {POSS-ADJ($attacker)} mouth. -suicide-popup-gun-complete-internal = You shoot yourself in the head! -suicide-popup-gun-complete-external = {$attacker} shoots {REFLEXIVE($attacker)} in the head! - execution-popup-melee-initial-internal = You ready {THE($weapon)} against {$victim}'s throat. execution-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against the throat of {$victim}. execution-popup-melee-complete-internal = You slit the throat of {$victim}! execution-popup-melee-complete-external = {$attacker} slits the throat of {$victim}! -suicide-popup-melee-initial-internal = You ready {THE($weapon)} against your throat. -suicide-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against {POSS-ADJ($attacker)} throat. -suicide-popup-melee-complete-internal = You slit your throat with {THE($weapon)}! -suicide-popup-melee-complete-external = {$attacker} slits {POSS-ADJ($attacker)} throat with {THE($weapon)}! \ No newline at end of file +execution-popup-self-initial-internal = You ready {THE($weapon)} against your own throat. +execution-popup-self-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against their own throat. +execution-popup-self-complete-internal = You slit your own throat! +execution-popup-self-complete-external = {$attacker} slits their own throat! \ No newline at end of file diff --git a/Resources/Locale/en-US/flavors/flavor-profiles.ftl b/Resources/Locale/en-US/flavors/flavor-profiles.ftl index 39b185bd02f..543e9be50d0 100644 --- a/Resources/Locale/en-US/flavors/flavor-profiles.ftl +++ b/Resources/Locale/en-US/flavors/flavor-profiles.ftl @@ -123,6 +123,7 @@ flavor-complex-meatballs = like meatballs flavor-complex-nettles = like nettles flavor-complex-jungle = like jungle flavor-complex-vegetables = like vegetables +flavor-complex-cherry = like cherries # use it when there multiple types of veggies ## Complex foodstuffs (cooked foods, joke flavors, etc) @@ -171,6 +172,11 @@ flavor-complex-violets = like violets flavor-complex-pyrotton = like a burning mouth flavor-complex-mothballs = like mothballs flavor-complex-paint-thinner = like paint thinner +flavor-complex-numbing-tranquility = like numbing tranquility +flavor-complex-true-nature = like the true nature of reality +flavor-complex-false-meat = not entirely unlike meat +flavor-complex-paper = like mushy pulp +flavor-complex-compressed-meat = like compressed meat # Drink-specific flavors. diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl index e92676a2160..c32718d6a8b 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl @@ -26,7 +26,7 @@ traitor-death-match-end-round-description-entry = {$originalName}'s PDA, with {$ traitor-role-greeting = You are an agent sent by {$corporation} on behalf of The Syndicate. Your objectives and codewords are listed in the character menu. - Use the uplink loaded into your PDA to buy the tools you'll need for this mission. + Use your uplink to buy the tools you'll need for this mission. Death to Nanotrasen! traitor-role-codewords = The codewords are: @@ -36,9 +36,13 @@ traitor-role-codewords = traitor-role-uplink-code = Set your ringtone to the notes {$code} to lock or unlock your uplink. Remember to lock it after, or the stations crew will easily open it too! +traitor-role-uplink-implant = + Your uplink implant has been activated, access it from your hotbar. + The uplink is secure unless someone removes it from your body. # don't need all the flavour text for character menu traitor-role-codewords-short = The codewords are: {$codewords}. traitor-role-uplink-code-short = Your uplink code is {$code}. Set it as your PDA ringtone to access uplink. +traitor-role-uplink-implant-short = Your uplink was implanted. Access it from your hotbar. diff --git a/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl b/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl index 4e6ec1f1886..1d7742690d1 100644 --- a/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl +++ b/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl @@ -10,6 +10,15 @@ ghost-role-information-mouse-description = A hungry and mischievous mouse. ghost-role-information-mothroach-name = Mothroach ghost-role-information-mothroach-description = A cute but mischievous mothroach. +ghost-role-information-snail-name = Snail +ghost-role-information-snail-description = A little snail who doesn't mind a bit of space. Just stay on grid! + +ghost-role-information-snailspeed-name = Snail +ghost-role-information-snailspeed-description = A little snail with snailborn thrusters. + +ghost-role-information-snoth-name = Snoth +ghost-role-information-snoth-description = A little snoth who doesn't mind a bit of space. Just stay on grid! + ghost-role-information-giant-spider-name = Giant spider ghost-role-information-giant-spider-description = This station's inhabitants look mighty tasty, and your sticky web is perfect to catch them! diff --git a/Resources/Locale/en-US/glue/glue.ftl b/Resources/Locale/en-US/glue/glue.ftl index 1a711d51c21..158ebc9ed85 100644 --- a/Resources/Locale/en-US/glue/glue.ftl +++ b/Resources/Locale/en-US/glue/glue.ftl @@ -1,5 +1,5 @@ glue-success = {THE($target)} has been covered in glue! -glued-name-prefix = Glued {$target} +glued-name-prefix = glued {$baseName} glue-failure = Can't cover {THE($target)} in glue! glue-verb-text = Apply Glue glue-verb-message = Glue an object diff --git a/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl b/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl index 6cbfc13a797..95aaf9126d7 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl @@ -28,6 +28,9 @@ reagent-effect-condition-guidebook-reagent-threshold = reagent-effect-condition-guidebook-mob-state-condition = the mob is { $state } +reagent-effect-condition-guidebook-job-condition = + the target's job is { $job } + reagent-effect-condition-guidebook-solution-temperature = the solution's temperature is { $max -> [2147483648] at least {NATURALFIXED($min, 2)}k diff --git a/Resources/Locale/en-US/guidebook/chemistry/statuseffects.ftl b/Resources/Locale/en-US/guidebook/chemistry/statuseffects.ftl index 3db8a3c5b0e..9a8f2f6c8a3 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/statuseffects.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/statuseffects.ftl @@ -11,3 +11,5 @@ reagent-effect-status-effect-PressureImmunity = pressure immunity reagent-effect-status-effect-Pacified = combat pacification reagent-effect-status-effect-RatvarianLanguage = ratvarian language patterns reagent-effect-status-effect-StaminaModifier = modified stamina +reagent-effect-status-effect-RadiationProtection = radiation protection +reagent-effect-status-effect-Drowsiness = drowsiness diff --git a/Resources/Locale/en-US/info/ban.ftl b/Resources/Locale/en-US/info/ban.ftl index 463dd1f5669..cc3a34c28b7 100644 --- a/Resources/Locale/en-US/info/ban.ftl +++ b/Resources/Locale/en-US/info/ban.ftl @@ -82,3 +82,6 @@ ban-panel-erase = Erase chat messages and player from round server-ban-string = {$admin} created a {$severity} severity server ban that expires {$expires} for [{$name}, {$ip}, {$hwid}], with reason: {$reason} server-ban-string-no-pii = {$admin} created a {$severity} severity server ban that expires {$expires} for {$name} with reason: {$reason} server-ban-string-never = never + +# Kick on ban +ban-kick-reason = You have been banned diff --git a/Resources/Locale/en-US/info/playerpanel.ftl b/Resources/Locale/en-US/info/playerpanel.ftl new file mode 100644 index 00000000000..138939c48c9 --- /dev/null +++ b/Resources/Locale/en-US/info/playerpanel.ftl @@ -0,0 +1,7 @@ +cmd-playerpanel-desc = Displays general information and actions for a player +cmd-playerpanel-help = Usage: playerpanel <name or user ID> + +cmd-playerpanel-server = This command cannot be run from the server +cmd-playerpanel-invalid-arguments = Invalid amount of arguments +cmd-playerpanel-invalid-player = Player not found +cmd-playerpanel-completion = <PlayerIndex> diff --git a/Resources/Locale/en-US/job/job-names.ftl b/Resources/Locale/en-US/job/job-names.ftl index 2e66ec56c5a..39d7ab86add 100644 --- a/Resources/Locale/en-US/job/job-names.ftl +++ b/Resources/Locale/en-US/job/job-names.ftl @@ -52,6 +52,22 @@ job-name-boxer = Boxer job-name-zookeeper = Zookeeper job-name-visitor = Visitor +# unused jobs +# these are required for the agent ID job icon tooltips +# I am keeping them for roleplaying opportunities +job-name-geneticist = Geneticist +job-name-no-id = No ID +job-name-prisoner = Prisoner +job-name-syndicate = Syndicate +job-name-unknown = Unknown +job-name-virologist = Virologist +job-name-zombie = Zombie + +# Job titles +job-title-visitor = Visitor +job-title-cluwne = Cluwne +job-title-universal = Universal + # Role timers - Make these alphabetical or I cut you JobAtmosphericTechnician = Atmospheric Technician JobBartender = Bartender diff --git a/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl b/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl index ff28cc44db6..4a83cd455d7 100644 --- a/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl +++ b/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl @@ -1,4 +1,4 @@ -butcherable-different-tool = You are going to need a different tool to butcher { THE($target) }. +butcherable-different-tool = You need a different tool to butcher { THE($target) }. butcherable-knife-butchered-success = You butcher { THE($target) } with { THE($knife) }. butcherable-need-knife = Use a sharp object to butcher { THE($target) }. butcherable-not-in-container = { CAPITALIZE(THE($target)) } can't be in a container. diff --git a/Resources/Locale/en-US/label/label-component.ftl b/Resources/Locale/en-US/label/label-component.ftl new file mode 100644 index 00000000000..ff3a250c7b8 --- /dev/null +++ b/Resources/Locale/en-US/label/label-component.ftl @@ -0,0 +1 @@ +comp-label-format = {$baseName} ({$label}) diff --git a/Resources/Locale/en-US/lathe/recipes.ftl b/Resources/Locale/en-US/lathe/recipes.ftl new file mode 100644 index 00000000000..99186c5b659 --- /dev/null +++ b/Resources/Locale/en-US/lathe/recipes.ftl @@ -0,0 +1,8 @@ +lathe-recipe-Medkit-name = first aid kit (empty) +lathe-recipe-MedkitBurn-name = burn treatment kit (empty) +lathe-recipe-MedkitToxin-name = toxin treatment kit (empty) +lathe-recipe-MedkitO2-name = oxygen deprivation treatment kit (empty) +lathe-recipe-MedkitBrute-name = brute trauma treatment kit (empty) +lathe-recipe-MedkitAdvanced-name = advanced first aid kit (empty) +lathe-recipe-MedkitRadiation-name = radiation treatment kit (empty) +lathe-recipe-MedkitCombat-name = combat medical kit (empty) diff --git a/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl b/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl index 71dd50d4091..16b67022ad5 100644 --- a/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl +++ b/Resources/Locale/en-US/lathe/ui/lathe-menu.ftl @@ -6,6 +6,9 @@ lathe-menu-search-designs = Search designs lathe-menu-category-all = All lathe-menu-search-filter = Filter: lathe-menu-amount = Amount: +lathe-menu-reagent-slot-examine = It has a slot for a beaker on the side. +lathe-reagent-dispense-no-container = Liquid pours out of {THE($name)} onto the floor! +lathe-menu-result-reagent-display = {$reagent} ({$amount}u) lathe-menu-material-display = {$material} ({$amount}) lathe-menu-tooltip-display = {$amount} of {$material} lathe-menu-description-display = [italic]{$description}[/italic] diff --git a/Resources/Locale/en-US/lube/lube.ftl b/Resources/Locale/en-US/lube/lube.ftl index 92dd2802ec7..1b5b66e069f 100644 --- a/Resources/Locale/en-US/lube/lube.ftl +++ b/Resources/Locale/en-US/lube/lube.ftl @@ -1,5 +1,5 @@ lube-success = {THE($target)} has been covered in lube! -lubed-name-prefix = Lubed {$target} +lubed-name-prefix = lubed {$baseName} lube-failure = Can't cover {THE($target)} in lube! lube-slip = {THE($target)} slips out of your hands! lube-verb-text = Apply Lube diff --git a/Resources/Locale/en-US/materials/materials.ftl b/Resources/Locale/en-US/materials/materials.ftl index b213e7d1e9a..73a731de4a3 100644 --- a/Resources/Locale/en-US/materials/materials.ftl +++ b/Resources/Locale/en-US/materials/materials.ftl @@ -25,6 +25,8 @@ materials-meat = meat materials-web = silk materials-bones = bone materials-coal = coal +materials-diamond = diamond +materials-gunpowder = gunpowder materials-bluespace = bluespace materials-normality = normality diff --git a/Resources/Locale/en-US/navmap-beacons/station-beacons.ftl b/Resources/Locale/en-US/navmap-beacons/station-beacons.ftl index 6434311f21f..3b0ce53e8aa 100644 --- a/Resources/Locale/en-US/navmap-beacons/station-beacons.ftl +++ b/Resources/Locale/en-US/navmap-beacons/station-beacons.ftl @@ -50,6 +50,7 @@ station-beacon-telecoms = Telecoms station-beacon-atmos = Atmos station-beacon-teg = TEG station-beacon-tech-vault = Tech Vault +station-beacon-anchor = Anchor station-beacon-service = Service station-beacon-kitchen = Kitchen diff --git a/Resources/Locale/en-US/navmap-beacons/station_map.ftl b/Resources/Locale/en-US/navmap-beacons/station_map.ftl index 1563e0abaf2..e2528515566 100644 --- a/Resources/Locale/en-US/navmap-beacons/station_map.ftl +++ b/Resources/Locale/en-US/navmap-beacons/station_map.ftl @@ -1,6 +1,7 @@ station-map-window-title = Station map station-map-user-interface-flavor-left = Don't panic station-map-user-interface-flavor-right = v1.42 +station-map-filter-placeholder = Search by name nav-beacon-window-title = Station Beacon nav-beacon-toggle-visible = Visible diff --git a/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl b/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl index 6ca108b653a..cf7bf2d03ac 100644 --- a/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl +++ b/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl @@ -1,3 +1,3 @@ -infant-name-prefix = baby {$name} +infant-name-prefix = baby {$baseName} reproductive-birth-popup = {CAPITALIZE(THE($parent))} gave birth! reproductive-laid-egg-popup = {CAPITALIZE(THE($parent))} lays an egg! diff --git a/Resources/Locale/en-US/nutrition/components/food-sequence.ftl b/Resources/Locale/en-US/nutrition/components/food-sequence.ftl new file mode 100644 index 00000000000..336d3d75fc6 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/food-sequence.ftl @@ -0,0 +1,139 @@ +food-sequence-no-space = You can't put any more! + +food-sequence-standart-gen = {$prefix}{$content}{$suffix} + +food-sequence-burger-suffix = burger +# GENERAL + +food-sequence-content-chicken = chicken +food-sequence-content-duck = duck +food-sequence-content-crab = crab +food-sequence-content-dragon = dragon +food-sequence-content-snake = snake +food-sequence-content-xeno = xeno +food-sequence-content-rouny = rouny +food-sequence-content-tomato = tomato +food-sequence-content-salami = salami +food-sequence-content-slime = slime +food-sequence-content-clown = clown +food-sequence-content-pea = pea +food-sequence-content-bungo = bungo +food-sequence-content-banana = banana +food-sequence-content-mimana = mimana +food-sequence-content-lemon = lemon +food-sequence-content-lemoon = lemoon +food-sequence-content-lime = lime +food-sequence-content-orange = orange +food-sequence-content-potato = potato +food-sequence-content-apple = apple +food-sequence-content-cocoa = cocoa +food-sequence-content-corn = corn +food-sequence-content-chili = chili +food-sequence-content-chilly = chilly +food-sequence-content-mushroom = shrooms +food-sequence-content-aloe = aloe +food-sequence-content-poppy = poppy +food-sequence-content-lily = lily +food-sequence-content-soy = soy +food-sequence-content-cheese = cheese +food-sequence-content-chevre = chèvre +food-sequence-content-tofu = tofu +food-sequence-content-brain = brain +food-sequence-content-tongue = tongue +food-sequence-content-ears = ear +food-sequence-content-stomach = stomach +food-sequence-content-liver = liver +food-sequence-content-clump = clump +food-sequence-content-raw-meat = raw meat +food-sequence-content-meat = meat +food-sequence-content-carp = carp meat +food-sequence-content-bear = bear meat +food-sequence-content-penguin = penguin meat +food-sequence-content-corgi = corgi meat +food-sequence-content-goliath = goliath meat +food-sequence-content-rat = rat +food-sequence-content-lizard = lizard meat +food-sequence-content-plant = plant meat +food-sequence-content-rotten = rotten meat +food-sequence-content-spider = spider legs +food-sequence-content-carrot = carrot +food-sequence-content-cabbage = cabbage +food-sequence-content-garlic = garlic +food-sequence-content-pineapple = pineapple +food-sequence-content-onion = onion +food-sequence-content-ambrosia = ambrosia +food-sequence-content-galaxy = galaxythistle +food-sequence-content-glasstle = glasstle +food-sequence-content-gatfruit = gatfruit +food-sequence-content-koibean = koibean +food-sequence-content-watermelon = watermelon +food-sequence-content-holymelon = holymelon +food-sequence-content-cannabis = cannabis +food-sequence-content-rainbow-cannabis = rainbow cannabis +food-sequence-content-tobacco = tobacco +food-sequence-content-hamster = hamster +food-sequence-content-suppermatter = suppermatter +food-sequence-content-capfruit = capfruit +food-sequence-content-berries = berries +food-sequence-content-spacemans-trumpet = spacemans trupmet +food-sequence-content-cherry = cherry +food-sequence-content-snail = snail + +# BURGERS + +food-sequence-burger-gen = {$content}burger + +food-sequence-burger-content-raw-meat = raw +food-sequence-burger-content-meat = meaty +food-sequence-burger-content-carp = carpo +food-sequence-burger-content-bear = bear +food-sequence-burger-content-crab = crabs +food-sequence-burger-content-penguin = peng +food-sequence-burger-content-corgi = corgi +food-sequence-burger-content-goliath = goli +food-sequence-burger-content-rat = rat +food-sequence-burger-content-lizard = lizzy +food-sequence-burger-content-plant = plant +food-sequence-burger-content-rotten = trash +food-sequence-burger-content-spider = web +food-sequence-burger-content-carrot = carro +food-sequence-burger-content-cabbage = cabba +food-sequence-burger-content-garlic = garli +food-sequence-burger-content-pineapple = pine +food-sequence-burger-content-onion = oni +food-sequence-burger-content-ambrosia = ambro +food-sequence-burger-content-galaxy = galaxy +food-sequence-burger-content-glasstle = glass +food-sequence-burger-content-gatfruit = gat +food-sequence-burger-content-capfruit = cap +food-sequence-burger-content-rice = rice +food-sequence-burger-content-soy = soy +food-sequence-burger-content-koibean = koi +food-sequence-burger-content-watermelon = water +food-sequence-burger-content-holymelon = holy +food-sequence-burger-content-cannabis = funny +food-sequence-burger-content-rainbow-cannabis = FUNNY +food-sequence-burger-content-tobacco = tobaco + +food-sequence-burger-content-cheese = cheese + +food-sequence-burger-content-brain = brain +food-sequence-burger-content-tongue = tongue +food-sequence-burger-content-appendix = appendi +food-sequence-burger-content-ears = ear +food-sequence-burger-content-stomach = stomach +food-sequence-burger-content-liver = liver +food-sequence-burger-content-suppermatter = supper +food-sequence-burger-content-hamster = hams +food-sequence-burger-content-berries = berri +food-sequence-burger-content-spacemans-trumpet = spacetrump +food-sequence-burger-content-extradimensional-orange = 3d +food-sequence-burger-content-world-pea = peace + +# TACO + +food-sequence-taco-gen = taco with {$content} + +# SKEWER + +food-sequence-skewer-gen = {$content} kebab diff --git a/Resources/Locale/en-US/nyanotrasen/job/job-names.ftl b/Resources/Locale/en-US/nyanotrasen/job/job-names.ftl index 8660d214615..429be6e49f0 100644 --- a/Resources/Locale/en-US/nyanotrasen/job/job-names.ftl +++ b/Resources/Locale/en-US/nyanotrasen/job/job-names.ftl @@ -2,7 +2,6 @@ job-name-gladiator = Gladiator job-name-guard = Prison Guard job-name-mail-carrier = Courier job-name-martialartist = Martial Artist -job-name-prisoner = Prisoner job-name-mantis = Mantis # Role timers diff --git a/Resources/Locale/en-US/objectives/round-end.ftl b/Resources/Locale/en-US/objectives/round-end.ftl index b4314b2caff..3da81fc9640 100644 --- a/Resources/Locale/en-US/objectives/round-end.ftl +++ b/Resources/Locale/en-US/objectives/round-end.ftl @@ -6,7 +6,6 @@ objectives-round-end-result = {$count -> objectives-round-end-result-in-custody = {$custody} out of {$count} {MAKEPLURAL($agent)} were in custody. objectives-player-user-named = [color=White]{$name}[/color] ([color=gray]{$user}[/color]) -objectives-player-user = [color=gray]{$user}[/color] objectives-player-named = [color=White]{$name}[/color] objectives-no-objectives = {$custody}{$title} was a {$agent}. diff --git a/Resources/Locale/en-US/paper/paper-misc.ftl b/Resources/Locale/en-US/paper/paper-misc.ftl index 587701a9274..c7c3a5e42ee 100644 --- a/Resources/Locale/en-US/paper/paper-misc.ftl +++ b/Resources/Locale/en-US/paper/paper-misc.ftl @@ -26,3 +26,21 @@ book-text-ame-scribbles = I don't know if you're trained already, so I hope this The golden rule is 2 injection for every 1 core. You can go lower to save fuel. Higher will burn the engine out and eventually make it explode. Don't. Don't forget to refuel it, it tends to stop at the worst possible time. + +book-text-combat-bakery-kit = Thank you for choosing our combat bakery kit! + Enclosed are two (2) CyberSun patented Throwing Croissants, and one (1) patent-pending Baguette Sword. + The included Donk Co. microwave board can construct a microwave capable of baking more weapons. + Just like the baked weapons, be sure to eat this note after use. Good luck, agent. + + Baguette Sword Recipe: + Dough x 1 + Salt 5u + Pepper 5u + Metal Rod x 1 + Cook Time: 15 seconds + + Throwing Croissant Recipe: + Raw Croissant x 1 + Butter Slice x 1 + Glass Shard x 1 + Cook Time: 5 seconds \ No newline at end of file diff --git a/Resources/Locale/en-US/power/components/power-charging-component.ftl b/Resources/Locale/en-US/power/components/power-charging-component.ftl new file mode 100644 index 00000000000..b4743bd1022 --- /dev/null +++ b/Resources/Locale/en-US/power/components/power-charging-component.ftl @@ -0,0 +1,22 @@ +## UI field names + +power-charge-window-status = Status: +power-charge-window-power = Power: +power-charge-window-eta = ETA: +power-charge-window-charge = Charge: + +## UI statuses +power-charge-window-status-fully-charged = Fully Charged +power-charge-window-status-off = Off +power-charge-window-status-charging = Charging +power-charge-window-status-discharging = Discharging + +## UI Power Buttons +power-charge-window-power-on = On +power-charge-window-power-off = Off +power-charge-window-power-label = { $draw } / { $max } W + +## UI ETA label + +power-charge-window-eta-none = N/A +power-charge-window-eta-value = { TOSTRING($left, "m\\:ss") } diff --git a/Resources/Locale/en-US/prototypes/roles/antags.ftl b/Resources/Locale/en-US/prototypes/roles/antags.ftl index 5c514dba8ec..1e057738e37 100644 --- a/Resources/Locale/en-US/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/prototypes/roles/antags.ftl @@ -1,6 +1,9 @@ roles-antag-syndicate-agent-name = Syndicate agent roles-antag-syndicate-agent-objective = Complete your objectives without being caught. +roles-antag-syndicate-agent-sleeper-name = Syndicate sleeper agent +roles-antag-syndicate-agent-sleeper-objective = A form of syndicate agent that can activate at any point in the middle of the shift. + roles-antag-initial-infected-name = Initial Infected roles-antag-initial-infected-objective = Once you turn, infect as many other crew members as possible. diff --git a/Resources/Locale/en-US/radio/components/intercom.ftl b/Resources/Locale/en-US/radio/components/intercom.ftl index e56e3cd0f73..63303999c21 100644 --- a/Resources/Locale/en-US/radio/components/intercom.ftl +++ b/Resources/Locale/en-US/radio/components/intercom.ftl @@ -1,5 +1,6 @@ intercom-menu-title = Intercom intercom-channel-label = Channel: intercom-button-text-mic = Mic. -intercom-button-text-speaker = Speak +intercom-button-text-speaker = Spkr. +intercom-options-none = No channels intercom-flavor-text-left = Keep lines free of chatter diff --git a/Resources/Locale/en-US/reagents/meta/consumable/drink/juice.ftl b/Resources/Locale/en-US/reagents/meta/consumable/drink/juice.ftl index 3f968421d06..810f3d12f51 100644 --- a/Resources/Locale/en-US/reagents/meta/consumable/drink/juice.ftl +++ b/Resources/Locale/en-US/reagents/meta/consumable/drink/juice.ftl @@ -36,3 +36,6 @@ reagent-desc-juice-tomato = Tomatoes made into juice. What a waste of good tomat reagent-name-juice-watermelon = water melon juice reagent-desc-juice-watermelon = The delicious juice of a watermelon. + +reagent-name-juice-cherry = cherry juice +reagent-desc-juice-cherry = Tasty cherry juice, sweet and tangy. diff --git a/Resources/Locale/en-US/reagents/meta/medicine.ftl b/Resources/Locale/en-US/reagents/meta/medicine.ftl index a84e8315fda..1e34411efbb 100644 --- a/Resources/Locale/en-US/reagents/meta/medicine.ftl +++ b/Resources/Locale/en-US/reagents/meta/medicine.ftl @@ -141,3 +141,9 @@ reagent-desc-mannitol = Efficiently restores brain damage. reagent-name-psicodine = psicodine reagent-desc-psicodine = Suppresses anxiety and other various forms of mental distress. Overdose causes hallucinations and minor toxin damage. + +reagent-name-potassium-iodide = potassium iodide +reagent-desc-potassium-iodide = Will reduce the damaging effects of radiation by 90%. Prophylactic use only. + +reagent-name-haloperidol = haloperidol +reagent-desc-haloperidol = Removes most stimulating and hallucinogenic drugs. Reduces druggy effects and jitteriness. Causes drowsiness. diff --git a/Resources/Locale/en-US/research/components/research-console-component.ftl b/Resources/Locale/en-US/research/components/research-console-component.ftl index 33070480e54..5a1e074f4d4 100644 --- a/Resources/Locale/en-US/research/components/research-console-component.ftl +++ b/Resources/Locale/en-US/research/components/research-console-component.ftl @@ -18,4 +18,4 @@ research-console-prereqs-list-start = Requires: research-console-prereqs-list-entry = - [color=orchid]{$text}[/color] research-console-no-access-popup = No access! -research-console-unlock-technology-radio-broadcast = Unlocked [bold]{$technology}[/bold] for [bold]{$amount}[/bold] research. +research-console-unlock-technology-radio-broadcast = Unlocked [bold]{$technology}[/bold] for [bold]{$amount}[/bold] research by [bold]{$approver}[/bold]. diff --git a/Resources/Locale/en-US/research/components/robotics-console.ftl b/Resources/Locale/en-US/research/components/robotics-console.ftl index 978fa9a43c0..a4c82bd0322 100644 --- a/Resources/Locale/en-US/research/components/robotics-console.ftl +++ b/Resources/Locale/en-US/research/components/robotics-console.ftl @@ -16,4 +16,4 @@ robotics-console-locked-message = Controls locked, swipe ID. robotics-console-disable = Disable robotics-console-destroy = Destroy -robotics-console-cyborg-destroyed = The cyborg {$name} has been remotely destroyed. +robotics-console-cyborg-destroying = {$name} is being remotely detonated! diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl index fe7293d8481..26cd4f83155 100644 --- a/Resources/Locale/en-US/research/technologies.ftl +++ b/Resources/Locale/en-US/research/technologies.ftl @@ -21,6 +21,7 @@ research-technology-super-powercells = Super Powercells research-technology-bluespace-storage = Bluespace Storage research-technology-portable-fission = Portable Fission research-technology-space-scanning = Space Scanning +research-technology-excavation = Mass Excavation research-technology-salvage-weapons = Salvage Weapons research-technology-draconic-munitions = Draconic Munitions @@ -45,7 +46,6 @@ research-technology-magnets-tech = Localized Magnetism research-technology-advanced-parts = Advanced Parts research-technology-advanced-bluespace = Advanced Bluespace Research research-technology-anomaly-harnessing = Anomaly Core Harnessing -research-technology-grappling = Grappling research-technology-abnormal-artifact-manipulation = Artifact Recycling research-technology-gravity-manipulation = Gravity Manipulation research-technology-quantum-leaping = Quantum Leaping diff --git a/Resources/Locale/en-US/salvage/salvage-magnet.ftl b/Resources/Locale/en-US/salvage/salvage-magnet.ftl index 027afffef2f..48bd321656a 100644 --- a/Resources/Locale/en-US/salvage/salvage-magnet.ftl +++ b/Resources/Locale/en-US/salvage/salvage-magnet.ftl @@ -13,6 +13,7 @@ salvage-magnet-resources = {$resource -> [OreQuartz] Quartz [OreSalt] Salt [OreGold] Gold + [OreDiamond] Diamond [OreSilver] Silver [OrePlasma] Plasma [OreUranium] Uranium diff --git a/Resources/Locale/en-US/sandbox/sandbox-manager.ftl b/Resources/Locale/en-US/sandbox/sandbox-manager.ftl index b7f4d03451f..b6f97367323 100644 --- a/Resources/Locale/en-US/sandbox/sandbox-manager.ftl +++ b/Resources/Locale/en-US/sandbox/sandbox-manager.ftl @@ -1,4 +1,5 @@ sandbox-window-title = Sandbox Panel +sandbox-window-ai-overlay-button = AI Overlay sandbox-window-respawn-button = Respawn sandbox-window-spawn-entities-button = Spawn Entities sandbox-window-spawn-tiles-button = Spawn Tiles diff --git a/Resources/Locale/en-US/seeds/seeds.ftl b/Resources/Locale/en-US/seeds/seeds.ftl index 9abfcdaff12..466924ae2a4 100644 --- a/Resources/Locale/en-US/seeds/seeds.ftl +++ b/Resources/Locale/en-US/seeds/seeds.ftl @@ -6,6 +6,8 @@ seeds-noun-spores = spores # Seeds seeds-wheat-name = wheat seeds-wheat-display-name = wheat stalks +seeds-meatwheat-name = meatwheat +seeds-meatwheat-display-name = meatwheat stalks seeds-oat-name = oat seeds-oat-display-name = oat stalks seeds-banana-name = banana @@ -26,12 +28,16 @@ seeds-lime-name = lime seeds-lime-display-name = lime trees seeds-orange-name = orange seeds-orange-display-name = orange trees +seeds-extradimensionalorange-name = extradimensional orange +seeds-extradimensionalorange-display-name = extradimensional orange trees seeds-pineapple-name = pineapple seeds-pineapple-display-name = pineapple plant seeds-potato-name = potato seeds-potato-display-name = potatoes seeds-sugarcane-name = sugarcane seeds-sugarcane-display-name = sugarcanes +seeds-papercane-name = papercane +seeds-papercane-display-name = papercanes seeds-towercap-name = tower cap seeds-towercap-display-name = tower caps seeds-steelcap-name = steel cap @@ -46,6 +52,8 @@ seeds-eggplant-name = eggplant seeds-eggplant-display-name = eggplants seeds-apple-name = apple seeds-apple-display-name = apple tree +seeds-goldenapple-name = golden apple +seeds-goldenapple-display-name = golden apple tree seeds-corn-name = corn seeds-corn-display-name = ears of corn seeds-onion-name = onion @@ -86,6 +94,8 @@ seeds-ambrosiadeus-name = ambrosia deus seeds-ambrosiadeus-display-name = ambrosia deus seeds-galaxythistle-name = galaxythistle seeds-galaxythistle-display-name = galaxythistle +seeds-glasstle-name = glasstle +seeds-glasstle-display-name = glasstle seeds-flyamanita-name = fly amanita seeds-flyamanita-display-name = fly amanita seeds-gatfruit-name = gatfruit @@ -99,7 +109,9 @@ seeds-spacemans-trumpet-display-name = spaceman's trumpet plant seeds-koibean-name = koibeans seeds-koibean-display-name = koibean plant seeds-watermelon-name = watermelon -seeds-watermelon-display-name = watermelon plant +seeds-watermelon-display-name = watermelon vines +seeds-holymelon-name = holymelon +seeds-holymelon-display-name = holymelon vines seeds-grape-name = grape seeds-grape-display-name = grape plant seeds-cocoa-name = cocoa @@ -108,11 +120,17 @@ seeds-berries-name = berries seeds-berries-display-name = berry bush seeds-bungo-name = bungo seeds-bungo-display-name = bungo plant -seeds-pea-name = pea +seeds-pea-name = peas seeds-pea-display-name = pea vines +seeds-worldpea-name = world peas +seeds-worldpea-display-name = world pea vines seeds-pumpkin-name = pumpkin seeds-pumpkin-display-name = pumpkins seeds-cotton-name = cotton seeds-cotton-display-name = cotton plant seeds-pyrotton-name = pyrotton seeds-pyrotton-display-name = pyrotton plant +seeds-capfruit-name = capfruit +seeds-capfruit-display-name = capfruit tree +seeds-cherry-name = cherry +seeds-cherry-display-name = cherry tree diff --git a/Resources/Locale/en-US/species/shadowkin.ftl b/Resources/Locale/en-US/species/shadowkin.ftl index b0c282df752..dc3f8371a07 100644 --- a/Resources/Locale/en-US/species/shadowkin.ftl +++ b/Resources/Locale/en-US/species/shadowkin.ftl @@ -13,4 +13,4 @@ examine-mindbroken-shadowkin-message = {CAPITALIZE($entity)} seems to be a black identity-eye-shadowkin = {$color}-eye shadowkin-blackeye = I feel my power draining away... -shadowkin-tired = Im too tired! +shadowkin-tired = I don't have the energy for that. diff --git a/Resources/Locale/en-US/species/species.ftl b/Resources/Locale/en-US/species/species.ftl index 9278267cc44..bfebc705c56 100644 --- a/Resources/Locale/en-US/species/species.ftl +++ b/Resources/Locale/en-US/species/species.ftl @@ -11,4 +11,8 @@ species-name-moth = Moth Person species-name-skeleton = Skeleton species-name-vox = Vox species-name-ipc = IPC -species-name-shadowkin = Shadowkin \ No newline at end of file +species-name-shadowkin = Shadowkin + +## Misc species things + +snail-hurt-by-salt-popup = The salty solution burns like acid! \ No newline at end of file diff --git a/Resources/Locale/en-US/station-events/events/vent-critters.ftl b/Resources/Locale/en-US/station-events/events/vent-critters.ftl index d99906be4a3..31cac5f73ac 100644 --- a/Resources/Locale/en-US/station-events/events/vent-critters.ftl +++ b/Resources/Locale/en-US/station-events/events/vent-critters.ftl @@ -1,4 +1,6 @@ station-event-cockroach-migration-announcement = Attention. A large influx of unknown life forms have been detected residing within the station's ventilation systems. Please be rid of these creatures before it begins to affect productivity. +station-event-snail-migration-announcement = Attention. A large influx of unknown life forms have been detected residing within the station's ventilation systems. Please be rid of these creatures before it begins to affect productivity. +station-event-snail-migration-low-pop-announcement = Attention. A large influx of unknown life forms have been detected residing within the station's ventilation systems. Please be rid of these creatures before it begins to affect productivity. station-event-slimes-spawn-announcement = Attention. A large influx of unknown life forms have been detected residing within the station's ventilation systems. Please be rid of these creatures before it begins to affect productivity. station-event-vent-critters-announcement = Attention. A large influx of unknown life forms have been detected residing within the station's ventilation systems. Please be rid of these creatures before it begins to affect productivity. station-event-spider-spawn-announcement = Attention. A large influx of unknown life forms have been detected residing within the station's ventilation systems. Please be rid of these creatures before it begins to affect productivity. diff --git a/Resources/Locale/en-US/station-records/general-station-records.ftl b/Resources/Locale/en-US/station-records/general-station-records.ftl index e36bfac7cb3..4516a547f4d 100644 --- a/Resources/Locale/en-US/station-records/general-station-records.ftl +++ b/Resources/Locale/en-US/station-records/general-station-records.ftl @@ -16,3 +16,4 @@ general-station-record-prints-filter = Fingerprints general-station-record-dna-filter = DNA general-station-record-console-search-records = Search general-station-record-console-reset-filters = Reset +general-station-record-console-delete = Delete diff --git a/Resources/Locale/en-US/store/categories.ftl b/Resources/Locale/en-US/store/categories.ftl index 0d0dc4aecc0..64ed0b5c637 100644 --- a/Resources/Locale/en-US/store/categories.ftl +++ b/Resources/Locale/en-US/store/categories.ftl @@ -1,18 +1,18 @@ -# Uplink +# Uplink store-category-debug = debug category store-category-debug2 = debug category 2 -store-category-weapons = Weapons +store-category-weapons = Weaponry store-category-ammo = Ammo store-category-explosives = Explosives -store-category-misc = Misc -store-category-bundles = Bundles -store-category-tools = Tools -store-category-utility = Utility +store-category-chemicals = Chemicals +store-category-deception = Deception +store-category-disruption = Disruption store-category-implants = Implants +store-category-allies = Allies store-category-job = Job -store-category-armor = Armor +store-category-wearables = Wearables store-category-pointless = Pointless -store-category-deception = Deception +store-discounted-items = Discounts # Revenant store-category-abilities = Abilities diff --git a/Resources/Locale/en-US/store/store.ftl b/Resources/Locale/en-US/store/store.ftl index 997afedfc06..a0b349e6a2a 100644 --- a/Resources/Locale/en-US/store/store.ftl +++ b/Resources/Locale/en-US/store/store.ftl @@ -2,9 +2,14 @@ store-ui-default-title = Store store-ui-default-withdraw-text = Withdraw store-ui-balance-display = {$currency}: {$amount} store-ui-price-display = {$amount} {$currency} +store-ui-discount-display-with-currency = {$amount} off on {$currency} +store-ui-discount-display = ({$amount} off!) store-ui-traitor-flavor = Copyright (C) NT -30643 store-ui-traitor-warning = Operatives must lock their uplinks after use to avoid detection. store-withdraw-button-ui = Withdraw {$currency} store-ui-button-out-of-stock = {""} (Out of Stock) store-not-account-owner = This {$store} is not bound to you! + +store-preset-name-uplink = Uplink +store-preset-name-spellbook = Spellbook diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 2ac91d171eb..7eaacc50267 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -317,6 +317,9 @@ uplink-clothing-shoes-boots-mag-syndie-desc = A pair of boots that prevent slipp uplink-eva-syndie-name = Syndicate EVA Bundle uplink-eva-syndie-desc = A simple EVA suit that offers no protection other than what's needed to survive in space. +uplink-hardsuit-carp-name = Carp Hardsuit +uplink-hardsuit-carp-desc = Looks like an ordinary carp suit, except fully spaceproof and tricks space carp into thinking you are one of them. + uplink-hardsuit-syndie-name = Syndicate Hardsuit uplink-hardsuit-syndie-desc = The Syndicate's well known armored blood red hardsuit, capable of space walks and bullet resistant. @@ -438,3 +441,6 @@ uplink-backpack-syndicate-desc = Lightweight explosion-proof а backpack for hol uplink-home-run-bat-name = Home Run Bat uplink-home-run-bat-desc = Rigged bat pre-coated in blood for Syndicate tee-ball practice. Launch your foes! + +uplink-combat-bakery-name = Combat Bakery Kit +uplink-combat-bakery-desc = A kit of clandestine baked weapons. Contains a baguette sword, a pair of throwing croissants, and a syndicate microwave board for making more. Once the job is done, eat the evidence. diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 97a8150c495..3e68fbbcabe 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -412,33 +412,30 @@ trait-description-CyberEyes = Their most basic functionality is to provide amelioration for weaknesses of the wearer's natural eyes. The functionality of these implants can be extended by a variety of commercially available modules. -trait-name-FlareShielding = Cyber-Eyes Flare Shielding +trait-name-FlareShielding = Cyber-Eyes: Eye Damage Resistance trait-description-FlareShielding = Your cybereyes have been fitted with a photochromic lense that automatically darkens in response to intense stimuli. - This provides substantial protection from bright flashes of light, such as those from welding arcs. + This provides immunity from most bright flashes of light, such as those from welding arcs. -trait-name-CyberEyesSecurity = Cyber-Eyes SecHud +trait-name-CyberEyesSecurity = Cyber-Eyes: SecHud Module trait-description-CyberEyesSecurity = Your Cyber-Eyes have been upgraded to include a built-in Security Hud. Note that this augmentation is considered Contraband for anyone not under the employ of station Security personel, and may be disabled by your employer before dispatch to the station. -trait-name-CyberEyesMedical = Cyber-Eyes MedHud +trait-name-CyberEyesMedical = Cyber-Eyes: MedHud Module trait-description-CyberEyesMedical = Your Cyber-Eyes have been upgraded to include a built-in Medical Hud, allowing you to track the relative health condition of biological organisms. -trait-name-CyberEyesDiagnostic = Cyber-Eyes DiagHud +trait-name-CyberEyesDiagnostic = Cyber-Eyes: Diagnostics Module trait-description-CyberEyesDiagnostic = Your Cyber-Eyes have been upgraded to include a built-in Diagnostic Hud, allowing you to track the condition of synthetic entities. -trait-name-CyberEyesOmni = Cyber-Eyes HudSuite +trait-name-CyberEyesOmni = Cyber-Eyes: Premium Suite Module trait-description-CyberEyesOmni = - This expensive implant provides the combined benefits of a SecHud, MedHud, and a DiagHud. + This expensive implant provides the combined benefits of a SecHud, MedHud, and a Diagnostics Module. Note that this augmentation is considered Contraband for anyone not under the employ of station Security personel, and may be disabled by your employer before dispatch to the station. -trait-name-ShadowkinBlackeye = Blackeye -trait-description-ShadowkinBlackeye = You lose your special Shadowkin powers, in return for some points. - trait-name-DispelPower = Normality Projection trait-description-DispelPower = Your Mentalic abilities include the power to enforce normality upon Noospheric phenomena. diff --git a/Resources/Locale/en-US/wieldable/wieldable-component.ftl b/Resources/Locale/en-US/wieldable/wieldable-component.ftl index 84b58224a77..585d6110164 100644 --- a/Resources/Locale/en-US/wieldable/wieldable-component.ftl +++ b/Resources/Locale/en-US/wieldable/wieldable-component.ftl @@ -18,3 +18,5 @@ wieldable-component-not-in-hands = { CAPITALIZE(THE($item)) } isn't in your hand wieldable-component-requires = { CAPITALIZE(THE($item))} must be wielded! gunwieldbonus-component-examine = This weapon has improved accuracy when wielded. + +gunrequireswield-component-examine = This weapon can only be fired when wielded. diff --git a/Resources/Locale/en-US/zombies/zombie.ftl b/Resources/Locale/en-US/zombies/zombie.ftl index a391a95b0da..d45943e825d 100644 --- a/Resources/Locale/en-US/zombies/zombie.ftl +++ b/Resources/Locale/en-US/zombies/zombie.ftl @@ -2,7 +2,7 @@ zombie-transform = {CAPITALIZE(THE($target))} turned into a zombie! zombie-infection-greeting = You have become a zombie. Your goal is to seek out the living and to try to infect them. Work together with the other zombies to overtake the station. zombie-generic = zombie -zombie-name-prefix = Zombified {$target} +zombie-name-prefix = zombified {$baseName} zombie-role-desc = A malevolent creature of the dead. zombie-role-rules = You are an antagonist. Search out the living and bite them in order to infect them and turn them into zombies. Work together with the other zombies to overtake the station. diff --git a/Resources/Maps/Nonstations/nukieplanet.yml b/Resources/Maps/Nonstations/nukieplanet.yml index 304def19c70..b1d5d558255 100644 --- a/Resources/Maps/Nonstations/nukieplanet.yml +++ b/Resources/Maps/Nonstations/nukieplanet.yml @@ -1,15274 +1,15274 @@ -meta: - format: 6 - postmapinit: false -tilemap: - 0: Space - 14: FloorBar - 20: FloorCarpetClown - 29: FloorDark - 44: FloorFreezer - 59: FloorIce - 77: FloorReinforced - 79: FloorRockVault - 80: FloorShowroom - 84: FloorShuttleOrange - 86: FloorShuttleRed - 89: FloorSnow - 91: FloorSteel - 106: FloorTechMaint - 110: FloorWhite - 120: FloorWood - 123: Lattice - 124: Plating -entities: -- proto: "" - entities: - - uid: 104 - components: - - type: MetaData - name: Syndicate Outpost - - type: Transform - parent: 1295 - - type: FTLDestination - whitelist: - tags: - - Syndicate - - type: MapGrid - chunks: - -1,-1: - ind: -1,-1 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAKWQAAAAAAfAAAAAAAHQAAAAACbgAAAAADbgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABbgAAAAADWwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAAAbgAAAAACbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAAAHQAAAAADHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAADWQAAAAAEWQAAAAAAWQAAAAAEfAAAAAAAeAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAeAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAeAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAADgAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAIfAAAAAAADgAAAAAB - version: 6 - 0,-1: - ind: 0,-1 - tiles: bgAAAAAAbgAAAAADfAAAAAAAbgAAAAAAbgAAAAACbgAAAAACbgAAAAADbgAAAAABHQAAAAAAHQAAAAABHQAAAAABHQAAAAABHQAAAAABHQAAAAAAHQAAAAADHQAAAAACWwAAAAAAbgAAAAADbgAAAAADbgAAAAADbgAAAAACbgAAAAAAbgAAAAADbgAAAAADHQAAAAADVgAAAAAAHQAAAAACVgAAAAAAHQAAAAADVgAAAAAAHQAAAAADHQAAAAADbgAAAAAAbgAAAAACfAAAAAAAbgAAAAADbgAAAAACbgAAAAADbgAAAAACbgAAAAACHQAAAAADVgAAAAAAHQAAAAADVgAAAAAAHQAAAAABVgAAAAAAHQAAAAABHQAAAAAAHQAAAAABHQAAAAADHQAAAAAAWwAAAAADWwAAAAACWwAAAAADbgAAAAADbgAAAAACHQAAAAABHQAAAAAAHQAAAAAAHQAAAAABHQAAAAABHQAAAAABHQAAAAAAHQAAAAADfAAAAAAAfAAAAAAAfAAAAAAAWwAAAAABWwAAAAABWwAAAAACbgAAAAADbgAAAAACDgAAAAACHQAAAAADHQAAAAAAHQAAAAADHQAAAAADHQAAAAACHQAAAAAAHQAAAAADWQAAAAAAWQAAAAAAbgAAAAAAWwAAAAABWwAAAAAAWwAAAAAAbgAAAAACbgAAAAAAWQAAAAAAHQAAAAADHQAAAAADVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAADgAAAAABDgAAAAADDgAAAAACDgAAAAAADgAAAAADfAAAAAAAHQAAAAABfAAAAAAADgAAAAADDgAAAAACDgAAAAAADgAAAAACDgAAAAAAHQAAAAABHQAAAAADHQAAAAABVgAAAAAAVgAAAAAAeAAAAAACeAAAAAACeAAAAAADHQAAAAAAHQAAAAABHQAAAAABHQAAAAAAHQAAAAADHQAAAAACfAAAAAAAHQAAAAACHQAAAAACHQAAAAADHQAAAAABeAAAAAAAeAAAAAACeAAAAAACeAAAAAADeAAAAAADHQAAAAAAHQAAAAAAHQAAAAACHQAAAAAAHQAAAAACHQAAAAACHQAAAAABHQAAAAADHQAAAAABHQAAAAACHQAAAAABeAAAAAAAeAAAAAABeAAAAAAAeAAAAAADeAAAAAADHQAAAAAAHQAAAAAAHQAAAAADHQAAAAAAHQAAAAABHQAAAAADfAAAAAAAHQAAAAACHQAAAAABHQAAAAABHQAAAAACDgAAAAACDgAAAAAADgAAAAACDgAAAAADDgAAAAACDgAAAAABDgAAAAACDgAAAAAADgAAAAAADgAAAAACDgAAAAADDgAAAAAADgAAAAABHQAAAAAAHQAAAAACHQAAAAACDgAAAAAADgAAAAABDgAAAAACDgAAAAAADgAAAAABDgAAAAABDgAAAAADDgAAAAAADgAAAAAALAAAAAAALAAAAAAALAAAAAAADgAAAAACeAAAAAAAeAAAAAADeAAAAAABDgAAAAACDgAAAAACDgAAAAAADgAAAAAADgAAAAADDgAAAAAADgAAAAAAVgAAAAAADgAAAAACLAAAAAAALAAAAAAALAAAAAAADgAAAAADeAAAAAABeAAAAAABeAAAAAADDgAAAAADDgAAAAABDgAAAAADDgAAAAACDgAAAAADDgAAAAAADgAAAAAAVgAAAAAADgAAAAAAUAAAAAAALAAAAAAADgAAAAAADgAAAAACHQAAAAAAHQAAAAACfAAAAAAADgAAAAACDgAAAAACDgAAAAACDgAAAAAADgAAAAADDgAAAAABDgAAAAACVgAAAAAADgAAAAADUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAADgAAAAAADgAAAAADDgAAAAACDgAAAAACDgAAAAACDgAAAAABDgAAAAABDgAAAAAADgAAAAACUAAAAAAAUAAAAAAALAAAAAAAUAAAAAAAUAAAAAAAfAAAAAAAfAAAAAAA - version: 6 - 0,0: - ind: 0,0 - tiles: HQAAAAADHQAAAAACHQAAAAACDgAAAAAADgAAAAABDgAAAAABDgAAAAABDgAAAAACDgAAAAADUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAfAAAAAAAWQAAAAAGfAAAAAAAfAAAAAAAfAAAAAAAHQAAAAACfAAAAAAADgAAAAAADgAAAAABDgAAAAABDgAAAAACDgAAAAABDgAAAAAADgAAAAACDgAAAAACWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATwAAAAAAfAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAfAAAAAAATwAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAWQAAAAAIWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAIWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAKWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAGWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAMWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAACWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAJWQAAAAAAWQAAAAAFTwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAHWQAAAAAKWQAAAAAFWQAAAAAAWQAAAAALWQAAAAAA - version: 6 - -1,0: - ind: -1,0 - tiles: ewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAAWQAAAAAKWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAKWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAGHQAAAAADHQAAAAADHQAAAAAAfAAAAAAAHQAAAAAAHQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAIfAAAAAAAHQAAAAABHQAAAAACHQAAAAACHQAAAAADHQAAAAACHQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAfAAAAAAAHQAAAAABHQAAAAABWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAGTwAAAAAAHQAAAAABHQAAAAAAfAAAAAAAfAAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAADWQAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAATwAAAAAATwAAAAAATwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAATwAAAAAATwAAAAAATwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKTwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAA - version: 6 - 1,-1: - ind: 1,-1 - tiles: HQAAAAADHQAAAAADHQAAAAACHQAAAAACHQAAAAAATQAAAAAATQAAAAAATQAAAAAAHQAAAAADHQAAAAACHQAAAAACHQAAAAACfAAAAAAAWQAAAAAAWQAAAAABWQAAAAACHQAAAAADHQAAAAADHQAAAAADHQAAAAAAHQAAAAADTQAAAAAATQAAAAAATQAAAAAAHQAAAAADHQAAAAACHQAAAAADHQAAAAACfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAHQAAAAABHQAAAAADHQAAAAAAHQAAAAABHQAAAAACHQAAAAAAHQAAAAADHQAAAAADHQAAAAAAHQAAAAADHQAAAAABHQAAAAACfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAHQAAAAADHQAAAAAAHQAAAAAAHQAAAAADHQAAAAADHQAAAAABHQAAAAACHQAAAAADHQAAAAADHQAAAAABfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAMWQAAAAAHWQAAAAAAHQAAAAABHQAAAAAAHQAAAAADHQAAAAABHQAAAAACHQAAAAAAHQAAAAADHQAAAAACHQAAAAACHQAAAAABWQAAAAAAWQAAAAADWQAAAAAAWQAAAAACWQAAAAAAAAAAAAAAHQAAAAAAHQAAAAABHQAAAAABHQAAAAADHQAAAAABHQAAAAABHQAAAAABHQAAAAADHQAAAAABHQAAAAACWQAAAAAFWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAHQAAAAADHQAAAAABVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAACHQAAAAACHQAAAAACWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAACAAAAAAAAfAAAAAAAHQAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAADHQAAAAADHQAAAAACWQAAAAAETwAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAAAAAAAAAHQAAAAAAHQAAAAABVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAACHQAAAAADHQAAAAACWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAACVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAAAHQAAAAADHQAAAAACWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAfAAAAAAAHQAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAAAHQAAAAABHQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABHQAAAAADHQAAAAAAHQAAAAAAHQAAAAABHQAAAAAAHQAAAAACHQAAAAABHQAAAAABWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABHQAAAAADHQAAAAACHQAAAAACHQAAAAADHQAAAAADHQAAAAACHQAAAAAAHQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAHQAAAAAAHQAAAAACHQAAAAACHQAAAAAAHQAAAAABHQAAAAADHQAAAAAAHQAAAAABWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAGfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAWQAAAAAATwAAAAAAWQAAAAAEWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAA - version: 6 - 0,-2: - ind: 0,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWwAAAAAAWwAAAAAAWwAAAAABWwAAAAAAWwAAAAACfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAbgAAAAAAbgAAAAADbgAAAAADbgAAAAADbgAAAAAAfAAAAAAAagAAAAAAWwAAAAACWwAAAAADWwAAAAABWwAAAAABWwAAAAADfAAAAAAAHQAAAAAAHQAAAAABWQAAAAAAbgAAAAABbgAAAAABbgAAAAADbgAAAAADbgAAAAAAagAAAAAAagAAAAAAWwAAAAADWwAAAAAAWwAAAAABWwAAAAABWwAAAAAAfAAAAAAA - version: 6 - -1,-2: - ind: -1,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAFWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABHQAAAAAAHQAAAAAC - version: 6 - -1,1: - ind: -1,1 - tiles: WQAAAAAIWQAAAAAAWQAAAAALWQAAAAAAWQAAAAADWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAFWQAAAAADWQAAAAAEWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFTwAAAAAATwAAAAAATwAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAACWQAAAAAAWQAAAAADTwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 0,1: - ind: 0,1 - tiles: WQAAAAAAWQAAAAAEWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAJWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAALWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAIWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAACWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAADWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAA - version: 6 - 1,1: - ind: 1,1 - tiles: WQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAABWQAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 1,0: - ind: 1,0 - tiles: WQAAAAAATwAAAAAATwAAAAAAWQAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAALWQAAAAAAWQAAAAAMWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAFWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAHWQAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEWQAAAAAJWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAACWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAABWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 1,-2: - ind: 1,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAABWQAAAAAAWQAAAAAAAAAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAMWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAagAAAAAAHQAAAAACHQAAAAADHQAAAAACfAAAAAAAfAAAAAAAfAAAAAAAHQAAAAAAHQAAAAACHQAAAAABagAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJfAAAAAAAagAAAAAAHQAAAAADHQAAAAACHQAAAAADHQAAAAACHQAAAAACHQAAAAADHQAAAAABHQAAAAABHQAAAAABagAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAagAAAAAAHQAAAAAAHQAAAAABHQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAHQAAAAACHQAAAAADHQAAAAACagAAAAAAfAAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAEHQAAAAACHQAAAAACHQAAAAABHQAAAAABTQAAAAAATQAAAAAATQAAAAAAHQAAAAABHQAAAAADHQAAAAACHQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAD - version: 6 - -2,0: - ind: -2,0 - tiles: ewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAABTwAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAATwAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAETwAAAAAATwAAAAAATwAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAALWQAAAAAAWQAAAAALWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAA - version: 6 - -2,1: - ind: -2,1 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAKFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAGWQAAAAAJWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAALWQAAAAAIWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAGFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAEWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEWQAAAAAAWQAAAAAMWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAA - version: 6 - -2,-2: - ind: -2,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 2,0: - ind: 2,0 - tiles: TwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 2,-1: - ind: 2,-1 - tiles: TwAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 2,-2: - ind: 2,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAIWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - -2,2: - ind: -2,2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - -3,-2: - ind: -3,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 0,2: - ind: 0,2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAEWQAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 1,2: - ind: 1,2 - tiles: WQAAAAAAWQAAAAAFWQAAAAACWQAAAAAJWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - -3,0: - ind: -3,0 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - - type: Broadphase - - type: Physics - bodyStatus: InAir - angularDamping: 0.05 - linearDamping: 0.05 - fixedRotation: False - bodyType: Dynamic - - type: Fixtures - fixtures: {} - - type: Gravity - gravityShakeSound: !type:SoundPathSpecifier - path: /Audio/Effects/alert.ogg - - type: DecalGrid - chunkCollection: - version: 2 - nodes: - - node: - angle: -3.141592653589793 rad - color: '#FFFFFFFF' - id: Arrows - decals: - 311: 27,-15 - - node: - color: '#FFFFFFFF' - id: Arrows - decals: - 309: 18,-19 - 310: 26,-19 - - node: - color: '#FFFFFFFF' - id: Basalt1 - decals: - 368: 26.834131,4.97881 - - node: - color: '#FFFFFFFF' - id: Bot - decals: - 295: 21,-16 - 296: 22,-15 - 297: 23,-16 - 302: 22,-17 - 303: 17,-19 - 304: 27,-19 - - node: - color: '#FFFFFFFF' - id: BotLeft - decals: - 194: 5,-11 - 195: 4,-11 - 300: 23,-15 - 301: 21,-17 - 305: 27,-18 - 306: 17,-20 - - node: - color: '#FFFFFFFF' - id: BotRight - decals: - 298: 23,-17 - 299: 21,-15 - 307: 27,-20 - 308: 17,-18 - - node: - color: '#FFFFFFFF' - id: Box - decals: - 206: 12,-22 - - node: - color: '#FFFFFFFF' - id: BrickTileDarkLineN - decals: - 326: 2,-1 - 327: 1,-1 - 328: 0,-1 - - node: - color: '#52B4E996' - id: BrickTileSteelBox - decals: - 238: -3,-15 - 239: -2,-17 - 240: 1,-17 - 241: 1,-13 - 242: -2,-13 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerNe - decals: - 143: 15,-12 - 149: 10,-11 - 186: 5,-11 - 332: 10,-7 - 339: 15,-7 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerNw - decals: - 150: 9,-11 - 187: 3,-11 - 340: 12,-7 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerSe - decals: - 141: 15,-15 - 188: 5,-13 - 259: 27,-17 - 264: 26,-20 - 329: 10,-9 - 342: 15,-9 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerSw - decals: - 127: 14,-15 - 138: 9,-13 - 189: 3,-13 - 256: 17,-17 - 263: 18,-20 - 335: 5,-9 - 341: 12,-9 - - node: - color: '#DE3A3A96' - id: BrickTileSteelEndS - decals: - 125: 10,-15 - 126: 12,-15 - - node: - color: '#DE3A3A96' - id: BrickTileSteelInnerNe - decals: - 148: 10,-12 - 250: -3,-17 - - node: - color: '#DE3A3A96' - id: BrickTileSteelInnerSe - decals: - 139: 10,-13 - 140: 12,-13 - 249: -3,-13 - 260: 26,-17 - 289: 20,-14 - - node: - color: '#DE3A3A96' - id: BrickTileSteelInnerSw - decals: - 133: 10,-13 - 134: 12,-13 - 135: 14,-13 - 261: 18,-17 - 285: 24,-14 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineE - decals: - 128: 12,-14 - 129: 10,-14 - 142: 15,-13 - 191: 5,-12 - 247: -3,-16 - 248: -3,-14 - 257: 26,-19 - 258: 26,-18 - 265: 20,-17 - 266: 20,-16 - 267: 20,-15 - 290: 20,-19 - 312: 27,-16 - 313: 27,-15 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineN - decals: - 144: 14,-12 - 145: 13,-12 - 146: 12,-12 - 147: 11,-12 - 192: 4,-11 - 243: 0,-17 - 244: -1,-17 - 274: 26,-14 - 275: 25,-14 - 276: 23,-14 - 277: 22,-14 - 278: 21,-14 - 279: 20,-14 - 280: 18,-14 - 281: 19,-14 - 284: 24,-14 - 333: 9,-7 - 334: 8,-7 - 345: 13,-7 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineS - decals: - 136: 13,-13 - 137: 11,-13 - 190: 4,-13 - 245: 0,-13 - 246: -1,-13 - 272: 25,-20 - 273: 19,-20 - 286: 23,-14 - 287: 22,-14 - 288: 21,-14 - 330: 8,-9 - 331: 7,-9 - 343: 14,-9 - 344: 13,-9 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineW - decals: - 130: 14,-14 - 131: 12,-14 - 132: 10,-14 - 151: 9,-12 - 193: 3,-12 - 253: 18,-18 - 254: 17,-16 - 255: 17,-15 - 262: 18,-19 - 268: 24,-19 - 269: 24,-17 - 270: 24,-16 - 271: 24,-15 - 336: 5,-8 - 359: 5,-7 - - node: - color: '#9FED5896' - id: BrickTileWhiteCornerNe - decals: - 159: 7,-11 - - node: - color: '#9FED5896' - id: BrickTileWhiteCornerNw - decals: - 160: 6,-11 - 178: 3,-14 - - node: - color: '#9FED5896' - id: BrickTileWhiteCornerSe - decals: - 171: 7,-18 - - node: - color: '#9FED5896' - id: BrickTileWhiteCornerSw - decals: - 174: 3,-18 - - node: - color: '#9FED5896' - id: BrickTileWhiteInnerNw - decals: - 162: 6,-14 - - node: - color: '#9FED5896' - id: BrickTileWhiteLineE - decals: - 165: 7,-12 - 166: 7,-13 - 167: 7,-14 - 168: 7,-15 - 169: 7,-16 - 170: 7,-17 - - node: - color: '#9FED5896' - id: BrickTileWhiteLineN - decals: - 163: 5,-14 - 164: 4,-14 - - node: - color: '#9FED5896' - id: BrickTileWhiteLineS - decals: - 172: 6,-18 - 173: 5,-18 - 175: 4,-18 - - node: - color: '#9FED5896' - id: BrickTileWhiteLineW - decals: - 161: 6,-13 - 176: 3,-17 - 177: 3,-16 - - node: - color: '#A4610647' - id: CheckerNWSE - decals: - 196: -13,6 - 197: -14,6 - 198: -15,6 - 199: -15,7 - 200: -14,7 - 201: -13,7 - 202: -14,8 - 203: -15,8 - - node: - color: '#D381C996' - id: CheckerNWSE - decals: - 207: 10,-19 - 208: 11,-19 - 209: 12,-19 - 210: 13,-19 - 211: 14,-19 - 212: 14,-18 - 213: 14,-17 - 214: 13,-17 - 215: 13,-18 - 216: 12,-18 - 217: 12,-17 - 218: 11,-17 - 219: 11,-18 - 220: 10,-18 - 221: 10,-17 - - node: - color: '#FFFFFFFF' - id: Delivery - decals: - 204: -16,8 - 205: -16,9 - 251: 7,-18 - 252: 6,-18 - - node: - color: '#DE3A3A96' - id: DeliveryGreyscale - decals: - 283: 16,-14 - - node: - color: '#9FED5896' - id: FullTileOverlayGreyscale - decals: - 181: 5,-17 - 182: 5,-16 - 183: 5,-15 - 184: 6,-16 - 185: 4,-16 - - node: - color: '#52B4E996' - id: HalfTileOverlayGreyscale - decals: - 232: 1,-14 - 233: 0,-14 - 234: -1,-14 - - node: - color: '#9FED5896' - id: HalfTileOverlayGreyscale - decals: - 324: 11,-2 - - node: - color: '#52B4E996' - id: HalfTileOverlayGreyscale180 - decals: - 229: 1,-16 - 230: 0,-16 - 231: -1,-16 - - node: - color: '#9FED5896' - id: HalfTileOverlayGreyscale180 - decals: - 325: 11,0 - - node: - color: '#52B4E996' - id: HalfTileOverlayGreyscale270 - decals: - 235: -2,-15 - - node: - color: '#9FED5896' - id: HalfTileOverlayGreyscale270 - decals: - 323: 12,-1 - - node: - color: '#9FED5896' - id: HalfTileOverlayGreyscale90 - decals: - 322: 10,-1 - - node: - color: '#9FED5896' - id: QuarterTileOverlayGreyscale - decals: - 314: 9,0 - 315: 10,0 - - node: - color: '#9FED5896' - id: QuarterTileOverlayGreyscale180 - decals: - 318: 13,-2 - 319: 12,-2 - - node: - color: '#9FED5896' - id: QuarterTileOverlayGreyscale270 - decals: - 320: 9,-2 - 321: 10,-2 - - node: - color: '#9FED5896' - id: QuarterTileOverlayGreyscale90 - decals: - 316: 13,0 - 317: 12,0 - - node: - color: '#FFFFFFFF' - id: Rock06 - decals: - 75: 9.232971,15.9332485 - - node: - color: '#52B4E996' - id: ThreeQuarterTileOverlayGreyscale - decals: - 237: -2,-14 - - node: - color: '#52B4E996' - id: ThreeQuarterTileOverlayGreyscale270 - decals: - 236: -2,-16 - - node: - color: '#FFFFFFFF' - id: WarnBox - decals: - 124: -5,-3 - - node: - angle: -1.5707963267948966 rad - color: '#FFFFFFFF' - id: WarnCorner - decals: - 21: 18,-5 - - node: - color: '#FFFFFFFF' - id: WarnCorner - decals: - 0: 17,-10 - 1: 18,-11 - 2: 19,-11 - - node: - angle: 4.71238898038469 rad - color: '#FFFFFFFF' - id: WarnCorner - decals: - 14: 17,-6 - - node: - color: '#FFFFFFFF' - id: WarnCornerFlipped - decals: - 7: 23,-10 - 13: 16,-6 - 19: 22,-11 - - node: - angle: 1.5707963267948966 rad - color: '#FFFFFFFF' - id: WarnCornerFlipped - decals: - 11: 23,-6 - 12: 22,-5 - - node: - color: '#DE3A3A96' - id: WarnCornerGreyscaleNW - decals: - 282: 17,-14 - - node: - color: '#FFFFFFFF' - id: WarnCornerSE - decals: - 292: 20,-20 - - node: - color: '#FFFFFFFF' - id: WarnCornerSW - decals: - 226: 10,-19 - 291: 24,-20 - - node: - color: '#FFFFFFFF' - id: WarnEndE - decals: - 154: 1,-9 - - node: - color: '#FFFFFFFF' - id: WarnEndN - decals: - 156: 7,-2 - - node: - color: '#FFFFFFFF' - id: WarnEndS - decals: - 123: -6,21 - 155: 7,-4 - - node: - color: '#FFFFFFFF' - id: WarnEndW - decals: - 153: 0,-9 - - node: - color: '#DE3A3A96' - id: WarnFullGreyscale - decals: - 352: 16,-8 - 353: 11,-8 - 354: 9,-10 - 355: 6,-10 - - node: - color: '#FFFFFFFF' - id: WarnLineE - decals: - 158: 7,-3 - 294: 20,-18 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleE - decals: - 152: 15,-14 - 349: 10,-8 - 350: 15,-8 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleN - decals: - 351: 14,-7 - 356: 7,-7 - 357: 6,-7 - 358: 5,-7 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleS - decals: - 346: 9,-9 - 347: 6,-9 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleW - decals: - 348: 12,-8 - - node: - color: '#FFFFFFFF' - id: WarnLineGreyscaleW - decals: - 179: 3,-15 - 180: 6,-12 - - node: - color: '#FFFFFFFF' - id: WarnLineN - decals: - 222: 14,-19 - 223: 13,-19 - 224: 12,-19 - 225: 11,-19 - - node: - color: '#FFFFFFFF' - id: WarnLineS - decals: - 94: -9,4 - 95: -9,3 - 96: -9,2 - 97: -9,1 - 98: -9,0 - 99: -9,-1 - 100: -9,-2 - 101: -9,-3 - 102: -9,-4 - 103: -9,-5 - 104: -9,-6 - 105: -9,-7 - 106: -9,-8 - 107: -9,-9 - 108: -9,-10 - 109: -9,-11 - 110: -9,-12 - 111: -9,-13 - 112: -9,-14 - 113: -9,-15 - 114: -9,-16 - 115: -9,-17 - 116: -9,-18 - 117: -9,-19 - 118: -9,-20 - 119: -9,-21 - 120: -9,-22 - 121: -9,-23 - 122: -9,-24 - 157: 7,-3 - 227: 10,-18 - 228: 10,-17 - 293: 24,-18 - - node: - color: '#FFFFFFFF' - id: WarningLine - decals: - 15: 19,-4 - 16: 20,-4 - 17: 21,-4 - - node: - angle: 3.141592653589793 rad - color: '#FFFFFFFF' - id: WarningLine - decals: - 3: 19,-11 - 4: 19,-12 - 5: 20,-12 - 6: 21,-12 - - node: - angle: 4.71238898038469 rad - color: '#FFFFFFFF' - id: WarningLine - decals: - 8: 24,-9 - 9: 24,-8 - 10: 24,-7 - - node: - color: '#FFFFFFFF' - id: WarningLineCornerFlipped - decals: - 20: 22,-4 - - node: - angle: 4.71238898038469 rad - color: '#FFFFFFFF' - id: WarningLineCornerFlipped - decals: - 18: 24,-10 - - node: - color: '#FFFFFFFF' - id: WoodTrimThinInnerSw - decals: - 363: 5,-6 - - node: - color: '#FFFFFFFF' - id: WoodTrimThinLineS - decals: - 361: 0,-6 - 362: -1,-6 - 364: 4,-6 - 365: 3,-6 - 366: 2,-6 - 367: 1,-6 - - node: - color: '#FFFFFFFF' - id: WoodTrimThinLineW - decals: - 337: 5,-9 - 338: 5,-8 - 360: 5,-7 - - node: - color: '#FFFFFFFF' - id: bushsnowa1 - decals: - 67: 12.033197,7.701807 - 69: 14.310135,6.6772776 - - node: - color: '#FFFFFFFF' - id: bushsnowa2 - decals: - 68: -1.884201,10.692345 - 70: 13.896899,12.679136 - 73: -2.6609726,10.251652 - - node: - color: '#FFFFFFFF' - id: bushsnowb2 - decals: - 71: 12.943774,14.853275 - - node: - color: '#FFFFFFFF' - id: bushsnowb3 - decals: - 72: 0.2452774,15.588417 - 74: -0.9488945,8.937782 - - node: - color: '#AB0000FF' - id: d - decals: - 373: 21.412256,2.9944348 - - node: - color: '#FFFFFFFF' - id: grasssnowa1 - decals: - 30: 9.251883,16.971285 - 41: 4.707512,15.727119 - 47: -2.8426914,5.7681084 - 84: 16.072964,17.452217 - 85: -4.644718,19.141476 - 369: 26.896631,2.9944348 - - node: - color: '#FFFFFFFF' - id: grasssnowa2 - decals: - 22: -3.6290493,-9.771207 - 25: 15.159365,5.392831 - 42: 2.707512,15.930454 - 53: 1.6412125,14.139866 - 62: 14.449558,12.269236 - 86: -6.332218,17.702477 - 87: -7.488468,14.82448 - 91: -5.738468,8.348984 - - node: - color: '#FFFFFFFF' - id: grasssnowa3 - decals: - 44: -1.3739414,16.399694 - 54: 0.43808746,13.654987 - 57: 3.1880875,12.434965 - 58: 2.1568375,9.893942 - 63: 16.060982,2.673603 - 90: -6.207218,16.0445 - - node: - color: '#FFFFFFFF' - id: grasssnowb1 - decals: - 51: 0.79449654,2.152192 - 55: 0.18808746,9.666458 - 79: 4.997473,19.080475 - - node: - color: '#FFFFFFFF' - id: grasssnowb2 - decals: - 36: 6.1287537,4.4684677 - 38: 13.017959,2.0284252 - 61: 8.19574,16.289047 - 64: 3.43816,14.798347 - 78: 2.4505978,18.17328 - 92: -5.832218,13.979851 - 93: 4.1790752,7.8279076 - - node: - color: '#FFFFFFFF' - id: grasssnowb3 - decals: - 23: -3.3477993,-19.991972 - 31: 9.061624,11.550716 - 35: 9.066255,4.2786684 - 50: -3.1864414,-0.9828911 - 52: -0.88188744,15.563225 - 65: 0.5527668,14.892197 - 83: 17.104214,9.229881 - - node: - color: '#FFFFFFFF' - id: grasssnowc1 - decals: - 24: -3.9999733,-6.081314 - 26: 15.819311,9.398571 - 32: 12.170999,10.565315 - 39: 10.861709,2.2630453 - 40: 8.471084,13.624069 - 45: -2.1864414,7.911888 - 48: -0.6551914,1.8769035 - 49: -3.7333164,0.34661865 - 56: 4.1724625,13.733192 - 76: 2.5912228,19.753052 - 82: 17.947964,3.1297789 - 88: -7.175968,10.382353 - - node: - color: '#FFFFFFFF' - id: grasssnowc2 - decals: - 27: 14.194311,14.208315 - 29: 12.819311,16.642818 - 33: 8.01938,8.972565 - 81: 19.322964,7.2277966 - 89: -5.675968,10.1320915 - - node: - color: '#FFFFFFFF' - id: grasssnowc3 - decals: - 28: 15.694311,15.172535 - 34: 10.48813,7.3443623 - 37: 9.0946245,2.0440664 - 43: -2.8895664,16.211998 - 46: -4.0458164,7.6303444 - 59: 0.45371246,11.270374 - 60: 9.166802,10.222887 - 66: -1.9470882,14.409473 - 77: 6.122473,18.001226 - 80: 18.166714,4.631343 - - node: - color: '#AB0000FF' - id: i - decals: - 374: 21.677881,2.8381848 - - node: - color: '#AB0000FF' - id: k - decals: - 377: 23.427881,2.8069348 - - node: - color: '#AB0000FF' - id: n - decals: - 372: 20.943506,2.9006848 - 378: 22.959131,2.9631848 - - node: - color: '#AB0000FF' - id: r - decals: - 375: 22.099756,2.7913098 - - node: - color: '#AB0000FF' - id: s - decals: - 370: 20.224756,2.9788098 - - node: - color: '#AB0000FF' - id: shortline - decals: - 376: 22.552881,2.8381848 - - node: - color: '#AB0000FF' - id: y - decals: - 371: 20.568506,2.9319348 - - type: GridAtmosphere - version: 2 - data: - tiles: - -1,-2: - 0: 65535 - -1,-1: - 0: 65503 - 1: 32 - 0,-1: - 0: 65535 - -2,-4: - 0: 65535 - -2,-3: - 0: 65535 - -2,-2: - 0: 65535 - -2,-1: - 0: 65535 - -1,-4: - 0: 65535 - -1,-3: - 0: 65535 - 0,-4: - 0: 65535 - 0,-3: - 0: 65535 - 0,-2: - 0: 65535 - 1,-4: - 0: 57343 - 1: 8192 - 1,-3: - 0: 65535 - 1,-2: - 0: 65535 - 1,-1: - 0: 65535 - 2,-4: - 0: 62815 - 1: 2720 - 2,-3: - 0: 65535 - 2,-2: - 0: 32767 - 1: 32768 - 2,-1: - 0: 65533 - 1: 2 - 3,-4: - 0: 64991 - 1: 544 - 3,-3: - 0: 65535 - 3,-2: - 0: 57343 - 1: 8192 - 3,-1: - 0: 65535 - 0,0: - 0: 65535 - 0,1: - 0: 65535 - 0,2: - 0: 65535 - 0,3: - 0: 65535 - 1,0: - 0: 65535 - 1,1: - 0: 65535 - 1,2: - 0: 65535 - 1,3: - 0: 65535 - 2,0: - 0: 65535 - 2,1: - 0: 65535 - 2,2: - 0: 65535 - 2,3: - 0: 65535 - 3,0: - 0: 65535 - 3,1: - 0: 65535 - 3,2: - 0: 65535 - 3,3: - 0: 65535 - -2,0: - 0: 65535 - -2,1: - 0: 65535 - -2,2: - 0: 65535 - -2,3: - 0: 65535 - -1,0: - 0: 65535 - -1,1: - 0: 65535 - -1,2: - 0: 65535 - -1,3: - 0: 65535 - 4,-4: - 0: 65535 - 4,-3: - 0: 65535 - 4,-2: - 0: 65535 - 4,-1: - 0: 65535 - 5,-4: - 0: 65519 - 1: 16 - 5,-3: - 0: 65535 - 5,-2: - 0: 65535 - 5,-1: - 0: 65535 - 6,-4: - 0: 65519 - 1: 16 - 6,-3: - 0: 65535 - 6,-2: - 0: 65535 - 6,-1: - 0: 65535 - 7,-4: - 0: 65535 - 7,-3: - 0: 65535 - 7,-2: - 0: 65535 - 7,-1: - 0: 65535 - 0,-6: - 0: 65535 - 0,-5: - 0: 65535 - 1,-6: - 0: 65535 - 1,-5: - 0: 62463 - 1: 3072 - 2,-6: - 0: 65535 - 2,-5: - 0: 65535 - 3,-6: - 0: 65535 - 3,-5: - 0: 65535 - -2,-6: - 0: 65535 - -2,-5: - 0: 65535 - -1,-6: - 0: 65535 - -1,-5: - 0: 65535 - -2,4: - 0: 65535 - -1,4: - 0: 65535 - 0,4: - 0: 65535 - 1,4: - 0: 65535 - 2,4: - 0: 65535 - 3,4: - 0: 65535 - 4,4: - 0: 65535 - 4,0: - 0: 65535 - 4,1: - 0: 65535 - 4,2: - 0: 65535 - 4,3: - 0: 65535 - 5,0: - 0: 65535 - 6,0: - 0: 65535 - 7,0: - 0: 65535 - 4,-6: - 0: 65535 - 4,-5: - 0: 65535 - 5,-6: - 0: 65535 - 5,-5: - 0: 65535 - 6,-6: - 0: 65535 - 6,-5: - 0: 65535 - 7,-6: - 0: 65535 - 7,-5: - 0: 65535 - -4,-4: - 0: 65535 - -4,-3: - 0: 4095 - -3,-4: - 0: 65535 - -3,-3: - 0: 61439 - -3,-2: - 0: 61166 - -3,-1: - 0: 61166 - -4,0: - 0: 65520 - 2: 15 - -4,1: - 0: 65535 - -4,2: - 0: 65535 - -4,3: - 0: 65535 - -3,0: - 0: 65534 - 2: 1 - -3,1: - 0: 65535 - -3,2: - 0: 65535 - -3,3: - 0: 65535 - 0,-7: - 0: 65535 - 1,-7: - 0: 61441 - 2: 3840 - 2,-7: - 0: 4096 - 2: 57600 - 3,-7: - 0: 32768 - 2: 28672 - -4,-7: - 0: 65535 - -4,-6: - 0: 65535 - -4,-5: - 0: 65535 - -3,-7: - 0: 65535 - -3,-6: - 0: 65535 - -3,-5: - 0: 65535 - -1,-7: - 0: 65535 - -4,4: - 0: 65535 - -4,5: - 0: 65535 - -3,4: - 0: 65535 - -3,5: - 0: 65535 - -2,5: - 0: 65535 - -2,6: - 0: 65535 - -1,5: - 0: 65535 - -1,6: - 0: 13107 - 0,5: - 0: 8191 - 1,5: - 0: 4095 - 2,5: - 0: 65535 - 3,5: - 0: 65535 - 4,5: - 0: 65535 - 5,4: - 0: 65535 - 5,5: - 0: 30719 - 6,4: - 0: 4915 - 5,1: - 0: 65535 - 5,2: - 0: 65535 - 5,3: - 0: 65535 - 6,1: - 0: 65535 - 6,2: - 0: 30719 - 6,3: - 0: 13107 - 7,1: - 0: 16383 - 7,2: - 0: 51 - 4,-7: - 0: 61440 - 5,-7: - 0: 61440 - 6,-7: - 0: 61440 - 7,-7: - 0: 28672 - -8,-4: - 0: 12 - -7,-4: - 0: 32783 - -6,-4: - 0: 61679 - -5,-4: - 0: 65535 - -5,-3: - 0: 4095 - -6,3: - 0: 65534 - -6,2: - 0: 52428 - -5,0: - 0: 65520 - 2: 15 - -5,1: - 0: 65535 - -5,2: - 0: 65535 - -5,3: - 0: 65535 - -7,7: - 0: 52424 - -7,5: - 0: 34944 - -7,6: - 0: 34952 - -6,4: - 0: 61183 - -6,5: - 0: 65535 - -6,6: - 0: 65535 - -6,7: - 0: 65535 - -5,4: - 0: 65535 - -5,5: - 0: 65535 - -8,-6: - 0: 65535 - -8,-5: - 0: 57343 - -7,-5: - 0: 65535 - -7,-6: - 0: 65535 - -6,-6: - 0: 65535 - -6,-5: - 0: 65535 - -6,-7: - 0: 64704 - -5,-7: - 0: 65528 - -5,-6: - 0: 65535 - -5,-5: - 0: 65535 - 8,0: - 0: 17 - 8,-4: - 0: 13107 - 8,-1: - 0: 4369 - 8,-6: - 0: 13072 - 8,-5: - 0: 13107 - -7,8: - 0: 12 - -6,8: - 0: 3 - -12,-6: - 0: 65535 - -12,-5: - 0: 65535 - -11,-6: - 0: 65533 - -11,-5: - 0: 65535 - -10,-6: - 0: 65535 - -10,-5: - 0: 65535 - -9,-6: - 0: 65535 - -9,-5: - 0: 65535 - -15,-4: - 0: 65535 - -15,-3: - 0: 65535 - -15,-2: - 0: 65535 - -15,-1: - 0: 65535 - -14,-4: - 0: 65535 - -14,-3: - 0: 29495 - -14,-2: - 0: 30583 - -14,-1: - 0: 13175 - -13,-4: - 0: 4607 - -12,-4: - 0: 255 - -11,-4: - 0: 255 - -10,-4: - 0: 255 - -9,-4: - 0: 119 - -15,-5: - 0: 65535 - -14,-5: - 0: 65535 - -14,-6: - 0: 65535 - -13,-6: - 0: 65535 - -13,-5: - 0: 65535 - -15,0: - 0: 65518 - -15,1: - 0: 65535 - -15,2: - 0: 65535 - -15,3: - 0: 65535 - -14,0: - 0: 13171 - -14,1: - 0: 13075 - -14,2: - 0: 13107 - -14,3: - 0: 29491 - -15,4: - 0: 65535 - -15,5: - 0: 65535 - -15,6: - 0: 35007 - -14,4: - 0: 65535 - -14,5: - 0: 65535 - -14,6: - 0: 16383 - -2,-7: - 0: 65535 - -4,6: - 0: 65535 - -4,7: - 0: 4095 - -3,6: - 0: 65535 - -3,7: - 0: 511 - -2,7: - 0: 255 - -1,7: - 0: 51 - 2,6: - 0: 52428 - 2,7: - 0: 52428 - 3,6: - 0: 65535 - 3,7: - 0: 65535 - 4,6: - 0: 65535 - 4,7: - 0: 65535 - 5,6: - 0: 13107 - 5,7: - 0: 4915 - -7,-3: - 0: 2184 - -6,-3: - 0: 4095 - -7,0: - 0: 65520 - 2: 15 - -7,1: - 0: 15 - -6,0: - 0: 65520 - 2: 15 - -6,1: - 0: 15 - 2: 51328 - -5,6: - 0: 65535 - -5,7: - 0: 32767 - -8,-7: - 0: 63744 - -7,-7: - 0: 65407 - -12,-7: - 0: 65519 - -11,-7: - 0: 4983 - -10,-7: - 0: 60622 - -9,-7: - 0: 65535 - -16,-4: - 0: 65535 - -16,-3: - 0: 65535 - -16,-2: - 0: 65535 - -16,-1: - 0: 65535 - -16,-7: - 0: 13107 - -16,-6: - 0: 65535 - -16,-5: - 0: 65535 - -15,-6: - 0: 65535 - -15,-7: - 0: 52360 - -14,-7: - 0: 65535 - -13,-7: - 0: 63251 - -16,0: - 0: 65535 - -16,1: - 0: 65535 - -16,2: - 0: 65535 - -16,3: - 0: 65535 - -16,4: - 0: 65535 - -16,5: - 0: 65535 - -16,6: - 0: 255 - -18,0: - 0: 65535 - -18,3: - 0: 61440 - -18,1: - 0: 34956 - -17,0: - 0: 65535 - -17,1: - 0: 65535 - -17,2: - 0: 65519 - -17,3: - 0: 65535 - -18,4: - 0: 52991 - -18,5: - 0: 65532 - -18,6: - 0: 3823 - -17,4: - 0: 65535 - -17,5: - 0: 65535 - -17,6: - 0: 4095 - -18,-4: - 0: 65535 - -18,-3: - 0: 52479 - -18,-1: - 0: 65484 - -18,-2: - 0: 52428 - -17,-4: - 0: 65535 - -17,-3: - 0: 65535 - -17,-2: - 0: 65535 - -17,-1: - 0: 65535 - -18,-6: - 0: 65484 - -18,-5: - 0: 65535 - -18,-7: - 0: 52224 - -17,-7: - 0: 65484 - -17,-6: - 0: 65535 - -17,-5: - 0: 65535 - 2,8: - 0: 12 - 3,8: - 0: 15 - 4,8: - 0: 15 - 5,8: - 0: 1 - -8,0: - 0: 65520 - 2: 15 - -8,1: - 0: 15 - -9,0: - 0: 61152 - 2: 14 - -9,1: - 0: 14 - uniqueMixes: - - volume: 2500 - temperature: 293.15 - moles: - - 21.824879 - - 82.10312 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.15 - moles: - - 21.213781 - - 79.80423 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.15 - moles: - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - chunkSize: 4 - - type: RadiationGridResistance - - type: OccluderTree - - type: Shuttle - - type: GravityShake - shakeTimes: 10 - - type: GasTileOverlay - - type: SpreaderGrid - - type: GridPathfinding - - type: BecomesStation - id: SyndicateOutpost - - uid: 1295 - components: - - type: MetaData - - type: Transform - - type: Map - - type: PhysicsMap - - type: Broadphase - - type: OccluderTree - - type: Parallax - parallax: Sky - - type: MapAtmosphere - space: False - mixture: - volume: 2500 - temperature: 248.15 - moles: - - 21.824879 - - 82.10312 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - type: LoadedMap - - type: GridTree - - type: MovedGrids -- proto: AirlockExternalGlassNukeopLocked - entities: - - uid: 49 - components: - - type: Transform - pos: 3.5,1.5 - parent: 104 - - uid: 55 - components: - - type: Transform - pos: -1.5,-3.5 - parent: 104 - - uid: 56 - components: - - type: Transform - pos: -5.5,-3.5 - parent: 104 - - uid: 58 - components: - - type: Transform - pos: 3.5,3.5 - parent: 104 -- proto: AirlockGlassShuttle - entities: - - uid: 2053 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -33.5,-19.5 - parent: 104 - - uid: 2057 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -29.5,-19.5 - parent: 104 - - uid: 2062 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -25.5,-19.5 - parent: 104 - - uid: 2185 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -21.5,-19.5 - parent: 104 - - uid: 2186 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -17.5,-19.5 - parent: 104 - - uid: 2187 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -13.5,-19.5 - parent: 104 - - uid: 2190 - components: - - type: Transform - pos: -13.5,0.5 - parent: 104 - - uid: 2191 - components: - - type: Transform - pos: -17.5,0.5 - parent: 104 - - uid: 2193 - components: - - type: Transform - pos: -21.5,0.5 - parent: 104 - - uid: 2448 - components: - - type: Transform - pos: -25.5,0.5 - parent: 104 - - uid: 2472 - components: - - type: Transform - pos: -29.5,0.5 - parent: 104 - - type: GridFill - addComponents: - - type: NukeOpsShuttle - path: /Maps/Shuttles/infiltrator.yml - - uid: 2481 - components: - - type: Transform - pos: -33.5,0.5 - parent: 104 -- proto: AirlockSyndicateNukeopGlassLocked - entities: - - uid: 48 - components: - - type: Transform - pos: -11.5,6.5 - parent: 104 - - uid: 65 - components: - - type: Transform - pos: 2.5,-14.5 - parent: 104 - - uid: 96 - components: - - type: Transform - pos: 8.5,-16.5 - parent: 104 - - uid: 112 - components: - - type: Transform - pos: 16.5,-7.5 - parent: 104 - - uid: 122 - components: - - type: Transform - pos: 9.5,-9.5 - parent: 104 - - uid: 130 - components: - - type: Transform - pos: 11.5,-7.5 - parent: 104 - - uid: 131 - components: - - type: Transform - pos: 8.5,-0.5 - parent: 104 - - uid: 165 - components: - - type: Transform - pos: 10.5,-2.5 - parent: 104 - - uid: 190 - components: - - type: Transform - pos: 16.5,-13.5 - parent: 104 - - uid: 302 - components: - - type: Transform - pos: 14.5,-5.5 - parent: 104 - - uid: 305 - components: - - type: Transform - pos: 6.5,-9.5 - parent: 104 -- proto: AirlockSyndicateNukeopLocked - entities: - - uid: 109 - components: - - type: Transform - pos: 14.5,-1.5 - parent: 104 - - uid: 120 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 23.5,-7.5 - parent: 104 - - uid: 136 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-7.5 - parent: 104 - - uid: 148 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 20.5,-10.5 - parent: 104 - - uid: 272 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 20.5,-4.5 - parent: 104 -- proto: AlwaysPoweredLightExterior - entities: - - uid: 1267 - components: - - type: Transform - pos: -15.5,23.5 - parent: 104 -- proto: AlwaysPoweredLightLED - entities: - - uid: 135 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -6.5,-1.5 - parent: 104 - - uid: 974 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-17.5 - parent: 104 - - uid: 1286 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -6.5,-5.5 - parent: 104 - - uid: 1287 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -6.5,-11.5 - parent: 104 - - uid: 1288 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -6.5,-20.5 - parent: 104 -- proto: AlwaysPoweredWallLight - entities: - - uid: 258 - components: - - type: Transform - pos: -3.5,-2.5 - parent: 104 - - uid: 1356 - components: - - type: Transform - pos: 1.5,-12.5 - parent: 104 - - uid: 1470 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 12.5,-14.5 - parent: 104 - - uid: 1471 - components: - - type: Transform - pos: 12.5,-10.5 - parent: 104 - - uid: 1532 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 10.5,-8.5 - parent: 104 - - uid: 1534 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-1.5 - parent: 104 - - uid: 1536 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-2.5 - parent: 104 - - uid: 1537 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-8.5 - parent: 104 - - uid: 1544 - components: - - type: Transform - pos: 15.5,-6.5 - parent: 104 - - uid: 1545 - components: - - type: Transform - pos: 14.5,-3.5 - parent: 104 - - uid: 1546 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 10.5,-4.5 - parent: 104 - - uid: 1570 - components: - - type: Transform - pos: 19.5,-5.5 - parent: 104 - - uid: 1571 - components: - - type: Transform - pos: 21.5,-5.5 - parent: 104 - - uid: 1572 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 19.5,-9.5 - parent: 104 - - uid: 1573 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 21.5,-9.5 - parent: 104 - - uid: 1758 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -10.5,5.5 - parent: 104 - - uid: 1759 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,12.5 - parent: 104 - - uid: 1760 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 14.5,-22.5 - parent: 104 - - uid: 1763 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 10.5,-22.5 - parent: 104 - - uid: 1867 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -2.5,-16.5 - parent: 104 - - uid: 2257 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,10.5 - parent: 104 - - uid: 2291 - components: - - type: Transform - pos: 5.5,14.5 - parent: 104 - - uid: 2397 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 24.5,-11.5 - parent: 104 - - uid: 2398 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 24.5,-3.5 - parent: 104 - - uid: 2399 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-3.5 - parent: 104 - - uid: 2400 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-11.5 - parent: 104 - - uid: 2401 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-15.5 - parent: 104 - - uid: 2402 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 27.5,-15.5 - parent: 104 - - uid: 2403 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 25.5,-19.5 - parent: 104 - - uid: 2404 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 19.5,-19.5 - parent: 104 - - uid: 3375 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 3.5,-11.5 - parent: 104 - - uid: 3377 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-11.5 - parent: 104 -- proto: APCHyperCapacity - entities: - - uid: 1431 - components: - - type: Transform - pos: 20.5,-12.5 - parent: 104 - - uid: 1445 - components: - - type: Transform - pos: 13.5,-9.5 - parent: 104 - - uid: 1477 - components: - - type: Transform - pos: 8.5,-5.5 - parent: 104 - - uid: 1574 - components: - - type: Transform - pos: 21.5,-4.5 - parent: 104 -- proto: AsteroidRock - entities: - - uid: 19 - components: - - type: Transform - pos: 16.5,1.5 - parent: 104 - - uid: 20 - components: - - type: Transform - pos: -3.5,-10.5 - parent: 104 - - uid: 216 - components: - - type: Transform - pos: -4.5,-9.5 - parent: 104 - - uid: 238 - components: - - type: Transform - pos: -4.5,-13.5 - parent: 104 - - uid: 243 - components: - - type: Transform - pos: -4.5,-6.5 - parent: 104 - - uid: 244 - components: - - type: Transform - pos: -4.5,-8.5 - parent: 104 - - uid: 246 - components: - - type: Transform - pos: -3.5,-8.5 - parent: 104 - - uid: 247 - components: - - type: Transform - pos: -4.5,-12.5 - parent: 104 - - uid: 248 - components: - - type: Transform - pos: -4.5,-7.5 - parent: 104 - - uid: 249 - components: - - type: Transform - pos: -8.5,9.5 - parent: 104 - - uid: 250 - components: - - type: Transform - pos: 0.5,-10.5 - parent: 104 - - uid: 251 - components: - - type: Transform - pos: 15.5,1.5 - parent: 104 - - uid: 255 - components: - - type: Transform - pos: -3.5,-7.5 - parent: 104 - - uid: 257 - components: - - type: Transform - pos: 15.5,0.5 - parent: 104 - - uid: 259 - components: - - type: Transform - pos: -4.5,-10.5 - parent: 104 - - uid: 263 - components: - - type: Transform - pos: -4.5,-11.5 - parent: 104 - - uid: 277 - components: - - type: Transform - pos: -10.5,22.5 - parent: 104 - - uid: 292 - components: - - type: Transform - pos: -9.5,21.5 - parent: 104 - - uid: 294 - components: - - type: Transform - pos: -10.5,21.5 - parent: 104 - - uid: 295 - components: - - type: Transform - pos: -11.5,22.5 - parent: 104 - - uid: 296 - components: - - type: Transform - pos: -20.5,9.5 - parent: 104 - - uid: 358 - components: - - type: Transform - pos: -22.5,21.5 - parent: 104 - - uid: 359 - components: - - type: Transform - pos: -22.5,15.5 - parent: 104 - - uid: 360 - components: - - type: Transform - pos: -22.5,16.5 - parent: 104 - - uid: 361 - components: - - type: Transform - pos: -21.5,17.5 - parent: 104 - - uid: 362 - components: - - type: Transform - pos: -21.5,16.5 - parent: 104 - - uid: 363 - components: - - type: Transform - pos: -21.5,15.5 - parent: 104 - - uid: 364 - components: - - type: Transform - pos: -21.5,14.5 - parent: 104 - - uid: 365 - components: - - type: Transform - pos: -22.5,14.5 - parent: 104 - - uid: 366 - components: - - type: Transform - pos: -21.5,13.5 - parent: 104 - - uid: 367 - components: - - type: Transform - pos: -21.5,20.5 - parent: 104 - - uid: 368 - components: - - type: Transform - pos: -21.5,21.5 - parent: 104 - - uid: 369 - components: - - type: Transform - pos: -20.5,21.5 - parent: 104 - - uid: 370 - components: - - type: Transform - pos: -19.5,21.5 - parent: 104 - - uid: 371 - components: - - type: Transform - pos: -20.5,22.5 - parent: 104 - - uid: 372 - components: - - type: Transform - pos: -18.5,21.5 - parent: 104 - - uid: 373 - components: - - type: Transform - pos: -20.5,8.5 - parent: 104 - - uid: 374 - components: - - type: Transform - pos: -22.5,22.5 - parent: 104 - - uid: 375 - components: - - type: Transform - pos: -21.5,22.5 - parent: 104 - - uid: 376 - components: - - type: Transform - pos: -23.5,22.5 - parent: 104 - - uid: 377 - components: - - type: Transform - pos: -20.5,15.5 - parent: 104 - - uid: 401 - components: - - type: Transform - pos: -20.5,20.5 - parent: 104 - - uid: 402 - components: - - type: Transform - pos: -20.5,19.5 - parent: 104 - - uid: 403 - components: - - type: Transform - pos: -20.5,18.5 - parent: 104 - - uid: 404 - components: - - type: Transform - pos: -20.5,17.5 - parent: 104 - - uid: 405 - components: - - type: Transform - pos: -20.5,16.5 - parent: 104 - - uid: 406 - components: - - type: Transform - pos: -20.5,14.5 - parent: 104 - - uid: 407 - components: - - type: Transform - pos: -24.5,23.5 - parent: 104 - - uid: 408 - components: - - type: Transform - pos: -24.5,22.5 - parent: 104 - - uid: 409 - components: - - type: Transform - pos: -20.5,10.5 - parent: 104 - - uid: 410 - components: - - type: Transform - pos: -9.5,22.5 - parent: 104 - - uid: 415 - components: - - type: Transform - pos: -18.5,23.5 - parent: 104 - - uid: 417 - components: - - type: Transform - pos: -16.5,6.5 - parent: 104 - - uid: 419 - components: - - type: Transform - pos: -7.5,21.5 - parent: 104 - - uid: 420 - components: - - type: Transform - pos: -8.5,21.5 - parent: 104 - - uid: 421 - components: - - type: Transform - pos: -10.5,23.5 - parent: 104 - - uid: 422 - components: - - type: Transform - pos: -11.5,23.5 - parent: 104 - - uid: 423 - components: - - type: Transform - pos: -19.5,22.5 - parent: 104 - - uid: 425 - components: - - type: Transform - pos: -18.5,22.5 - parent: 104 - - uid: 426 - components: - - type: Transform - pos: -7.5,12.5 - parent: 104 - - uid: 455 - components: - - type: Transform - pos: 1.5,-19.5 - parent: 104 - - uid: 469 - components: - - type: Transform - pos: -8.5,10.5 - parent: 104 - - uid: 471 - components: - - type: Transform - pos: -9.5,10.5 - parent: 104 - - uid: 473 - components: - - type: Transform - pos: -8.5,11.5 - parent: 104 - - uid: 475 - components: - - type: Transform - pos: -9.5,11.5 - parent: 104 - - uid: 477 - components: - - type: Transform - pos: 16.5,0.5 - parent: 104 - - uid: 478 - components: - - type: Transform - pos: -6.5,12.5 - parent: 104 - - uid: 479 - components: - - type: Transform - pos: -2.5,-18.5 - parent: 104 - - uid: 480 - components: - - type: Transform - pos: -2.5,-19.5 - parent: 104 - - uid: 483 - components: - - type: Transform - pos: -2.5,-10.5 - parent: 104 - - uid: 485 - components: - - type: Transform - pos: -2.5,-6.5 - parent: 104 - - uid: 488 - components: - - type: Transform - pos: -2.5,-7.5 - parent: 104 - - uid: 489 - components: - - type: Transform - pos: -2.5,-8.5 - parent: 104 - - uid: 492 - components: - - type: Transform - pos: -3.5,-20.5 - parent: 104 - - uid: 502 - components: - - type: Transform - pos: -3.5,-18.5 - parent: 104 - - uid: 504 - components: - - type: Transform - pos: -2.5,-9.5 - parent: 104 - - uid: 505 - components: - - type: Transform - pos: -3.5,-19.5 - parent: 104 - - uid: 506 - components: - - type: Transform - pos: 17.5,-1.5 - parent: 104 - - uid: 509 - components: - - type: Transform - pos: -4.5,-19.5 - parent: 104 - - uid: 511 - components: - - type: Transform - pos: -4.5,-18.5 - parent: 104 - - uid: 512 - components: - - type: Transform - pos: -9.5,12.5 - parent: 104 - - uid: 513 - components: - - type: Transform - pos: -3.5,-9.5 - parent: 104 - - uid: 521 - components: - - type: Transform - pos: -4.5,-14.5 - parent: 104 - - uid: 522 - components: - - type: Transform - pos: -1.5,19.5 - parent: 104 - - uid: 524 - components: - - type: Transform - pos: -0.5,19.5 - parent: 104 - - uid: 525 - components: - - type: Transform - pos: 9.5,20.5 - parent: 104 - - uid: 529 - components: - - type: Transform - pos: 10.5,19.5 - parent: 104 - - uid: 531 - components: - - type: Transform - pos: 11.5,19.5 - parent: 104 - - uid: 532 - components: - - type: Transform - pos: 12.5,19.5 - parent: 104 - - uid: 533 - components: - - type: Transform - pos: 16.5,19.5 - parent: 104 - - uid: 534 - components: - - type: Transform - pos: 17.5,19.5 - parent: 104 - - uid: 535 - components: - - type: Transform - pos: 18.5,19.5 - parent: 104 - - uid: 536 - components: - - type: Transform - pos: 18.5,11.5 - parent: 104 - - uid: 537 - components: - - type: Transform - pos: 18.5,12.5 - parent: 104 - - uid: 538 - components: - - type: Transform - pos: 18.5,15.5 - parent: 104 - - uid: 539 - components: - - type: Transform - pos: 18.5,18.5 - parent: 104 - - uid: 543 - components: - - type: Transform - pos: 26.5,9.5 - parent: 104 - - uid: 546 - components: - - type: Transform - pos: 28.5,3.5 - parent: 104 - - uid: 547 - components: - - type: Transform - pos: 29.5,3.5 - parent: 104 - - uid: 548 - components: - - type: Transform - pos: 30.5,3.5 - parent: 104 - - uid: 549 - components: - - type: Transform - pos: 22.5,-22.5 - parent: 104 - - uid: 550 - components: - - type: Transform - pos: 31.5,-20.5 - parent: 104 - - uid: 551 - components: - - type: Transform - pos: 31.5,-19.5 - parent: 104 - - uid: 552 - components: - - type: Transform - pos: 31.5,-18.5 - parent: 104 - - uid: 553 - components: - - type: Transform - pos: 31.5,-17.5 - parent: 104 - - uid: 554 - components: - - type: Transform - pos: 31.5,-16.5 - parent: 104 - - uid: 555 - components: - - type: Transform - pos: 31.5,-15.5 - parent: 104 - - uid: 556 - components: - - type: Transform - pos: 31.5,-14.5 - parent: 104 - - uid: 557 - components: - - type: Transform - pos: 31.5,-13.5 - parent: 104 - - uid: 558 - components: - - type: Transform - pos: 31.5,-12.5 - parent: 104 - - uid: 559 - components: - - type: Transform - pos: 31.5,-7.5 - parent: 104 - - uid: 560 - components: - - type: Transform - pos: 31.5,-6.5 - parent: 104 - - uid: 561 - components: - - type: Transform - pos: 31.5,-5.5 - parent: 104 - - uid: 562 - components: - - type: Transform - pos: 31.5,-4.5 - parent: 104 - - uid: 563 - components: - - type: Transform - pos: 31.5,-3.5 - parent: 104 - - uid: 564 - components: - - type: Transform - pos: 31.5,-2.5 - parent: 104 - - uid: 565 - components: - - type: Transform - pos: 31.5,-1.5 - parent: 104 - - uid: 566 - components: - - type: Transform - pos: 31.5,-0.5 - parent: 104 - - uid: 567 - components: - - type: Transform - pos: 31.5,0.5 - parent: 104 - - uid: 568 - components: - - type: Transform - pos: 31.5,1.5 - parent: 104 - - uid: 569 - components: - - type: Transform - pos: 17.5,-22.5 - parent: 104 - - uid: 570 - components: - - type: Transform - pos: 18.5,-22.5 - parent: 104 - - uid: 571 - components: - - type: Transform - pos: 19.5,-22.5 - parent: 104 - - uid: 572 - components: - - type: Transform - pos: 20.5,-22.5 - parent: 104 - - uid: 573 - components: - - type: Transform - pos: 21.5,-22.5 - parent: 104 - - uid: 574 - components: - - type: Transform - pos: -5.5,-22.5 - parent: 104 - - uid: 575 - components: - - type: Transform - pos: 23.5,-22.5 - parent: 104 - - uid: 576 - components: - - type: Transform - pos: 24.5,-22.5 - parent: 104 - - uid: 577 - components: - - type: Transform - pos: 25.5,-22.5 - parent: 104 - - uid: 578 - components: - - type: Transform - pos: 26.5,-22.5 - parent: 104 - - uid: 579 - components: - - type: Transform - pos: 27.5,-22.5 - parent: 104 - - uid: 580 - components: - - type: Transform - pos: 28.5,-22.5 - parent: 104 - - uid: 581 - components: - - type: Transform - pos: -4.5,-20.5 - parent: 104 - - uid: 582 - components: - - type: Transform - pos: -4.5,-15.5 - parent: 104 - - uid: 586 - components: - - type: Transform - pos: 4.5,-22.5 - parent: 104 - - uid: 587 - components: - - type: Transform - pos: 5.5,-22.5 - parent: 104 - - uid: 588 - components: - - type: Transform - pos: 6.5,-22.5 - parent: 104 - - uid: 589 - components: - - type: Transform - pos: 7.5,-22.5 - parent: 104 - - uid: 590 - components: - - type: Transform - pos: 8.5,-22.5 - parent: 104 - - uid: 595 - components: - - type: Transform - pos: -5.5,-21.5 - parent: 104 - - uid: 596 - components: - - type: Transform - pos: -5.5,-20.5 - parent: 104 - - uid: 597 - components: - - type: Transform - pos: -5.5,-19.5 - parent: 104 - - uid: 598 - components: - - type: Transform - pos: -5.5,-18.5 - parent: 104 - - uid: 599 - components: - - type: Transform - pos: -5.5,-17.5 - parent: 104 - - uid: 600 - components: - - type: Transform - pos: -5.5,-16.5 - parent: 104 - - uid: 601 - components: - - type: Transform - pos: -5.5,-15.5 - parent: 104 - - uid: 602 - components: - - type: Transform - pos: -5.5,-14.5 - parent: 104 - - uid: 603 - components: - - type: Transform - pos: -5.5,-13.5 - parent: 104 - - uid: 604 - components: - - type: Transform - pos: -5.5,-12.5 - parent: 104 - - uid: 605 - components: - - type: Transform - pos: -5.5,-11.5 - parent: 104 - - uid: 606 - components: - - type: Transform - pos: -5.5,-10.5 - parent: 104 - - uid: 607 - components: - - type: Transform - pos: -5.5,-9.5 - parent: 104 - - uid: 608 - components: - - type: Transform - pos: -5.5,-8.5 - parent: 104 - - uid: 609 - components: - - type: Transform - pos: -5.5,-7.5 - parent: 104 - - uid: 610 - components: - - type: Transform - pos: -14.5,16.5 - parent: 104 - - uid: 614 - components: - - type: Transform - pos: -16.5,15.5 - parent: 104 - - uid: 615 - components: - - type: Transform - pos: -10.5,9.5 - parent: 104 - - uid: 618 - components: - - type: Transform - pos: -8.5,12.5 - parent: 104 - - uid: 621 - components: - - type: Transform - pos: -9.5,9.5 - parent: 104 - - uid: 622 - components: - - type: Transform - pos: -10.5,10.5 - parent: 104 - - uid: 632 - components: - - type: Transform - pos: -10.5,12.5 - parent: 104 - - uid: 637 - components: - - type: Transform - pos: -10.5,11.5 - parent: 104 - - uid: 640 - components: - - type: Transform - pos: -16.5,5.5 - parent: 104 - - uid: 648 - components: - - type: Transform - pos: -11.5,13.5 - parent: 104 - - uid: 670 - components: - - type: Transform - pos: -18.5,10.5 - parent: 104 - - uid: 672 - components: - - type: Transform - pos: -16.5,16.5 - parent: 104 - - uid: 673 - components: - - type: Transform - pos: -11.5,12.5 - parent: 104 - - uid: 674 - components: - - type: Transform - pos: -19.5,15.5 - parent: 104 - - uid: 676 - components: - - type: Transform - pos: -19.5,16.5 - parent: 104 - - uid: 677 - components: - - type: Transform - pos: -18.5,11.5 - parent: 104 - - uid: 678 - components: - - type: Transform - pos: -14.5,11.5 - parent: 104 - - uid: 679 - components: - - type: Transform - pos: -14.5,17.5 - parent: 104 - - uid: 680 - components: - - type: Transform - pos: -14.5,18.5 - parent: 104 - - uid: 681 - components: - - type: Transform - pos: -15.5,14.5 - parent: 104 - - uid: 682 - components: - - type: Transform - pos: -24.5,25.5 - parent: 104 - - uid: 684 - components: - - type: Transform - pos: -17.5,13.5 - parent: 104 - - uid: 685 - components: - - type: Transform - pos: -15.5,16.5 - parent: 104 - - uid: 686 - components: - - type: Transform - pos: -12.5,12.5 - parent: 104 - - uid: 687 - components: - - type: Transform - pos: -12.5,11.5 - parent: 104 - - uid: 688 - components: - - type: Transform - pos: -11.5,14.5 - parent: 104 - - uid: 689 - components: - - type: Transform - pos: -15.5,15.5 - parent: 104 - - uid: 690 - components: - - type: Transform - pos: -14.5,14.5 - parent: 104 - - uid: 691 - components: - - type: Transform - pos: -19.5,14.5 - parent: 104 - - uid: 692 - components: - - type: Transform - pos: -13.5,16.5 - parent: 104 - - uid: 693 - components: - - type: Transform - pos: -17.5,15.5 - parent: 104 - - uid: 694 - components: - - type: Transform - pos: -11.5,9.5 - parent: 104 - - uid: 695 - components: - - type: Transform - pos: -20.5,23.5 - parent: 104 - - uid: 699 - components: - - type: Transform - pos: -18.5,8.5 - parent: 104 - - uid: 700 - components: - - type: Transform - pos: -18.5,9.5 - parent: 104 - - uid: 701 - components: - - type: Transform - pos: -13.5,12.5 - parent: 104 - - uid: 702 - components: - - type: Transform - pos: -13.5,17.5 - parent: 104 - - uid: 703 - components: - - type: Transform - pos: -13.5,18.5 - parent: 104 - - uid: 704 - components: - - type: Transform - pos: -15.5,18.5 - parent: 104 - - uid: 705 - components: - - type: Transform - pos: -17.5,14.5 - parent: 104 - - uid: 706 - components: - - type: Transform - pos: -11.5,11.5 - parent: 104 - - uid: 708 - components: - - type: Transform - pos: -17.5,11.5 - parent: 104 - - uid: 709 - components: - - type: Transform - pos: -17.5,16.5 - parent: 104 - - uid: 711 - components: - - type: Transform - pos: -17.5,10.5 - parent: 104 - - uid: 712 - components: - - type: Transform - pos: -15.5,17.5 - parent: 104 - - uid: 714 - components: - - type: Transform - pos: -11.5,17.5 - parent: 104 - - uid: 716 - components: - - type: Transform - pos: -13.5,13.5 - parent: 104 - - uid: 717 - components: - - type: Transform - pos: -13.5,14.5 - parent: 104 - - uid: 718 - components: - - type: Transform - pos: -18.5,14.5 - parent: 104 - - uid: 719 - components: - - type: Transform - pos: -18.5,15.5 - parent: 104 - - uid: 720 - components: - - type: Transform - pos: -18.5,16.5 - parent: 104 - - uid: 721 - components: - - type: Transform - pos: -17.5,8.5 - parent: 104 - - uid: 722 - components: - - type: Transform - pos: -16.5,13.5 - parent: 104 - - uid: 723 - components: - - type: Transform - pos: -16.5,14.5 - parent: 104 - - uid: 724 - components: - - type: Transform - pos: -12.5,17.5 - parent: 104 - - uid: 725 - components: - - type: Transform - pos: -12.5,18.5 - parent: 104 - - uid: 726 - components: - - type: Transform - pos: -14.5,12.5 - parent: 104 - - uid: 728 - components: - - type: Transform - pos: -14.5,13.5 - parent: 104 - - uid: 729 - components: - - type: Transform - pos: -18.5,17.5 - parent: 104 - - uid: 733 - components: - - type: Transform - pos: -18.5,18.5 - parent: 104 - - uid: 734 - components: - - type: Transform - pos: -18.5,19.5 - parent: 104 - - uid: 735 - components: - - type: Transform - pos: -12.5,13.5 - parent: 104 - - uid: 740 - components: - - type: Transform - pos: -12.5,15.5 - parent: 104 - - uid: 741 - components: - - type: Transform - pos: -19.5,17.5 - parent: 104 - - uid: 745 - components: - - type: Transform - pos: -19.5,18.5 - parent: 104 - - uid: 746 - components: - - type: Transform - pos: -19.5,19.5 - parent: 104 - - uid: 747 - components: - - type: Transform - pos: -16.5,17.5 - parent: 104 - - uid: 752 - components: - - type: Transform - pos: -16.5,18.5 - parent: 104 - - uid: 753 - components: - - type: Transform - pos: -15.5,11.5 - parent: 104 - - uid: 755 - components: - - type: Transform - pos: -15.5,12.5 - parent: 104 - - uid: 756 - components: - - type: Transform - pos: -12.5,14.5 - parent: 104 - - uid: 757 - components: - - type: Transform - pos: -16.5,12.5 - parent: 104 - - uid: 758 - components: - - type: Transform - pos: -13.5,15.5 - parent: 104 - - uid: 759 - components: - - type: Transform - pos: -19.5,9.5 - parent: 104 - - uid: 762 - components: - - type: Transform - pos: -17.5,9.5 - parent: 104 - - uid: 763 - components: - - type: Transform - pos: -17.5,17.5 - parent: 104 - - uid: 764 - components: - - type: Transform - pos: -17.5,18.5 - parent: 104 - - uid: 765 - components: - - type: Transform - pos: -16.5,11.5 - parent: 104 - - uid: 768 - components: - - type: Transform - pos: -19.5,8.5 - parent: 104 - - uid: 771 - components: - - type: Transform - pos: -6.5,13.5 - parent: 104 - - uid: 774 - components: - - type: Transform - pos: -10.5,18.5 - parent: 104 - - uid: 775 - components: - - type: Transform - pos: -10.5,17.5 - parent: 104 - - uid: 776 - components: - - type: Transform - pos: -10.5,16.5 - parent: 104 - - uid: 777 - components: - - type: Transform - pos: -10.5,15.5 - parent: 104 - - uid: 778 - components: - - type: Transform - pos: -10.5,14.5 - parent: 104 - - uid: 779 - components: - - type: Transform - pos: -10.5,13.5 - parent: 104 - - uid: 780 - components: - - type: Transform - pos: -9.5,19.5 - parent: 104 - - uid: 781 - components: - - type: Transform - pos: -9.5,18.5 - parent: 104 - - uid: 782 - components: - - type: Transform - pos: -9.5,17.5 - parent: 104 - - uid: 783 - components: - - type: Transform - pos: -9.5,16.5 - parent: 104 - - uid: 784 - components: - - type: Transform - pos: -9.5,15.5 - parent: 104 - - uid: 785 - components: - - type: Transform - pos: -9.5,14.5 - parent: 104 - - uid: 786 - components: - - type: Transform - pos: -9.5,13.5 - parent: 104 - - uid: 787 - components: - - type: Transform - pos: -8.5,19.5 - parent: 104 - - uid: 788 - components: - - type: Transform - pos: -8.5,18.5 - parent: 104 - - uid: 789 - components: - - type: Transform - pos: -8.5,17.5 - parent: 104 - - uid: 790 - components: - - type: Transform - pos: -8.5,15.5 - parent: 104 - - uid: 791 - components: - - type: Transform - pos: -8.5,14.5 - parent: 104 - - uid: 792 - components: - - type: Transform - pos: -8.5,13.5 - parent: 104 - - uid: 793 - components: - - type: Transform - pos: -7.5,19.5 - parent: 104 - - uid: 794 - components: - - type: Transform - pos: -7.5,18.5 - parent: 104 - - uid: 795 - components: - - type: Transform - pos: -7.5,15.5 - parent: 104 - - uid: 796 - components: - - type: Transform - pos: -7.5,14.5 - parent: 104 - - uid: 797 - components: - - type: Transform - pos: -7.5,13.5 - parent: 104 - - uid: 798 - components: - - type: Transform - pos: -15.5,13.5 - parent: 104 - - uid: 801 - components: - - type: Transform - pos: -12.5,16.5 - parent: 104 - - uid: 802 - components: - - type: Transform - pos: -14.5,15.5 - parent: 104 - - uid: 803 - components: - - type: Transform - pos: -19.5,23.5 - parent: 104 - - uid: 804 - components: - - type: Transform - pos: -24.5,24.5 - parent: 104 - - uid: 805 - components: - - type: Transform - pos: -19.5,10.5 - parent: 104 - - uid: 806 - components: - - type: Transform - pos: -17.5,12.5 - parent: 104 - - uid: 807 - components: - - type: Transform - pos: -11.5,16.5 - parent: 104 - - uid: 808 - components: - - type: Transform - pos: -11.5,15.5 - parent: 104 - - uid: 809 - components: - - type: Transform - pos: -13.5,11.5 - parent: 104 - - uid: 810 - components: - - type: Transform - pos: -19.5,20.5 - parent: 104 - - uid: 1020 - components: - - type: Transform - pos: -18.5,20.5 - parent: 104 - - uid: 1021 - components: - - type: Transform - pos: -10.5,20.5 - parent: 104 - - uid: 1027 - components: - - type: Transform - pos: -9.5,20.5 - parent: 104 - - uid: 1028 - components: - - type: Transform - pos: -8.5,20.5 - parent: 104 - - uid: 1029 - components: - - type: Transform - pos: -7.5,20.5 - parent: 104 - - uid: 1030 - components: - - type: Transform - pos: -2.5,20.5 - parent: 104 - - uid: 1031 - components: - - type: Transform - pos: -1.5,20.5 - parent: 104 - - uid: 1032 - components: - - type: Transform - pos: -0.5,20.5 - parent: 104 - - uid: 1033 - components: - - type: Transform - pos: 9.5,22.5 - parent: 104 - - uid: 1036 - components: - - type: Transform - pos: 10.5,20.5 - parent: 104 - - uid: 1041 - components: - - type: Transform - pos: 11.5,20.5 - parent: 104 - - uid: 1042 - components: - - type: Transform - pos: 12.5,20.5 - parent: 104 - - uid: 1043 - components: - - type: Transform - pos: 13.5,20.5 - parent: 104 - - uid: 1044 - components: - - type: Transform - pos: 16.5,20.5 - parent: 104 - - uid: 1045 - components: - - type: Transform - pos: 17.5,20.5 - parent: 104 - - uid: 1046 - components: - - type: Transform - pos: 18.5,20.5 - parent: 104 - - uid: 1047 - components: - - type: Transform - pos: 19.5,20.5 - parent: 104 - - uid: 1048 - components: - - type: Transform - pos: 19.5,19.5 - parent: 104 - - uid: 1049 - components: - - type: Transform - pos: 19.5,18.5 - parent: 104 - - uid: 1050 - components: - - type: Transform - pos: 19.5,16.5 - parent: 104 - - uid: 1051 - components: - - type: Transform - pos: 19.5,15.5 - parent: 104 - - uid: 1052 - components: - - type: Transform - pos: 19.5,14.5 - parent: 104 - - uid: 1053 - components: - - type: Transform - pos: 19.5,12.5 - parent: 104 - - uid: 1054 - components: - - type: Transform - pos: 19.5,11.5 - parent: 104 - - uid: 1055 - components: - - type: Transform - pos: 19.5,10.5 - parent: 104 - - uid: 1056 - components: - - type: Transform - pos: -11.5,20.5 - parent: 104 - - uid: 1057 - components: - - type: Transform - pos: 15.5,3.5 - parent: 104 - - uid: 1058 - components: - - type: Transform - pos: 22.5,4.5 - parent: 104 - - uid: 1059 - components: - - type: Transform - pos: 23.5,4.5 - parent: 104 - - uid: 1060 - components: - - type: Transform - pos: 24.5,4.5 - parent: 104 - - uid: 1064 - components: - - type: Transform - pos: 28.5,4.5 - parent: 104 - - uid: 1065 - components: - - type: Transform - pos: 29.5,4.5 - parent: 104 - - uid: 1066 - components: - - type: Transform - pos: 30.5,4.5 - parent: 104 - - uid: 1067 - components: - - type: Transform - pos: 32.5,0.5 - parent: 104 - - uid: 1068 - components: - - type: Transform - pos: 32.5,-0.5 - parent: 104 - - uid: 1069 - components: - - type: Transform - pos: 32.5,-3.5 - parent: 104 - - uid: 1070 - components: - - type: Transform - pos: 32.5,-12.5 - parent: 104 - - uid: 1071 - components: - - type: Transform - pos: 32.5,-13.5 - parent: 104 - - uid: 1072 - components: - - type: Transform - pos: 32.5,-14.5 - parent: 104 - - uid: 1073 - components: - - type: Transform - pos: 32.5,-15.5 - parent: 104 - - uid: 1074 - components: - - type: Transform - pos: 32.5,-16.5 - parent: 104 - - uid: 1075 - components: - - type: Transform - pos: 32.5,-17.5 - parent: 104 - - uid: 1076 - components: - - type: Transform - pos: 32.5,-18.5 - parent: 104 - - uid: 1077 - components: - - type: Transform - pos: 32.5,-19.5 - parent: 104 - - uid: 1078 - components: - - type: Transform - pos: 32.5,-20.5 - parent: 104 - - uid: 1079 - components: - - type: Transform - pos: 32.5,-21.5 - parent: 104 - - uid: 1083 - components: - - type: Transform - pos: -4.5,-16.5 - parent: 104 - - uid: 1084 - components: - - type: Transform - pos: -4.5,-21.5 - parent: 104 - - uid: 1085 - components: - - type: Transform - pos: 28.5,-23.5 - parent: 104 - - uid: 1092 - components: - - type: Transform - pos: 27.5,-23.5 - parent: 104 - - uid: 1093 - components: - - type: Transform - pos: 26.5,-23.5 - parent: 104 - - uid: 1094 - components: - - type: Transform - pos: 25.5,-23.5 - parent: 104 - - uid: 1095 - components: - - type: Transform - pos: 24.5,-23.5 - parent: 104 - - uid: 1096 - components: - - type: Transform - pos: 23.5,-23.5 - parent: 104 - - uid: 1097 - components: - - type: Transform - pos: 22.5,-23.5 - parent: 104 - - uid: 1098 - components: - - type: Transform - pos: 21.5,-23.5 - parent: 104 - - uid: 1099 - components: - - type: Transform - pos: 20.5,-23.5 - parent: 104 - - uid: 1100 - components: - - type: Transform - pos: 19.5,-23.5 - parent: 104 - - uid: 1101 - components: - - type: Transform - pos: 18.5,-23.5 - parent: 104 - - uid: 1105 - components: - - type: Transform - pos: 8.5,-23.5 - parent: 104 - - uid: 1106 - components: - - type: Transform - pos: 7.5,-23.5 - parent: 104 - - uid: 1107 - components: - - type: Transform - pos: 6.5,-23.5 - parent: 104 - - uid: 1108 - components: - - type: Transform - pos: 5.5,-23.5 - parent: 104 - - uid: 1109 - components: - - type: Transform - pos: -23.5,23.5 - parent: 104 - - uid: 1155 - components: - - type: Transform - pos: -23.5,24.5 - parent: 104 - - uid: 1156 - components: - - type: Transform - pos: -23.5,25.5 - parent: 104 - - uid: 1157 - components: - - type: Transform - pos: -22.5,32.5 - parent: 104 - - uid: 1158 - components: - - type: Transform - pos: -23.5,30.5 - parent: 104 - - uid: 1160 - components: - - type: Transform - pos: -23.5,31.5 - parent: 104 - - uid: 1161 - components: - - type: Transform - pos: -22.5,23.5 - parent: 104 - - uid: 1162 - components: - - type: Transform - pos: -22.5,24.5 - parent: 104 - - uid: 1163 - components: - - type: Transform - pos: -22.5,25.5 - parent: 104 - - uid: 1164 - components: - - type: Transform - pos: -21.5,26.5 - parent: 104 - - uid: 1165 - components: - - type: Transform - pos: -22.5,30.5 - parent: 104 - - uid: 1166 - components: - - type: Transform - pos: -21.5,30.5 - parent: 104 - - uid: 1167 - components: - - type: Transform - pos: -24.5,31.5 - parent: 104 - - uid: 1168 - components: - - type: Transform - pos: -23.5,20.5 - parent: 104 - - uid: 1169 - components: - - type: Transform - pos: -24.5,21.5 - parent: 104 - - uid: 1170 - components: - - type: Transform - pos: -22.5,31.5 - parent: 104 - - uid: 1171 - components: - - type: Transform - pos: -25.5,30.5 - parent: 104 - - uid: 1172 - components: - - type: Transform - pos: -25.5,31.5 - parent: 104 - - uid: 1173 - components: - - type: Transform - pos: -25.5,32.5 - parent: 104 - - uid: 1174 - components: - - type: Transform - pos: -23.5,21.5 - parent: 104 - - uid: 1175 - components: - - type: Transform - pos: -23.5,29.5 - parent: 104 - - uid: 1176 - components: - - type: Transform - pos: -22.5,20.5 - parent: 104 - - uid: 1177 - components: - - type: Transform - pos: -23.5,15.5 - parent: 104 - - uid: 1178 - components: - - type: Transform - pos: -23.5,13.5 - parent: 104 - - uid: 1179 - components: - - type: Transform - pos: -22.5,13.5 - parent: 104 - - uid: 1180 - components: - - type: Transform - pos: -11.5,10.5 - parent: 104 - - uid: 1224 - components: - - type: Transform - pos: -23.5,14.5 - parent: 104 - - uid: 1250 - components: - - type: Transform - pos: 1.5,-10.5 - parent: 104 - - uid: 1390 - components: - - type: Transform - pos: -0.5,-10.5 - parent: 104 - - uid: 1391 - components: - - type: Transform - pos: -23.5,28.5 - parent: 104 - - uid: 1392 - components: - - type: Transform - pos: -24.5,30.5 - parent: 104 - - uid: 1393 - components: - - type: Transform - pos: -23.5,32.5 - parent: 104 - - uid: 1394 - components: - - type: Transform - pos: -22.5,26.5 - parent: 104 - - uid: 1395 - components: - - type: Transform - pos: -23.5,26.5 - parent: 104 - - uid: 1396 - components: - - type: Transform - pos: -24.5,32.5 - parent: 104 - - uid: 1397 - components: - - type: Transform - pos: -23.5,27.5 - parent: 104 - - uid: 1398 - components: - - type: Transform - pos: -22.5,28.5 - parent: 104 - - uid: 1399 - components: - - type: Transform - pos: -21.5,29.5 - parent: 104 - - uid: 1400 - components: - - type: Transform - pos: -22.5,29.5 - parent: 104 - - uid: 1401 - components: - - type: Transform - pos: -22.5,27.5 - parent: 104 - - uid: 1402 - components: - - type: Transform - pos: -21.5,27.5 - parent: 104 - - uid: 1403 - components: - - type: Transform - pos: -21.5,28.5 - parent: 104 - - uid: 1404 - components: - - type: Transform - pos: -21.5,25.5 - parent: 104 - - uid: 1405 - components: - - type: Transform - pos: -21.5,24.5 - parent: 104 - - uid: 1406 - components: - - type: Transform - pos: -21.5,23.5 - parent: 104 - - uid: 1407 - components: - - type: Transform - pos: 6.5,-24.5 - parent: 104 - - uid: 1408 - components: - - type: Transform - pos: 7.5,-24.5 - parent: 104 - - uid: 1415 - components: - - type: Transform - pos: 3.5,-21.5 - parent: 104 - - uid: 1416 - components: - - type: Transform - pos: 4.5,-21.5 - parent: 104 - - uid: 1417 - components: - - type: Transform - pos: 20.5,-24.5 - parent: 104 - - uid: 1418 - components: - - type: Transform - pos: 21.5,-24.5 - parent: 104 - - uid: 1419 - components: - - type: Transform - pos: 22.5,-24.5 - parent: 104 - - uid: 1420 - components: - - type: Transform - pos: 23.5,-24.5 - parent: 104 - - uid: 1421 - components: - - type: Transform - pos: 24.5,-24.5 - parent: 104 - - uid: 1422 - components: - - type: Transform - pos: 25.5,-24.5 - parent: 104 - - uid: 1423 - components: - - type: Transform - pos: 26.5,-24.5 - parent: 104 - - uid: 1424 - components: - - type: Transform - pos: 27.5,-24.5 - parent: 104 - - uid: 1425 - components: - - type: Transform - pos: -4.5,-22.5 - parent: 104 - - uid: 1427 - components: - - type: Transform - pos: -4.5,-17.5 - parent: 104 - - uid: 1433 - components: - - type: Transform - pos: 5.5,-24.5 - parent: 104 - - uid: 1434 - components: - - type: Transform - pos: 33.5,-21.5 - parent: 104 - - uid: 1435 - components: - - type: Transform - pos: 33.5,-20.5 - parent: 104 - - uid: 1436 - components: - - type: Transform - pos: 33.5,-19.5 - parent: 104 - - uid: 1437 - components: - - type: Transform - pos: 33.5,-18.5 - parent: 104 - - uid: 1440 - components: - - type: Transform - pos: 33.5,-17.5 - parent: 104 - - uid: 1441 - components: - - type: Transform - pos: 33.5,-16.5 - parent: 104 - - uid: 1442 - components: - - type: Transform - pos: 33.5,-15.5 - parent: 104 - - uid: 1443 - components: - - type: Transform - pos: 33.5,-14.5 - parent: 104 - - uid: 1444 - components: - - type: Transform - pos: 33.5,-13.5 - parent: 104 - - uid: 1446 - components: - - type: Transform - pos: 33.5,-12.5 - parent: 104 - - uid: 1447 - components: - - type: Transform - pos: 30.5,5.5 - parent: 104 - - uid: 1448 - components: - - type: Transform - pos: 29.5,5.5 - parent: 104 - - uid: 1472 - components: - - type: Transform - pos: 28.5,5.5 - parent: 104 - - uid: 1542 - components: - - type: Transform - pos: 24.5,5.5 - parent: 104 - - uid: 1550 - components: - - type: Transform - pos: 23.5,5.5 - parent: 104 - - uid: 1552 - components: - - type: Transform - pos: 22.5,5.5 - parent: 104 - - uid: 1553 - components: - - type: Transform - pos: 21.5,5.5 - parent: 104 - - uid: 1557 - components: - - type: Transform - pos: 19.5,19.5 - parent: 104 - - uid: 1559 - components: - - type: Transform - pos: 19.5,20.5 - parent: 104 - - uid: 1560 - components: - - type: Transform - pos: 19.5,21.5 - parent: 104 - - uid: 1562 - components: - - type: Transform - pos: -11.5,19.5 - parent: 104 - - uid: 1564 - components: - - type: Transform - pos: -11.5,18.5 - parent: 104 - - uid: 1566 - components: - - type: Transform - pos: 20.5,9.5 - parent: 104 - - uid: 1567 - components: - - type: Transform - pos: 20.5,10.5 - parent: 104 - - uid: 1568 - components: - - type: Transform - pos: 20.5,11.5 - parent: 104 - - uid: 1569 - components: - - type: Transform - pos: 20.5,12.5 - parent: 104 - - uid: 1594 - components: - - type: Transform - pos: 20.5,13.5 - parent: 104 - - uid: 1598 - components: - - type: Transform - pos: 20.5,14.5 - parent: 104 - - uid: 1599 - components: - - type: Transform - pos: 20.5,15.5 - parent: 104 - - uid: 1600 - components: - - type: Transform - pos: 20.5,16.5 - parent: 104 - - uid: 1601 - components: - - type: Transform - pos: 20.5,17.5 - parent: 104 - - uid: 1602 - components: - - type: Transform - pos: 20.5,18.5 - parent: 104 - - uid: 1603 - components: - - type: Transform - pos: 20.5,19.5 - parent: 104 - - uid: 1604 - components: - - type: Transform - pos: 20.5,20.5 - parent: 104 - - uid: 1605 - components: - - type: Transform - pos: 20.5,21.5 - parent: 104 - - uid: 1606 - components: - - type: Transform - pos: -10.5,19.5 - parent: 104 - - uid: 1607 - components: - - type: Transform - pos: 19.5,11.5 - parent: 104 - - uid: 1608 - components: - - type: Transform - pos: 19.5,12.5 - parent: 104 - - uid: 1609 - components: - - type: Transform - pos: 19.5,15.5 - parent: 104 - - uid: 1610 - components: - - type: Transform - pos: 18.5,21.5 - parent: 104 - - uid: 1611 - components: - - type: Transform - pos: 17.5,21.5 - parent: 104 - - uid: 1612 - components: - - type: Transform - pos: 16.5,21.5 - parent: 104 - - uid: 1613 - components: - - type: Transform - pos: 15.5,21.5 - parent: 104 - - uid: 1614 - components: - - type: Transform - pos: 14.5,21.5 - parent: 104 - - uid: 1615 - components: - - type: Transform - pos: 13.5,21.5 - parent: 104 - - uid: 1616 - components: - - type: Transform - pos: 12.5,21.5 - parent: 104 - - uid: 1623 - components: - - type: Transform - pos: 11.5,21.5 - parent: 104 - - uid: 1625 - components: - - type: Transform - pos: 10.5,21.5 - parent: 104 - - uid: 1632 - components: - - type: Transform - pos: 9.5,21.5 - parent: 104 - - uid: 1636 - components: - - type: Transform - pos: -0.5,21.5 - parent: 104 - - uid: 1637 - components: - - type: Transform - pos: -1.5,21.5 - parent: 104 - - uid: 1638 - components: - - type: Transform - pos: -2.5,21.5 - parent: 104 - - uid: 1639 - components: - - type: Transform - pos: -0.5,22.5 - parent: 104 - - uid: 1640 - components: - - type: Transform - pos: -1.5,22.5 - parent: 104 - - uid: 1641 - components: - - type: Transform - pos: -2.5,22.5 - parent: 104 - - uid: 1642 - components: - - type: Transform - pos: -3.5,22.5 - parent: 104 - - uid: 1643 - components: - - type: Transform - pos: -7.5,22.5 - parent: 104 - - uid: 1644 - components: - - type: Transform - pos: -8.5,22.5 - parent: 104 - - uid: 1645 - components: - - type: Transform - pos: -8.5,23.5 - parent: 104 - - uid: 1646 - components: - - type: Transform - pos: -9.5,23.5 - parent: 104 - - uid: 1647 - components: - - type: Transform - pos: -10.5,23.5 - parent: 104 - - uid: 1649 - components: - - type: Transform - pos: 28.5,7.5 - parent: 104 - - uid: 1650 - components: - - type: Transform - pos: 28.5,8.5 - parent: 104 - - uid: 1651 - components: - - type: Transform - pos: 22.5,13.5 - parent: 104 - - uid: 1657 - components: - - type: Transform - pos: 23.5,13.5 - parent: 104 - - uid: 1658 - components: - - type: Transform - pos: 17.5,22.5 - parent: 104 - - uid: 1659 - components: - - type: Transform - pos: 24.5,16.5 - parent: 104 - - uid: 1660 - components: - - type: Transform - pos: 24.5,15.5 - parent: 104 - - uid: 1661 - components: - - type: Transform - pos: 24.5,14.5 - parent: 104 - - uid: 1662 - components: - - type: Transform - pos: 23.5,17.5 - parent: 104 - - uid: 1663 - components: - - type: Transform - pos: 28.5,6.5 - parent: 104 - - uid: 1664 - components: - - type: Transform - pos: 23.5,14.5 - parent: 104 - - uid: 1665 - components: - - type: Transform - pos: 22.5,14.5 - parent: 104 - - uid: 1666 - components: - - type: Transform - pos: 23.5,12.5 - parent: 104 - - uid: 1667 - components: - - type: Transform - pos: 21.5,8.5 - parent: 104 - - uid: 1676 - components: - - type: Transform - pos: 24.5,9.5 - parent: 104 - - uid: 1677 - components: - - type: Transform - pos: 24.5,10.5 - parent: 104 - - uid: 1678 - components: - - type: Transform - pos: 24.5,17.5 - parent: 104 - - uid: 1679 - components: - - type: Transform - pos: 23.5,16.5 - parent: 104 - - uid: 1682 - components: - - type: Transform - pos: 23.5,15.5 - parent: 104 - - uid: 1683 - components: - - type: Transform - pos: 24.5,11.5 - parent: 104 - - uid: 1685 - components: - - type: Transform - pos: 23.5,11.5 - parent: 104 - - uid: 1686 - components: - - type: Transform - pos: 21.5,9.5 - parent: 104 - - uid: 1687 - components: - - type: Transform - pos: 27.5,7.5 - parent: 104 - - uid: 1688 - components: - - type: Transform - pos: 27.5,8.5 - parent: 104 - - uid: 1691 - components: - - type: Transform - pos: 26.5,8.5 - parent: 104 - - uid: 1692 - components: - - type: Transform - pos: 22.5,15.5 - parent: 104 - - uid: 1693 - components: - - type: Transform - pos: 24.5,12.5 - parent: 104 - - uid: 1695 - components: - - type: Transform - pos: 23.5,10.5 - parent: 104 - - uid: 1696 - components: - - type: Transform - pos: 22.5,10.5 - parent: 104 - - uid: 1698 - components: - - type: Transform - pos: 12.5,22.5 - parent: 104 - - uid: 1700 - components: - - type: Transform - pos: 25.5,9.5 - parent: 104 - - uid: 1701 - components: - - type: Transform - pos: 25.5,10.5 - parent: 104 - - uid: 1702 - components: - - type: Transform - pos: 13.5,22.5 - parent: 104 - - uid: 1709 - components: - - type: Transform - pos: 24.5,13.5 - parent: 104 - - uid: 1710 - components: - - type: Transform - pos: 22.5,8.5 - parent: 104 - - uid: 1712 - components: - - type: Transform - pos: 22.5,9.5 - parent: 104 - - uid: 1713 - components: - - type: Transform - pos: 11.5,22.5 - parent: 104 - - uid: 1714 - components: - - type: Transform - pos: 25.5,11.5 - parent: 104 - - uid: 1715 - components: - - type: Transform - pos: 22.5,16.5 - parent: 104 - - uid: 1716 - components: - - type: Transform - pos: 22.5,17.5 - parent: 104 - - uid: 1717 - components: - - type: Transform - pos: 19.5,22.5 - parent: 104 - - uid: 1719 - components: - - type: Transform - pos: 21.5,13.5 - parent: 104 - - uid: 1720 - components: - - type: Transform - pos: 23.5,9.5 - parent: 104 - - uid: 1721 - components: - - type: Transform - pos: 21.5,11.5 - parent: 104 - - uid: 1722 - components: - - type: Transform - pos: 22.5,11.5 - parent: 104 - - uid: 1723 - components: - - type: Transform - pos: 10.5,22.5 - parent: 104 - - uid: 1724 - components: - - type: Transform - pos: 22.5,18.5 - parent: 104 - - uid: 1725 - components: - - type: Transform - pos: 16.5,22.5 - parent: 104 - - uid: 1726 - components: - - type: Transform - pos: 15.5,22.5 - parent: 104 - - uid: 1727 - components: - - type: Transform - pos: 14.5,22.5 - parent: 104 - - uid: 1729 - components: - - type: Transform - pos: 23.5,13.5 - parent: 104 - - uid: 1731 - components: - - type: Transform - pos: -11.5,21.5 - parent: 104 - - uid: 1732 - components: - - type: Transform - pos: 22.5,12.5 - parent: 104 - - uid: 1733 - components: - - type: Transform - pos: 27.5,6.5 - parent: 104 - - uid: 1734 - components: - - type: Transform - pos: 22.5,19.5 - parent: 104 - - uid: 1735 - components: - - type: Transform - pos: 18.5,22.5 - parent: 104 - - uid: 1736 - components: - - type: Transform - pos: 21.5,21.5 - parent: 104 - - uid: 1740 - components: - - type: Transform - pos: -11.5,20.5 - parent: 104 - - uid: 1741 - components: - - type: Transform - pos: 21.5,10.5 - parent: 104 - - uid: 1742 - components: - - type: Transform - pos: 21.5,20.5 - parent: 104 - - uid: 1743 - components: - - type: Transform - pos: 20.5,22.5 - parent: 104 - - uid: 1744 - components: - - type: Transform - pos: 21.5,19.5 - parent: 104 - - uid: 1745 - components: - - type: Transform - pos: 21.5,18.5 - parent: 104 - - uid: 1746 - components: - - type: Transform - pos: 21.5,17.5 - parent: 104 - - uid: 1747 - components: - - type: Transform - pos: 21.5,16.5 - parent: 104 - - uid: 1748 - components: - - type: Transform - pos: 21.5,15.5 - parent: 104 - - uid: 1749 - components: - - type: Transform - pos: 21.5,14.5 - parent: 104 - - uid: 1750 - components: - - type: Transform - pos: 21.5,12.5 - parent: 104 - - uid: 1751 - components: - - type: Transform - pos: 9.5,19.5 - parent: 104 - - uid: 1752 - components: - - type: Transform - pos: -2.5,23.5 - parent: 104 - - uid: 1753 - components: - - type: Transform - pos: -3.5,23.5 - parent: 104 - - uid: 1754 - components: - - type: Transform - pos: -4.5,23.5 - parent: 104 - - uid: 1755 - components: - - type: Transform - pos: -5.5,23.5 - parent: 104 - - uid: 1756 - components: - - type: Transform - pos: -6.5,23.5 - parent: 104 - - uid: 1757 - components: - - type: Transform - pos: -7.5,23.5 - parent: 104 - - uid: 1762 - components: - - type: Transform - pos: 1.5,-18.5 - parent: 104 - - uid: 1767 - components: - - type: Transform - pos: -19.5,7.5 - parent: 104 - - uid: 1768 - components: - - type: Transform - pos: -19.5,6.5 - parent: 104 - - uid: 1769 - components: - - type: Transform - pos: -19.5,5.5 - parent: 104 - - uid: 1770 - components: - - type: Transform - pos: -18.5,7.5 - parent: 104 - - uid: 1771 - components: - - type: Transform - pos: -18.5,6.5 - parent: 104 - - uid: 1772 - components: - - type: Transform - pos: -18.5,5.5 - parent: 104 - - uid: 1773 - components: - - type: Transform - pos: -17.5,7.5 - parent: 104 - - uid: 1774 - components: - - type: Transform - pos: -17.5,6.5 - parent: 104 - - uid: 1775 - components: - - type: Transform - pos: -17.5,5.5 - parent: 104 - - uid: 1776 - components: - - type: Transform - pos: -21.5,19.5 - parent: 104 - - uid: 1777 - components: - - type: Transform - pos: -22.5,19.5 - parent: 104 - - uid: 1778 - components: - - type: Transform - pos: -21.5,18.5 - parent: 104 - - uid: 1779 - components: - - type: Transform - pos: -22.5,18.5 - parent: 104 - - uid: 1780 - components: - - type: Transform - pos: -22.5,17.5 - parent: 104 - - uid: 1781 - components: - - type: Transform - pos: -23.5,17.5 - parent: 104 - - uid: 1782 - components: - - type: Transform - pos: -23.5,16.5 - parent: 104 - - uid: 1792 - components: - - type: Transform - pos: 4.5,-19.5 - parent: 104 - - uid: 1793 - components: - - type: Transform - pos: 4.5,-20.5 - parent: 104 - - uid: 1794 - components: - - type: Transform - pos: 2.5,-19.5 - parent: 104 - - uid: 1795 - components: - - type: Transform - pos: 2.5,-20.5 - parent: 104 - - uid: 1796 - components: - - type: Transform - pos: -1.5,-10.5 - parent: 104 - - uid: 1797 - components: - - type: Transform - pos: 3.5,-19.5 - parent: 104 - - uid: 1798 - components: - - type: Transform - pos: 3.5,-20.5 - parent: 104 - - uid: 1801 - components: - - type: Transform - pos: 7.5,-19.5 - parent: 104 - - uid: 1802 - components: - - type: Transform - pos: 7.5,-20.5 - parent: 104 - - uid: 1803 - components: - - type: Transform - pos: 7.5,-21.5 - parent: 104 - - uid: 1804 - components: - - type: Transform - pos: 5.5,-19.5 - parent: 104 - - uid: 1805 - components: - - type: Transform - pos: 5.5,-20.5 - parent: 104 - - uid: 1806 - components: - - type: Transform - pos: 5.5,-21.5 - parent: 104 - - uid: 1807 - components: - - type: Transform - pos: 6.5,-19.5 - parent: 104 - - uid: 1808 - components: - - type: Transform - pos: 6.5,-20.5 - parent: 104 - - uid: 1809 - components: - - type: Transform - pos: 6.5,-21.5 - parent: 104 - - uid: 1835 - components: - - type: Transform - pos: 8.5,-19.5 - parent: 104 - - uid: 1836 - components: - - type: Transform - pos: 8.5,-20.5 - parent: 104 - - uid: 1837 - components: - - type: Transform - pos: 8.5,-21.5 - parent: 104 - - uid: 1865 - components: - - type: Transform - pos: 16.5,-21.5 - parent: 104 - - uid: 1870 - components: - - type: Transform - pos: 17.5,-21.5 - parent: 104 - - uid: 1875 - components: - - type: Transform - pos: 18.5,-21.5 - parent: 104 - - uid: 1880 - components: - - type: Transform - pos: 19.5,-21.5 - parent: 104 - - uid: 1884 - components: - - type: Transform - pos: 20.5,-21.5 - parent: 104 - - uid: 1888 - components: - - type: Transform - pos: 21.5,-21.5 - parent: 104 - - uid: 1892 - components: - - type: Transform - pos: 22.5,-21.5 - parent: 104 - - uid: 1896 - components: - - type: Transform - pos: 23.5,-21.5 - parent: 104 - - uid: 1900 - components: - - type: Transform - pos: 24.5,-21.5 - parent: 104 - - uid: 1904 - components: - - type: Transform - pos: 25.5,-21.5 - parent: 104 - - uid: 1909 - components: - - type: Transform - pos: 28.5,-11.5 - parent: 104 - - uid: 1910 - components: - - type: Transform - pos: 28.5,-10.5 - parent: 104 - - uid: 1911 - components: - - type: Transform - pos: 28.5,-9.5 - parent: 104 - - uid: 1912 - components: - - type: Transform - pos: 28.5,-8.5 - parent: 104 - - uid: 1913 - components: - - type: Transform - pos: 26.5,-21.5 - parent: 104 - - uid: 1923 - components: - - type: Transform - pos: 26.5,-11.5 - parent: 104 - - uid: 1924 - components: - - type: Transform - pos: 26.5,-10.5 - parent: 104 - - uid: 1925 - components: - - type: Transform - pos: 26.5,-9.5 - parent: 104 - - uid: 1926 - components: - - type: Transform - pos: 26.5,-8.5 - parent: 104 - - uid: 1927 - components: - - type: Transform - pos: 26.5,-7.5 - parent: 104 - - uid: 1928 - components: - - type: Transform - pos: 26.5,-6.5 - parent: 104 - - uid: 1930 - components: - - type: Transform - pos: 26.5,-5.5 - parent: 104 - - uid: 1934 - components: - - type: Transform - pos: 26.5,-4.5 - parent: 104 - - uid: 1935 - components: - - type: Transform - pos: 26.5,-3.5 - parent: 104 - - uid: 1936 - components: - - type: Transform - pos: 26.5,-2.5 - parent: 104 - - uid: 1937 - components: - - type: Transform - pos: 26.5,-1.5 - parent: 104 - - uid: 1938 - components: - - type: Transform - pos: 26.5,-0.5 - parent: 104 - - uid: 1939 - components: - - type: Transform - pos: 26.5,0.5 - parent: 104 - - uid: 1940 - components: - - type: Transform - pos: 26.5,1.5 - parent: 104 - - uid: 1942 - components: - - type: Transform - pos: 27.5,-21.5 - parent: 104 - - uid: 1952 - components: - - type: Transform - pos: 27.5,-11.5 - parent: 104 - - uid: 1953 - components: - - type: Transform - pos: 27.5,-10.5 - parent: 104 - - uid: 1954 - components: - - type: Transform - pos: 27.5,-9.5 - parent: 104 - - uid: 1955 - components: - - type: Transform - pos: 27.5,-8.5 - parent: 104 - - uid: 1956 - components: - - type: Transform - pos: 27.5,-7.5 - parent: 104 - - uid: 1957 - components: - - type: Transform - pos: 27.5,-6.5 - parent: 104 - - uid: 1958 - components: - - type: Transform - pos: 27.5,-5.5 - parent: 104 - - uid: 1959 - components: - - type: Transform - pos: 27.5,-4.5 - parent: 104 - - uid: 1960 - components: - - type: Transform - pos: 27.5,-3.5 - parent: 104 - - uid: 1961 - components: - - type: Transform - pos: 27.5,-2.5 - parent: 104 - - uid: 1962 - components: - - type: Transform - pos: 27.5,-1.5 - parent: 104 - - uid: 1963 - components: - - type: Transform - pos: 27.5,-0.5 - parent: 104 - - uid: 1964 - components: - - type: Transform - pos: 27.5,0.5 - parent: 104 - - uid: 1965 - components: - - type: Transform - pos: 27.5,1.5 - parent: 104 - - uid: 1966 - components: - - type: Transform - pos: 27.5,2.5 - parent: 104 - - uid: 1967 - components: - - type: Transform - pos: 28.5,-21.5 - parent: 104 - - uid: 1973 - components: - - type: Transform - pos: 30.5,-1.5 - parent: 104 - - uid: 1974 - components: - - type: Transform - pos: 30.5,-0.5 - parent: 104 - - uid: 1975 - components: - - type: Transform - pos: 30.5,0.5 - parent: 104 - - uid: 1976 - components: - - type: Transform - pos: 30.5,1.5 - parent: 104 - - uid: 1977 - components: - - type: Transform - pos: 30.5,2.5 - parent: 104 - - uid: 1978 - components: - - type: Transform - pos: 28.5,-7.5 - parent: 104 - - uid: 1979 - components: - - type: Transform - pos: 28.5,-6.5 - parent: 104 - - uid: 1980 - components: - - type: Transform - pos: 28.5,-5.5 - parent: 104 - - uid: 1981 - components: - - type: Transform - pos: 28.5,-4.5 - parent: 104 - - uid: 1982 - components: - - type: Transform - pos: 28.5,-3.5 - parent: 104 - - uid: 1983 - components: - - type: Transform - pos: 28.5,-2.5 - parent: 104 - - uid: 1984 - components: - - type: Transform - pos: 28.5,-1.5 - parent: 104 - - uid: 1985 - components: - - type: Transform - pos: 28.5,-0.5 - parent: 104 - - uid: 1986 - components: - - type: Transform - pos: 28.5,0.5 - parent: 104 - - uid: 1987 - components: - - type: Transform - pos: 28.5,1.5 - parent: 104 - - uid: 1988 - components: - - type: Transform - pos: 28.5,2.5 - parent: 104 - - uid: 1989 - components: - - type: Transform - pos: 29.5,-20.5 - parent: 104 - - uid: 1990 - components: - - type: Transform - pos: 29.5,-19.5 - parent: 104 - - uid: 1991 - components: - - type: Transform - pos: 29.5,-18.5 - parent: 104 - - uid: 1992 - components: - - type: Transform - pos: 29.5,-17.5 - parent: 104 - - uid: 1993 - components: - - type: Transform - pos: 29.5,-16.5 - parent: 104 - - uid: 1994 - components: - - type: Transform - pos: 29.5,-15.5 - parent: 104 - - uid: 1995 - components: - - type: Transform - pos: 29.5,-14.5 - parent: 104 - - uid: 1996 - components: - - type: Transform - pos: 29.5,-13.5 - parent: 104 - - uid: 1997 - components: - - type: Transform - pos: 29.5,-12.5 - parent: 104 - - uid: 1998 - components: - - type: Transform - pos: 29.5,-11.5 - parent: 104 - - uid: 1999 - components: - - type: Transform - pos: 29.5,-10.5 - parent: 104 - - uid: 2000 - components: - - type: Transform - pos: 29.5,-9.5 - parent: 104 - - uid: 2001 - components: - - type: Transform - pos: 29.5,-8.5 - parent: 104 - - uid: 2002 - components: - - type: Transform - pos: 29.5,-7.5 - parent: 104 - - uid: 2003 - components: - - type: Transform - pos: 29.5,-6.5 - parent: 104 - - uid: 2004 - components: - - type: Transform - pos: 29.5,-5.5 - parent: 104 - - uid: 2005 - components: - - type: Transform - pos: 29.5,-4.5 - parent: 104 - - uid: 2006 - components: - - type: Transform - pos: 29.5,-3.5 - parent: 104 - - uid: 2007 - components: - - type: Transform - pos: 29.5,-2.5 - parent: 104 - - uid: 2008 - components: - - type: Transform - pos: 29.5,-1.5 - parent: 104 - - uid: 2009 - components: - - type: Transform - pos: 29.5,-0.5 - parent: 104 - - uid: 2010 - components: - - type: Transform - pos: 29.5,0.5 - parent: 104 - - uid: 2011 - components: - - type: Transform - pos: 29.5,1.5 - parent: 104 - - uid: 2012 - components: - - type: Transform - pos: 29.5,2.5 - parent: 104 - - uid: 2013 - components: - - type: Transform - pos: 30.5,-19.5 - parent: 104 - - uid: 2014 - components: - - type: Transform - pos: 30.5,-18.5 - parent: 104 - - uid: 2015 - components: - - type: Transform - pos: 30.5,-17.5 - parent: 104 - - uid: 2020 - components: - - type: Transform - pos: 30.5,-16.5 - parent: 104 - - uid: 2023 - components: - - type: Transform - pos: 30.5,-15.5 - parent: 104 - - uid: 2024 - components: - - type: Transform - pos: 30.5,-14.5 - parent: 104 - - uid: 2025 - components: - - type: Transform - pos: 30.5,-13.5 - parent: 104 - - uid: 2026 - components: - - type: Transform - pos: 30.5,-12.5 - parent: 104 - - uid: 2029 - components: - - type: Transform - pos: 30.5,-9.5 - parent: 104 - - uid: 2030 - components: - - type: Transform - pos: 30.5,-8.5 - parent: 104 - - uid: 2032 - components: - - type: Transform - pos: 30.5,-7.5 - parent: 104 - - uid: 2033 - components: - - type: Transform - pos: 30.5,-6.5 - parent: 104 - - uid: 2034 - components: - - type: Transform - pos: 30.5,-5.5 - parent: 104 - - uid: 2035 - components: - - type: Transform - pos: 30.5,-4.5 - parent: 104 - - uid: 2036 - components: - - type: Transform - pos: 30.5,-3.5 - parent: 104 - - uid: 2037 - components: - - type: Transform - pos: 30.5,-2.5 - parent: 104 - - uid: 2038 - components: - - type: Transform - pos: 20.5,-0.5 - parent: 104 - - uid: 2039 - components: - - type: Transform - pos: 20.5,-1.5 - parent: 104 - - uid: 2040 - components: - - type: Transform - pos: 15.5,2.5 - parent: 104 - - uid: 2041 - components: - - type: Transform - pos: 16.5,2.5 - parent: 104 - - uid: 2042 - components: - - type: Transform - pos: 19.5,0.5 - parent: 104 - - uid: 2043 - components: - - type: Transform - pos: 19.5,-0.5 - parent: 104 - - uid: 2044 - components: - - type: Transform - pos: 19.5,-1.5 - parent: 104 - - uid: 2047 - components: - - type: Transform - pos: 25.5,0.5 - parent: 104 - - uid: 2049 - components: - - type: Transform - pos: 25.5,-0.5 - parent: 104 - - uid: 2051 - components: - - type: Transform - pos: 25.5,-1.5 - parent: 104 - - uid: 2054 - components: - - type: Transform - pos: 24.5,0.5 - parent: 104 - - uid: 2055 - components: - - type: Transform - pos: 24.5,-0.5 - parent: 104 - - uid: 2056 - components: - - type: Transform - pos: 24.5,-1.5 - parent: 104 - - uid: 2058 - components: - - type: Transform - pos: 23.5,1.5 - parent: 104 - - uid: 2059 - components: - - type: Transform - pos: 23.5,0.5 - parent: 104 - - uid: 2060 - components: - - type: Transform - pos: 23.5,-0.5 - parent: 104 - - uid: 2061 - components: - - type: Transform - pos: 23.5,-1.5 - parent: 104 - - uid: 2063 - components: - - type: Transform - pos: 22.5,1.5 - parent: 104 - - uid: 2064 - components: - - type: Transform - pos: 22.5,0.5 - parent: 104 - - uid: 2065 - components: - - type: Transform - pos: 22.5,-0.5 - parent: 104 - - uid: 2066 - components: - - type: Transform - pos: 22.5,-1.5 - parent: 104 - - uid: 2067 - components: - - type: Transform - pos: 14.5,2.5 - parent: 104 - - uid: 2068 - components: - - type: Transform - pos: 15.5,1.5 - parent: 104 - - uid: 2069 - components: - - type: Transform - pos: 21.5,0.5 - parent: 104 - - uid: 2070 - components: - - type: Transform - pos: 21.5,-0.5 - parent: 104 - - uid: 2071 - components: - - type: Transform - pos: 21.5,-1.5 - parent: 104 - - uid: 2072 - components: - - type: Transform - pos: 14.5,3.5 - parent: 104 - - uid: 2073 - components: - - type: Transform - pos: 17.5,-0.5 - parent: 104 - - uid: 2074 - components: - - type: Transform - pos: 17.5,0.5 - parent: 104 - - uid: 2075 - components: - - type: Transform - pos: 17.5,1.5 - parent: 104 - - uid: 2076 - components: - - type: Transform - pos: 18.5,-1.5 - parent: 104 - - uid: 2077 - components: - - type: Transform - pos: 18.5,-0.5 - parent: 104 - - uid: 2078 - components: - - type: Transform - pos: 18.5,0.5 - parent: 104 - - uid: 2079 - components: - - type: Transform - pos: 18.5,1.5 - parent: 104 - - uid: 2080 - components: - - type: Transform - pos: -12.5,28.5 - parent: 104 - - uid: 2084 - components: - - type: Transform - pos: -12.5,27.5 - parent: 104 - - uid: 2085 - components: - - type: Transform - pos: -12.5,26.5 - parent: 104 - - uid: 2086 - components: - - type: Transform - pos: -12.5,25.5 - parent: 104 - - uid: 2087 - components: - - type: Transform - pos: -16.5,25.5 - parent: 104 - - uid: 2088 - components: - - type: Transform - pos: -3.5,-6.5 - parent: 104 - - uid: 2091 - components: - - type: Transform - pos: -15.5,25.5 - parent: 104 - - uid: 2093 - components: - - type: Transform - pos: -14.5,26.5 - parent: 104 - - uid: 2094 - components: - - type: Transform - pos: -14.5,25.5 - parent: 104 - - uid: 2096 - components: - - type: Transform - pos: -13.5,27.5 - parent: 104 - - uid: 2098 - components: - - type: Transform - pos: -13.5,26.5 - parent: 104 - - uid: 2099 - components: - - type: Transform - pos: -13.5,25.5 - parent: 104 - - uid: 2100 - components: - - type: Transform - pos: -20.5,30.5 - parent: 104 - - uid: 2101 - components: - - type: Transform - pos: -20.5,29.5 - parent: 104 - - uid: 2102 - components: - - type: Transform - pos: -20.5,28.5 - parent: 104 - - uid: 2103 - components: - - type: Transform - pos: -20.5,27.5 - parent: 104 - - uid: 2104 - components: - - type: Transform - pos: -20.5,26.5 - parent: 104 - - uid: 2105 - components: - - type: Transform - pos: -20.5,25.5 - parent: 104 - - uid: 2106 - components: - - type: Transform - pos: -20.5,24.5 - parent: 104 - - uid: 2107 - components: - - type: Transform - pos: -19.5,30.5 - parent: 104 - - uid: 2108 - components: - - type: Transform - pos: -19.5,29.5 - parent: 104 - - uid: 2109 - components: - - type: Transform - pos: -19.5,28.5 - parent: 104 - - uid: 2110 - components: - - type: Transform - pos: -19.5,27.5 - parent: 104 - - uid: 2111 - components: - - type: Transform - pos: -19.5,26.5 - parent: 104 - - uid: 2112 - components: - - type: Transform - pos: -19.5,25.5 - parent: 104 - - uid: 2113 - components: - - type: Transform - pos: -19.5,24.5 - parent: 104 - - uid: 2115 - components: - - type: Transform - pos: -18.5,30.5 - parent: 104 - - uid: 2116 - components: - - type: Transform - pos: -18.5,29.5 - parent: 104 - - uid: 2117 - components: - - type: Transform - pos: -18.5,28.5 - parent: 104 - - uid: 2118 - components: - - type: Transform - pos: -18.5,27.5 - parent: 104 - - uid: 2119 - components: - - type: Transform - pos: -18.5,26.5 - parent: 104 - - uid: 2120 - components: - - type: Transform - pos: -18.5,25.5 - parent: 104 - - uid: 2121 - components: - - type: Transform - pos: -18.5,24.5 - parent: 104 - - uid: 2122 - components: - - type: Transform - pos: -17.5,28.5 - parent: 104 - - uid: 2123 - components: - - type: Transform - pos: -17.5,27.5 - parent: 104 - - uid: 2124 - components: - - type: Transform - pos: -17.5,25.5 - parent: 104 - - uid: 2125 - components: - - type: Transform - pos: -11.5,28.5 - parent: 104 - - uid: 2127 - components: - - type: Transform - pos: -11.5,27.5 - parent: 104 - - uid: 2128 - components: - - type: Transform - pos: -11.5,26.5 - parent: 104 - - uid: 2129 - components: - - type: Transform - pos: -11.5,25.5 - parent: 104 - - uid: 2130 - components: - - type: Transform - pos: -11.5,24.5 - parent: 104 - - uid: 2131 - components: - - type: Transform - pos: -21.5,31.5 - parent: 104 - - uid: 2132 - components: - - type: Transform - pos: -20.5,31.5 - parent: 104 - - uid: 2133 - components: - - type: Transform - pos: -19.5,31.5 - parent: 104 - - uid: 2134 - components: - - type: Transform - pos: -10.5,28.5 - parent: 104 - - uid: 2136 - components: - - type: Transform - pos: -10.5,27.5 - parent: 104 - - uid: 2137 - components: - - type: Transform - pos: -10.5,26.5 - parent: 104 - - uid: 2138 - components: - - type: Transform - pos: -10.5,25.5 - parent: 104 - - uid: 2139 - components: - - type: Transform - pos: -10.5,24.5 - parent: 104 - - uid: 2140 - components: - - type: Transform - pos: -9.5,29.5 - parent: 104 - - uid: 2141 - components: - - type: Transform - pos: -9.5,28.5 - parent: 104 - - uid: 2142 - components: - - type: Transform - pos: -9.5,27.5 - parent: 104 - - uid: 2143 - components: - - type: Transform - pos: -9.5,26.5 - parent: 104 - - uid: 2144 - components: - - type: Transform - pos: -9.5,25.5 - parent: 104 - - uid: 2145 - components: - - type: Transform - pos: -9.5,24.5 - parent: 104 - - uid: 2146 - components: - - type: Transform - pos: -8.5,29.5 - parent: 104 - - uid: 2147 - components: - - type: Transform - pos: -8.5,28.5 - parent: 104 - - uid: 2148 - components: - - type: Transform - pos: -8.5,27.5 - parent: 104 - - uid: 2149 - components: - - type: Transform - pos: -8.5,26.5 - parent: 104 - - uid: 2150 - components: - - type: Transform - pos: -8.5,25.5 - parent: 104 - - uid: 2151 - components: - - type: Transform - pos: -8.5,24.5 - parent: 104 - - uid: 2152 - components: - - type: Transform - pos: -7.5,29.5 - parent: 104 - - uid: 2153 - components: - - type: Transform - pos: -7.5,28.5 - parent: 104 - - uid: 2154 - components: - - type: Transform - pos: -7.5,27.5 - parent: 104 - - uid: 2155 - components: - - type: Transform - pos: -7.5,26.5 - parent: 104 - - uid: 2156 - components: - - type: Transform - pos: -7.5,25.5 - parent: 104 - - uid: 2157 - components: - - type: Transform - pos: -7.5,24.5 - parent: 104 - - uid: 2158 - components: - - type: Transform - pos: -6.5,29.5 - parent: 104 - - uid: 2159 - components: - - type: Transform - pos: -6.5,28.5 - parent: 104 - - uid: 2160 - components: - - type: Transform - pos: -6.5,27.5 - parent: 104 - - uid: 2161 - components: - - type: Transform - pos: -6.5,26.5 - parent: 104 - - uid: 2162 - components: - - type: Transform - pos: -6.5,25.5 - parent: 104 - - uid: 2163 - components: - - type: Transform - pos: -6.5,24.5 - parent: 104 - - uid: 2164 - components: - - type: Transform - pos: -5.5,29.5 - parent: 104 - - uid: 2165 - components: - - type: Transform - pos: -5.5,28.5 - parent: 104 - - uid: 2166 - components: - - type: Transform - pos: -5.5,27.5 - parent: 104 - - uid: 2167 - components: - - type: Transform - pos: -5.5,26.5 - parent: 104 - - uid: 2168 - components: - - type: Transform - pos: -5.5,25.5 - parent: 104 - - uid: 2169 - components: - - type: Transform - pos: -5.5,24.5 - parent: 104 - - uid: 2170 - components: - - type: Transform - pos: -4.5,29.5 - parent: 104 - - uid: 2171 - components: - - type: Transform - pos: -4.5,28.5 - parent: 104 - - uid: 2172 - components: - - type: Transform - pos: -4.5,27.5 - parent: 104 - - uid: 2173 - components: - - type: Transform - pos: -4.5,26.5 - parent: 104 - - uid: 2174 - components: - - type: Transform - pos: -4.5,25.5 - parent: 104 - - uid: 2175 - components: - - type: Transform - pos: -4.5,24.5 - parent: 104 - - uid: 2176 - components: - - type: Transform - pos: -3.5,28.5 - parent: 104 - - uid: 2177 - components: - - type: Transform - pos: -3.5,27.5 - parent: 104 - - uid: 2178 - components: - - type: Transform - pos: -3.5,26.5 - parent: 104 - - uid: 2179 - components: - - type: Transform - pos: -3.5,25.5 - parent: 104 - - uid: 2180 - components: - - type: Transform - pos: -3.5,24.5 - parent: 104 - - uid: 2181 - components: - - type: Transform - pos: -2.5,26.5 - parent: 104 - - uid: 2183 - components: - - type: Transform - pos: -2.5,25.5 - parent: 104 - - uid: 2184 - components: - - type: Transform - pos: -2.5,24.5 - parent: 104 - - uid: 2192 - components: - - type: Transform - pos: -5.5,-23.5 - parent: 104 - - uid: 2194 - components: - - type: Transform - pos: 15.5,29.5 - parent: 104 - - uid: 2195 - components: - - type: Transform - pos: 10.5,24.5 - parent: 104 - - uid: 2196 - components: - - type: Transform - pos: 15.5,31.5 - parent: 104 - - uid: 2197 - components: - - type: Transform - pos: 16.5,26.5 - parent: 104 - - uid: 2198 - components: - - type: Transform - pos: 15.5,30.5 - parent: 104 - - uid: 2199 - components: - - type: Transform - pos: 15.5,32.5 - parent: 104 - - uid: 2200 - components: - - type: Transform - pos: 16.5,23.5 - parent: 104 - - uid: 2201 - components: - - type: Transform - pos: 16.5,24.5 - parent: 104 - - uid: 2202 - components: - - type: Transform - pos: 16.5,25.5 - parent: 104 - - uid: 2203 - components: - - type: Transform - pos: 10.5,23.5 - parent: 104 - - uid: 2204 - components: - - type: Transform - pos: 10.5,25.5 - parent: 104 - - uid: 2205 - components: - - type: Transform - pos: 10.5,26.5 - parent: 104 - - uid: 2206 - components: - - type: Transform - pos: 10.5,27.5 - parent: 104 - - uid: 2207 - components: - - type: Transform - pos: 10.5,28.5 - parent: 104 - - uid: 2208 - components: - - type: Transform - pos: 11.5,23.5 - parent: 104 - - uid: 2209 - components: - - type: Transform - pos: 11.5,24.5 - parent: 104 - - uid: 2210 - components: - - type: Transform - pos: 11.5,25.5 - parent: 104 - - uid: 2211 - components: - - type: Transform - pos: 11.5,26.5 - parent: 104 - - uid: 2212 - components: - - type: Transform - pos: 11.5,27.5 - parent: 104 - - uid: 2213 - components: - - type: Transform - pos: 11.5,28.5 - parent: 104 - - uid: 2214 - components: - - type: Transform - pos: 11.5,29.5 - parent: 104 - - uid: 2215 - components: - - type: Transform - pos: 12.5,23.5 - parent: 104 - - uid: 2216 - components: - - type: Transform - pos: 12.5,24.5 - parent: 104 - - uid: 2217 - components: - - type: Transform - pos: 12.5,25.5 - parent: 104 - - uid: 2218 - components: - - type: Transform - pos: 12.5,26.5 - parent: 104 - - uid: 2219 - components: - - type: Transform - pos: 12.5,27.5 - parent: 104 - - uid: 2220 - components: - - type: Transform - pos: 12.5,28.5 - parent: 104 - - uid: 2221 - components: - - type: Transform - pos: 12.5,29.5 - parent: 104 - - uid: 2222 - components: - - type: Transform - pos: 12.5,30.5 - parent: 104 - - uid: 2223 - components: - - type: Transform - pos: 12.5,31.5 - parent: 104 - - uid: 2224 - components: - - type: Transform - pos: 13.5,23.5 - parent: 104 - - uid: 2225 - components: - - type: Transform - pos: 13.5,24.5 - parent: 104 - - uid: 2226 - components: - - type: Transform - pos: 13.5,25.5 - parent: 104 - - uid: 2227 - components: - - type: Transform - pos: 13.5,26.5 - parent: 104 - - uid: 2228 - components: - - type: Transform - pos: 13.5,27.5 - parent: 104 - - uid: 2229 - components: - - type: Transform - pos: 13.5,28.5 - parent: 104 - - uid: 2230 - components: - - type: Transform - pos: 13.5,29.5 - parent: 104 - - uid: 2231 - components: - - type: Transform - pos: 13.5,30.5 - parent: 104 - - uid: 2232 - components: - - type: Transform - pos: 13.5,31.5 - parent: 104 - - uid: 2233 - components: - - type: Transform - pos: 14.5,23.5 - parent: 104 - - uid: 2234 - components: - - type: Transform - pos: 14.5,24.5 - parent: 104 - - uid: 2235 - components: - - type: Transform - pos: 14.5,25.5 - parent: 104 - - uid: 2236 - components: - - type: Transform - pos: 14.5,26.5 - parent: 104 - - uid: 2237 - components: - - type: Transform - pos: 14.5,27.5 - parent: 104 - - uid: 2238 - components: - - type: Transform - pos: 14.5,28.5 - parent: 104 - - uid: 2239 - components: - - type: Transform - pos: 14.5,29.5 - parent: 104 - - uid: 2240 - components: - - type: Transform - pos: 14.5,30.5 - parent: 104 - - uid: 2241 - components: - - type: Transform - pos: 14.5,31.5 - parent: 104 - - uid: 2242 - components: - - type: Transform - pos: 14.5,32.5 - parent: 104 - - uid: 2243 - components: - - type: Transform - pos: 15.5,23.5 - parent: 104 - - uid: 2244 - components: - - type: Transform - pos: 15.5,24.5 - parent: 104 - - uid: 2245 - components: - - type: Transform - pos: 15.5,25.5 - parent: 104 - - uid: 2246 - components: - - type: Transform - pos: 15.5,26.5 - parent: 104 - - uid: 2247 - components: - - type: Transform - pos: 15.5,27.5 - parent: 104 - - uid: 2248 - components: - - type: Transform - pos: 15.5,28.5 - parent: 104 - - uid: 2249 - components: - - type: Transform - pos: 19.5,25.5 - parent: 104 - - uid: 2250 - components: - - type: Transform - pos: 19.5,26.5 - parent: 104 - - uid: 2251 - components: - - type: Transform - pos: 19.5,27.5 - parent: 104 - - uid: 2252 - components: - - type: Transform - pos: 19.5,28.5 - parent: 104 - - uid: 2253 - components: - - type: Transform - pos: 19.5,29.5 - parent: 104 - - uid: 2254 - components: - - type: Transform - pos: 19.5,30.5 - parent: 104 - - uid: 2255 - components: - - type: Transform - pos: 19.5,31.5 - parent: 104 - - uid: 2256 - components: - - type: Transform - pos: 20.5,26.5 - parent: 104 - - uid: 2259 - components: - - type: Transform - pos: 20.5,27.5 - parent: 104 - - uid: 2260 - components: - - type: Transform - pos: 20.5,28.5 - parent: 104 - - uid: 2261 - components: - - type: Transform - pos: 20.5,29.5 - parent: 104 - - uid: 2262 - components: - - type: Transform - pos: 16.5,27.5 - parent: 104 - - uid: 2263 - components: - - type: Transform - pos: 16.5,28.5 - parent: 104 - - uid: 2264 - components: - - type: Transform - pos: 16.5,29.5 - parent: 104 - - uid: 2265 - components: - - type: Transform - pos: 16.5,30.5 - parent: 104 - - uid: 2266 - components: - - type: Transform - pos: 16.5,31.5 - parent: 104 - - uid: 2267 - components: - - type: Transform - pos: 16.5,32.5 - parent: 104 - - uid: 2268 - components: - - type: Transform - pos: 17.5,23.5 - parent: 104 - - uid: 2269 - components: - - type: Transform - pos: 17.5,24.5 - parent: 104 - - uid: 2270 - components: - - type: Transform - pos: 17.5,25.5 - parent: 104 - - uid: 2271 - components: - - type: Transform - pos: 17.5,26.5 - parent: 104 - - uid: 2272 - components: - - type: Transform - pos: 17.5,27.5 - parent: 104 - - uid: 2273 - components: - - type: Transform - pos: 17.5,28.5 - parent: 104 - - uid: 2274 - components: - - type: Transform - pos: 17.5,29.5 - parent: 104 - - uid: 2275 - components: - - type: Transform - pos: 17.5,30.5 - parent: 104 - - uid: 2276 - components: - - type: Transform - pos: 17.5,31.5 - parent: 104 - - uid: 2277 - components: - - type: Transform - pos: 17.5,32.5 - parent: 104 - - uid: 2278 - components: - - type: Transform - pos: 18.5,23.5 - parent: 104 - - uid: 2279 - components: - - type: Transform - pos: 18.5,24.5 - parent: 104 - - uid: 2280 - components: - - type: Transform - pos: 18.5,25.5 - parent: 104 - - uid: 2281 - components: - - type: Transform - pos: 18.5,26.5 - parent: 104 - - uid: 2282 - components: - - type: Transform - pos: 18.5,27.5 - parent: 104 - - uid: 2283 - components: - - type: Transform - pos: 18.5,28.5 - parent: 104 - - uid: 2284 - components: - - type: Transform - pos: 18.5,29.5 - parent: 104 - - uid: 2285 - components: - - type: Transform - pos: 18.5,30.5 - parent: 104 - - uid: 2286 - components: - - type: Transform - pos: 18.5,31.5 - parent: 104 - - uid: 2287 - components: - - type: Transform - pos: 18.5,32.5 - parent: 104 - - uid: 2288 - components: - - type: Transform - pos: 19.5,23.5 - parent: 104 - - uid: 2289 - components: - - type: Transform - pos: 19.5,24.5 - parent: 104 -- proto: BalloonSyn - entities: - - uid: 1327 - components: - - type: Transform - pos: 0.03361702,-6.368435 - parent: 104 -- proto: BaseComputer - entities: - - uid: 18 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-8.5 - parent: 104 - - uid: 29 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 1.5,-8.5 - parent: 104 - - uid: 111 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-1.5 - parent: 104 - - uid: 2292 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-2.5 - parent: 104 - - uid: 2295 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-3.5 - parent: 104 -- proto: Beaker - entities: - - uid: 2031 - components: - - type: Transform - pos: 3.6114278,-10.732791 - parent: 104 - - uid: 2095 - components: - - type: Transform - pos: 4.017678,-12.520861 - parent: 104 -- proto: Bed - entities: - - uid: 957 - components: - - type: Transform - pos: 15.5,-3.5 - parent: 104 - - uid: 1548 - components: - - type: Transform - pos: 15.5,-4.5 - parent: 104 -- proto: BedsheetMedical - entities: - - uid: 1872 - components: - - type: Transform - pos: 1.5,-12.5 - parent: 104 - - uid: 1877 - components: - - type: Transform - pos: -1.5,-16.5 - parent: 104 - - uid: 1898 - components: - - type: Transform - pos: 1.5,-16.5 - parent: 104 - - uid: 1899 - components: - - type: Transform - pos: -1.5,-12.5 - parent: 104 -- proto: BedsheetSyndie - entities: - - uid: 154 - components: - - type: Transform - pos: 15.5,-3.5 - parent: 104 - - uid: 289 - components: - - type: Transform - pos: 15.5,-4.5 - parent: 104 -- proto: BigBox - entities: - - uid: 2458 - components: - - type: Transform - pos: 13.500806,-4.4874377 - parent: 104 -- proto: Bookshelf - entities: - - uid: 140 - components: - - type: Transform - pos: -0.5,-0.5 - parent: 104 -- proto: BoozeDispenser - entities: - - uid: 230 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 2.5,-8.5 - parent: 104 -- proto: BoxBeaker - entities: - - uid: 2114 - components: - - type: Transform - pos: 5.539936,-12.489611 - parent: 104 -- proto: BoxFolderBlack - entities: - - uid: 1022 - components: - - type: Transform - pos: -13.494709,9.54891 - parent: 104 -- proto: BoxMagazinePistolCaselessRiflePractice - entities: - - uid: 1635 - components: - - type: Transform - pos: 5.4970627,10.597828 - parent: 104 -- proto: BoxMagazinePistolSubMachineGunPractice - entities: - - uid: 1634 - components: - - type: Transform - pos: 5.388983,13.953581 - parent: 104 -- proto: BoxPillCanister - entities: - - uid: 1844 - components: - - type: Transform - pos: 5.430561,-12.364611 - parent: 104 -- proto: BoxShotgunPractice - entities: - - uid: 1633 - components: - - type: Transform - pos: 5.4972115,14.562988 - parent: 104 - - type: BallisticAmmoProvider - unspawnedCount: 12 -- proto: Bucket - entities: - - uid: 153 - components: - - type: Transform - pos: 10.809023,-4.25956 - parent: 104 -- proto: CableApcExtension - entities: - - uid: 13 - components: - - type: Transform - pos: -10.5,-5.5 - parent: 104 - - uid: 24 - components: - - type: Transform - pos: -31.5,0.5 - parent: 104 - - uid: 100 - components: - - type: Transform - pos: 13.5,0.5 - parent: 104 - - uid: 143 - components: - - type: Transform - pos: 12.5,0.5 - parent: 104 - - uid: 213 - components: - - type: Transform - pos: -16.5,-19.5 - parent: 104 - - uid: 214 - components: - - type: Transform - pos: -15.5,-19.5 - parent: 104 - - uid: 220 - components: - - type: Transform - pos: 11.5,0.5 - parent: 104 - - uid: 235 - components: - - type: Transform - pos: 1.5,-13.5 - parent: 104 - - uid: 245 - components: - - type: Transform - pos: 12.5,-18.5 - parent: 104 - - uid: 254 - components: - - type: Transform - pos: -30.5,0.5 - parent: 104 - - uid: 279 - components: - - type: Transform - pos: 11.5,-0.5 - parent: 104 - - uid: 287 - components: - - type: Transform - pos: -15.5,0.5 - parent: 104 - - uid: 297 - components: - - type: Transform - pos: 3.5,-14.5 - parent: 104 - - uid: 390 - components: - - type: Transform - pos: -10.5,-7.5 - parent: 104 - - uid: 397 - components: - - type: Transform - pos: -10.5,0.5 - parent: 104 - - uid: 411 - components: - - type: Transform - pos: -10.5,-8.5 - parent: 104 - - uid: 412 - components: - - type: Transform - pos: -26.5,-19.5 - parent: 104 - - uid: 428 - components: - - type: Transform - pos: -23.5,-19.5 - parent: 104 - - uid: 429 - components: - - type: Transform - pos: -10.5,-0.5 - parent: 104 - - uid: 430 - components: - - type: Transform - pos: -10.5,1.5 - parent: 104 - - uid: 432 - components: - - type: Transform - pos: -27.5,0.5 - parent: 104 - - uid: 433 - components: - - type: Transform - pos: -17.5,-19.5 - parent: 104 - - uid: 434 - components: - - type: Transform - pos: -21.5,-19.5 - parent: 104 - - uid: 435 - components: - - type: Transform - pos: -10.5,-4.5 - parent: 104 - - uid: 436 - components: - - type: Transform - pos: -10.5,-6.5 - parent: 104 - - uid: 437 - components: - - type: Transform - pos: -10.5,-9.5 - parent: 104 - - uid: 438 - components: - - type: Transform - pos: -20.5,-19.5 - parent: 104 - - uid: 439 - components: - - type: Transform - pos: -10.5,-3.5 - parent: 104 - - uid: 442 - components: - - type: Transform - pos: -19.5,-19.5 - parent: 104 - - uid: 444 - components: - - type: Transform - pos: -10.5,1.5 - parent: 104 - - uid: 445 - components: - - type: Transform - pos: -26.5,0.5 - parent: 104 - - uid: 449 - components: - - type: Transform - pos: -18.5,-19.5 - parent: 104 - - uid: 452 - components: - - type: Transform - pos: -30.5,-19.5 - parent: 104 - - uid: 453 - components: - - type: Transform - pos: -29.5,-19.5 - parent: 104 - - uid: 454 - components: - - type: Transform - pos: -23.5,0.5 - parent: 104 - - uid: 465 - components: - - type: Transform - pos: -28.5,-19.5 - parent: 104 - - uid: 466 - components: - - type: Transform - pos: -27.5,-19.5 - parent: 104 - - uid: 467 - components: - - type: Transform - pos: -24.5,-19.5 - parent: 104 - - uid: 476 - components: - - type: Transform - pos: -22.5,-19.5 - parent: 104 - - uid: 619 - components: - - type: Transform - pos: -10.5,-12.5 - parent: 104 - - uid: 624 - components: - - type: Transform - pos: -10.5,-10.5 - parent: 104 - - uid: 625 - components: - - type: Transform - pos: -10.5,-11.5 - parent: 104 - - uid: 627 - components: - - type: Transform - pos: -10.5,-15.5 - parent: 104 - - uid: 628 - components: - - type: Transform - pos: -10.5,-16.5 - parent: 104 - - uid: 630 - components: - - type: Transform - pos: -10.5,-17.5 - parent: 104 - - uid: 657 - components: - - type: Transform - pos: -25.5,-19.5 - parent: 104 - - uid: 659 - components: - - type: Transform - pos: -32.5,-19.5 - parent: 104 - - uid: 660 - components: - - type: Transform - pos: -31.5,-19.5 - parent: 104 - - uid: 666 - components: - - type: Transform - pos: -22.5,0.5 - parent: 104 - - uid: 814 - components: - - type: Transform - pos: -6.5,-3.5 - parent: 104 - - uid: 832 - components: - - type: Transform - pos: -7.5,-3.5 - parent: 104 - - uid: 840 - components: - - type: Transform - pos: -25.5,0.5 - parent: 104 - - uid: 845 - components: - - type: Transform - pos: -10.5,-1.5 - parent: 104 - - uid: 866 - components: - - type: Transform - pos: -10.5,-2.5 - parent: 104 - - uid: 867 - components: - - type: Transform - pos: -5.5,-3.5 - parent: 104 - - uid: 868 - components: - - type: Transform - pos: -10.5,-3.5 - parent: 104 - - uid: 870 - components: - - type: Transform - pos: -9.5,-3.5 - parent: 104 - - uid: 877 - components: - - type: Transform - pos: -8.5,-3.5 - parent: 104 - - uid: 884 - components: - - type: Transform - pos: -4.5,-3.5 - parent: 104 - - uid: 920 - components: - - type: Transform - pos: -10.5,-13.5 - parent: 104 - - uid: 926 - components: - - type: Transform - pos: -12.5,0.5 - parent: 104 - - uid: 927 - components: - - type: Transform - pos: -12.5,-19.5 - parent: 104 - - uid: 929 - components: - - type: Transform - pos: -24.5,0.5 - parent: 104 - - uid: 931 - components: - - type: Transform - pos: 12.5,-19.5 - parent: 104 - - uid: 933 - components: - - type: Transform - pos: -10.5,-19.5 - parent: 104 - - uid: 934 - components: - - type: Transform - pos: -10.5,-14.5 - parent: 104 - - uid: 960 - components: - - type: Transform - pos: 12.5,-17.5 - parent: 104 - - uid: 961 - components: - - type: Transform - pos: 11.5,-16.5 - parent: 104 - - uid: 962 - components: - - type: Transform - pos: 6.5,-14.5 - parent: 104 - - uid: 963 - components: - - type: Transform - pos: -14.5,0.5 - parent: 104 - - uid: 967 - components: - - type: Transform - pos: 9.5,-16.5 - parent: 104 - - uid: 968 - components: - - type: Transform - pos: 7.5,-16.5 - parent: 104 - - uid: 969 - components: - - type: Transform - pos: -10.5,-18.5 - parent: 104 - - uid: 970 - components: - - type: Transform - pos: -33.5,-19.5 - parent: 104 - - uid: 971 - components: - - type: Transform - pos: -33.5,0.5 - parent: 104 - - uid: 973 - components: - - type: Transform - pos: 5.5,-17.5 - parent: 104 - - uid: 979 - components: - - type: Transform - pos: 1.5,-16.5 - parent: 104 - - uid: 981 - components: - - type: Transform - pos: 5.5,-14.5 - parent: 104 - - uid: 982 - components: - - type: Transform - pos: 12.5,-16.5 - parent: 104 - - uid: 983 - components: - - type: Transform - pos: 5.5,-16.5 - parent: 104 - - uid: 1081 - components: - - type: Transform - pos: -17.5,0.5 - parent: 104 - - uid: 1082 - components: - - type: Transform - pos: -11.5,0.5 - parent: 104 - - uid: 1115 - components: - - type: Transform - pos: 4.5,-14.5 - parent: 104 - - uid: 1116 - components: - - type: Transform - pos: 1.5,-12.5 - parent: 104 - - uid: 1117 - components: - - type: Transform - pos: 6.5,-16.5 - parent: 104 - - uid: 1122 - components: - - type: Transform - pos: 1.5,-14.5 - parent: 104 - - uid: 1131 - components: - - type: Transform - pos: -13.5,0.5 - parent: 104 - - uid: 1152 - components: - - type: Transform - pos: 12.5,-20.5 - parent: 104 - - uid: 1154 - components: - - type: Transform - pos: 3.5,-13.5 - parent: 104 - - uid: 1182 - components: - - type: Transform - pos: 5.5,-15.5 - parent: 104 - - uid: 1183 - components: - - type: Transform - pos: 7.5,-14.5 - parent: 104 - - uid: 1187 - components: - - type: Transform - pos: -20.5,0.5 - parent: 104 - - uid: 1189 - components: - - type: Transform - pos: -32.5,0.5 - parent: 104 - - uid: 1191 - components: - - type: Transform - pos: 3.5,-12.5 - parent: 104 - - uid: 1194 - components: - - type: Transform - pos: 2.5,-14.5 - parent: 104 - - uid: 1195 - components: - - type: Transform - pos: 8.5,-16.5 - parent: 104 - - uid: 1200 - components: - - type: Transform - pos: 1.5,-15.5 - parent: 104 - - uid: 1204 - components: - - type: Transform - pos: 10.5,-16.5 - parent: 104 - - uid: 1232 - components: - - type: Transform - pos: -10.5,6.5 - parent: 104 - - uid: 1233 - components: - - type: Transform - pos: -10.5,5.5 - parent: 104 - - uid: 1234 - components: - - type: Transform - pos: -10.5,4.5 - parent: 104 - - uid: 1235 - components: - - type: Transform - pos: -10.5,3.5 - parent: 104 - - uid: 1236 - components: - - type: Transform - pos: -10.5,2.5 - parent: 104 - - uid: 1237 - components: - - type: Transform - pos: -11.5,6.5 - parent: 104 - - uid: 1238 - components: - - type: Transform - pos: -12.5,6.5 - parent: 104 - - uid: 1239 - components: - - type: Transform - pos: -13.5,6.5 - parent: 104 - - uid: 1240 - components: - - type: Transform - pos: -14.5,6.5 - parent: 104 - - uid: 1249 - components: - - type: Transform - pos: -11.5,-19.5 - parent: 104 - - uid: 1275 - components: - - type: Transform - pos: -6.5,-2.5 - parent: 104 - - uid: 1277 - components: - - type: Transform - pos: -21.5,0.5 - parent: 104 - - uid: 1278 - components: - - type: Transform - pos: -16.5,0.5 - parent: 104 - - uid: 1281 - components: - - type: Transform - pos: -6.5,-1.5 - parent: 104 - - uid: 1282 - components: - - type: Transform - pos: -6.5,-0.5 - parent: 104 - - uid: 1283 - components: - - type: Transform - pos: -6.5,0.5 - parent: 104 - - uid: 1292 - components: - - type: Transform - pos: 3.5,1.5 - parent: 104 - - uid: 1337 - components: - - type: Transform - pos: -6.5,-4.5 - parent: 104 - - uid: 1338 - components: - - type: Transform - pos: -6.5,-5.5 - parent: 104 - - uid: 1339 - components: - - type: Transform - pos: -6.5,-6.5 - parent: 104 - - uid: 1340 - components: - - type: Transform - pos: -6.5,-7.5 - parent: 104 - - uid: 1341 - components: - - type: Transform - pos: -6.5,-8.5 - parent: 104 - - uid: 1342 - components: - - type: Transform - pos: -6.5,-9.5 - parent: 104 - - uid: 1343 - components: - - type: Transform - pos: -6.5,-10.5 - parent: 104 - - uid: 1344 - components: - - type: Transform - pos: -6.5,-11.5 - parent: 104 - - uid: 1345 - components: - - type: Transform - pos: -6.5,-12.5 - parent: 104 - - uid: 1346 - components: - - type: Transform - pos: -6.5,-13.5 - parent: 104 - - uid: 1347 - components: - - type: Transform - pos: -6.5,-14.5 - parent: 104 - - uid: 1348 - components: - - type: Transform - pos: -6.5,-15.5 - parent: 104 - - uid: 1349 - components: - - type: Transform - pos: -6.5,-16.5 - parent: 104 - - uid: 1429 - components: - - type: Transform - pos: -18.5,0.5 - parent: 104 - - uid: 1430 - components: - - type: Transform - pos: -19.5,0.5 - parent: 104 - - uid: 1439 - components: - - type: Transform - pos: 20.5,-12.5 - parent: 104 - - uid: 1449 - components: - - type: Transform - pos: 16.5,-13.5 - parent: 104 - - uid: 1450 - components: - - type: Transform - pos: 15.5,-13.5 - parent: 104 - - uid: 1451 - components: - - type: Transform - pos: 15.5,-12.5 - parent: 104 - - uid: 1452 - components: - - type: Transform - pos: 15.5,-11.5 - parent: 104 - - uid: 1453 - components: - - type: Transform - pos: 15.5,-10.5 - parent: 104 - - uid: 1454 - components: - - type: Transform - pos: 14.5,-10.5 - parent: 104 - - uid: 1455 - components: - - type: Transform - pos: 13.5,-10.5 - parent: 104 - - uid: 1456 - components: - - type: Transform - pos: 12.5,-10.5 - parent: 104 - - uid: 1457 - components: - - type: Transform - pos: 11.5,-10.5 - parent: 104 - - uid: 1458 - components: - - type: Transform - pos: 15.5,-14.5 - parent: 104 - - uid: 1459 - components: - - type: Transform - pos: 14.5,-14.5 - parent: 104 - - uid: 1460 - components: - - type: Transform - pos: 13.5,-14.5 - parent: 104 - - uid: 1461 - components: - - type: Transform - pos: 12.5,-14.5 - parent: 104 - - uid: 1473 - components: - - type: Transform - pos: -13.5,-19.5 - parent: 104 - - uid: 1474 - components: - - type: Transform - pos: 7.5,-5.5 - parent: 104 - - uid: 1475 - components: - - type: Transform - pos: 6.5,-1.5 - parent: 104 - - uid: 1476 - components: - - type: Transform - pos: 3.5,0.5 - parent: 104 - - uid: 1488 - components: - - type: Transform - pos: 6.5,-5.5 - parent: 104 - - uid: 1489 - components: - - type: Transform - pos: 8.5,-5.5 - parent: 104 - - uid: 1490 - components: - - type: Transform - pos: 6.5,-4.5 - parent: 104 - - uid: 1491 - components: - - type: Transform - pos: 6.5,-3.5 - parent: 104 - - uid: 1492 - components: - - type: Transform - pos: 6.5,-2.5 - parent: 104 - - uid: 1493 - components: - - type: Transform - pos: 6.5,-0.5 - parent: 104 - - uid: 1494 - components: - - type: Transform - pos: 5.5,-0.5 - parent: 104 - - uid: 1495 - components: - - type: Transform - pos: 4.5,-0.5 - parent: 104 - - uid: 1496 - components: - - type: Transform - pos: 4.5,0.5 - parent: 104 - - uid: 1497 - components: - - type: Transform - pos: 2.5,0.5 - parent: 104 - - uid: 1498 - components: - - type: Transform - pos: 2.5,-0.5 - parent: 104 - - uid: 1499 - components: - - type: Transform - pos: 1.5,-0.5 - parent: 104 - - uid: 1500 - components: - - type: Transform - pos: 0.5,-0.5 - parent: 104 - - uid: 1501 - components: - - type: Transform - pos: 0.5,-1.5 - parent: 104 - - uid: 1502 - components: - - type: Transform - pos: -0.5,-1.5 - parent: 104 - - uid: 1503 - components: - - type: Transform - pos: -0.5,-1.5 - parent: 104 - - uid: 1504 - components: - - type: Transform - pos: -0.5,-2.5 - parent: 104 - - uid: 1505 - components: - - type: Transform - pos: -0.5,-3.5 - parent: 104 - - uid: 1506 - components: - - type: Transform - pos: -0.5,-4.5 - parent: 104 - - uid: 1507 - components: - - type: Transform - pos: -0.5,-5.5 - parent: 104 - - uid: 1508 - components: - - type: Transform - pos: -0.5,-6.5 - parent: 104 - - uid: 1509 - components: - - type: Transform - pos: -0.5,-7.5 - parent: 104 - - uid: 1510 - components: - - type: Transform - pos: 0.5,-7.5 - parent: 104 - - uid: 1511 - components: - - type: Transform - pos: 1.5,-7.5 - parent: 104 - - uid: 1512 - components: - - type: Transform - pos: 2.5,-7.5 - parent: 104 - - uid: 1513 - components: - - type: Transform - pos: 4.5,-7.5 - parent: 104 - - uid: 1514 - components: - - type: Transform - pos: 3.5,-7.5 - parent: 104 - - uid: 1515 - components: - - type: Transform - pos: 5.5,-7.5 - parent: 104 - - uid: 1516 - components: - - type: Transform - pos: 5.5,-8.5 - parent: 104 - - uid: 1517 - components: - - type: Transform - pos: 6.5,-8.5 - parent: 104 - - uid: 1518 - components: - - type: Transform - pos: 7.5,-8.5 - parent: 104 - - uid: 1519 - components: - - type: Transform - pos: 8.5,-8.5 - parent: 104 - - uid: 1520 - components: - - type: Transform - pos: 9.5,-8.5 - parent: 104 - - uid: 1521 - components: - - type: Transform - pos: 10.5,-8.5 - parent: 104 - - uid: 1522 - components: - - type: Transform - pos: 10.5,-7.5 - parent: 104 - - uid: 1523 - components: - - type: Transform - pos: 11.5,-7.5 - parent: 104 - - uid: 1524 - components: - - type: Transform - pos: 12.5,-7.5 - parent: 104 - - uid: 1525 - components: - - type: Transform - pos: 13.5,-7.5 - parent: 104 - - uid: 1526 - components: - - type: Transform - pos: 14.5,-7.5 - parent: 104 - - uid: 1527 - components: - - type: Transform - pos: 14.5,-7.5 - parent: 104 - - uid: 1528 - components: - - type: Transform - pos: 15.5,-7.5 - parent: 104 - - uid: 1529 - components: - - type: Transform - pos: 14.5,-6.5 - parent: 104 - - uid: 1530 - components: - - type: Transform - pos: 14.5,-5.5 - parent: 104 - - uid: 1531 - components: - - type: Transform - pos: 14.5,-4.5 - parent: 104 - - uid: 1533 - components: - - type: Transform - pos: -29.5,0.5 - parent: 104 - - uid: 1538 - components: - - type: Transform - pos: 7.5,-0.5 - parent: 104 - - uid: 1539 - components: - - type: Transform - pos: 8.5,-0.5 - parent: 104 - - uid: 1540 - components: - - type: Transform - pos: 9.5,-0.5 - parent: 104 - - uid: 1541 - components: - - type: Transform - pos: 8.5,-0.5 - parent: 104 - - uid: 1543 - components: - - type: Transform - pos: 10.5,-0.5 - parent: 104 - - uid: 1575 - components: - - type: Transform - pos: 21.5,-5.5 - parent: 104 - - uid: 1576 - components: - - type: Transform - pos: 20.5,-5.5 - parent: 104 - - uid: 1577 - components: - - type: Transform - pos: 19.5,-5.5 - parent: 104 - - uid: 1578 - components: - - type: Transform - pos: 19.5,-6.5 - parent: 104 - - uid: 1579 - components: - - type: Transform - pos: 19.5,-7.5 - parent: 104 - - uid: 1580 - components: - - type: Transform - pos: 19.5,-9.5 - parent: 104 - - uid: 1581 - components: - - type: Transform - pos: 19.5,-9.5 - parent: 104 - - uid: 1582 - components: - - type: Transform - pos: 19.5,-8.5 - parent: 104 - - uid: 1583 - components: - - type: Transform - pos: 20.5,-9.5 - parent: 104 - - uid: 1584 - components: - - type: Transform - pos: 21.5,-9.5 - parent: 104 - - uid: 1585 - components: - - type: Transform - pos: 21.5,-4.5 - parent: 104 - - uid: 1586 - components: - - type: Transform - pos: 18.5,-7.5 - parent: 104 - - uid: 1587 - components: - - type: Transform - pos: 17.5,-7.5 - parent: 104 - - uid: 1588 - components: - - type: Transform - pos: 21.5,-8.5 - parent: 104 - - uid: 1589 - components: - - type: Transform - pos: 21.5,-7.5 - parent: 104 - - uid: 1590 - components: - - type: Transform - pos: 21.5,-6.5 - parent: 104 - - uid: 1591 - components: - - type: Transform - pos: 10.5,-1.5 - parent: 104 - - uid: 1592 - components: - - type: Transform - pos: 10.5,-3.5 - parent: 104 - - uid: 1593 - components: - - type: Transform - pos: 10.5,-2.5 - parent: 104 - - uid: 1595 - components: - - type: Transform - pos: -1.5,-3.5 - parent: 104 - - uid: 1596 - components: - - type: Transform - pos: -2.5,-3.5 - parent: 104 - - uid: 1597 - components: - - type: Transform - pos: -3.5,-3.5 - parent: 104 - - uid: 1941 - components: - - type: Transform - pos: -28.5,0.5 - parent: 104 - - uid: 1948 - components: - - type: Transform - pos: -14.5,-19.5 - parent: 104 - - uid: 2301 - components: - - type: Transform - pos: 12.5,-21.5 - parent: 104 - - uid: 2302 - components: - - type: Transform - pos: 11.5,-21.5 - parent: 104 - - uid: 2303 - components: - - type: Transform - pos: 13.5,-21.5 - parent: 104 - - uid: 2304 - components: - - type: Transform - pos: 0.5,-14.5 - parent: 104 - - uid: 2305 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 104 - - uid: 2306 - components: - - type: Transform - pos: -1.5,-14.5 - parent: 104 - - uid: 2307 - components: - - type: Transform - pos: -2.5,-14.5 - parent: 104 - - uid: 2374 - components: - - type: Transform - pos: 20.5,-12.5 - parent: 104 - - uid: 2375 - components: - - type: Transform - pos: 20.5,-13.5 - parent: 104 - - uid: 2376 - components: - - type: Transform - pos: 20.5,-14.5 - parent: 104 - - uid: 2377 - components: - - type: Transform - pos: 20.5,-15.5 - parent: 104 - - uid: 2378 - components: - - type: Transform - pos: 20.5,-16.5 - parent: 104 - - uid: 2379 - components: - - type: Transform - pos: 20.5,-17.5 - parent: 104 - - uid: 2381 - components: - - type: Transform - pos: 20.5,-18.5 - parent: 104 - - uid: 2382 - components: - - type: Transform - pos: 20.5,-19.5 - parent: 104 - - uid: 2383 - components: - - type: Transform - pos: 19.5,-17.5 - parent: 104 - - uid: 2384 - components: - - type: Transform - pos: 18.5,-17.5 - parent: 104 - - uid: 2385 - components: - - type: Transform - pos: 21.5,-17.5 - parent: 104 - - uid: 2386 - components: - - type: Transform - pos: 22.5,-17.5 - parent: 104 - - uid: 2387 - components: - - type: Transform - pos: 23.5,-17.5 - parent: 104 - - uid: 2388 - components: - - type: Transform - pos: 24.5,-17.5 - parent: 104 - - uid: 2389 - components: - - type: Transform - pos: 25.5,-17.5 - parent: 104 - - uid: 2390 - components: - - type: Transform - pos: 26.5,-17.5 - parent: 104 - - uid: 2391 - components: - - type: Transform - pos: 25.5,-18.5 - parent: 104 - - uid: 2392 - components: - - type: Transform - pos: 25.5,-19.5 - parent: 104 - - uid: 2393 - components: - - type: Transform - pos: 25.5,-16.5 - parent: 104 - - uid: 2394 - components: - - type: Transform - pos: 25.5,-15.5 - parent: 104 - - uid: 2395 - components: - - type: Transform - pos: 25.5,-14.5 - parent: 104 - - uid: 2396 - components: - - type: Transform - pos: 25.5,-13.5 - parent: 104 - - uid: 2439 - components: - - type: Transform - pos: 13.5,-9.5 - parent: 104 - - uid: 2440 - components: - - type: Transform - pos: 5.5,-6.5 - parent: 104 - - uid: 2441 - components: - - type: Transform - pos: 5.5,-5.5 - parent: 104 - - uid: 3378 - components: - - type: Transform - pos: 7.5,-9.5 - parent: 104 - - uid: 3379 - components: - - type: Transform - pos: 7.5,-10.5 - parent: 104 - - uid: 3380 - components: - - type: Transform - pos: 6.5,-10.5 - parent: 104 - - uid: 3381 - components: - - type: Transform - pos: 5.5,-10.5 - parent: 104 - - uid: 3382 - components: - - type: Transform - pos: 4.5,-10.5 - parent: 104 - - uid: 3383 - components: - - type: Transform - pos: 3.5,-10.5 - parent: 104 - - uid: 3384 - components: - - type: Transform - pos: 3.5,-11.5 - parent: 104 -- proto: CableApcStack - entities: - - uid: 211 - components: - - type: Transform - pos: 11.428643,-10.548745 - parent: 104 - - uid: 1324 - components: - - type: Transform - pos: 4.439247,-17.36072 - parent: 104 -- proto: CableHV - entities: - - uid: 2334 - components: - - type: Transform - pos: 17.5,-19.5 - parent: 104 - - uid: 2335 - components: - - type: Transform - pos: 17.5,-18.5 - parent: 104 - - uid: 2336 - components: - - type: Transform - pos: 17.5,-17.5 - parent: 104 - - uid: 2337 - components: - - type: Transform - pos: 18.5,-19.5 - parent: 104 - - uid: 2338 - components: - - type: Transform - pos: 19.5,-19.5 - parent: 104 - - uid: 2339 - components: - - type: Transform - pos: 20.5,-19.5 - parent: 104 - - uid: 2340 - components: - - type: Transform - pos: 21.5,-19.5 - parent: 104 - - uid: 2341 - components: - - type: Transform - pos: 22.5,-19.5 - parent: 104 - - uid: 2342 - components: - - type: Transform - pos: 23.5,-19.5 - parent: 104 - - uid: 2343 - components: - - type: Transform - pos: 24.5,-19.5 - parent: 104 - - uid: 2344 - components: - - type: Transform - pos: 25.5,-19.5 - parent: 104 - - uid: 2345 - components: - - type: Transform - pos: 26.5,-19.5 - parent: 104 - - uid: 2346 - components: - - type: Transform - pos: 27.5,-19.5 - parent: 104 - - uid: 2347 - components: - - type: Transform - pos: 27.5,-18.5 - parent: 104 - - uid: 2348 - components: - - type: Transform - pos: 27.5,-17.5 - parent: 104 - - uid: 2352 - components: - - type: Transform - pos: 21.5,-18.5 - parent: 104 - - uid: 2353 - components: - - type: Transform - pos: 22.5,-18.5 - parent: 104 - - uid: 2354 - components: - - type: Transform - pos: 23.5,-18.5 - parent: 104 - - uid: 2355 - components: - - type: Transform - pos: 22.5,-17.5 - parent: 104 - - uid: 2356 - components: - - type: Transform - pos: 23.5,-17.5 - parent: 104 - - uid: 2357 - components: - - type: Transform - pos: 24.5,-17.5 - parent: 104 - - uid: 2358 - components: - - type: Transform - pos: 24.5,-16.5 - parent: 104 - - uid: 2359 - components: - - type: Transform - pos: 24.5,-15.5 - parent: 104 - - uid: 2360 - components: - - type: Transform - pos: 24.5,-14.5 - parent: 104 - - uid: 2361 - components: - - type: Transform - pos: 21.5,-17.5 - parent: 104 - - uid: 2362 - components: - - type: Transform - pos: 25.5,-14.5 - parent: 104 - - uid: 2363 - components: - - type: Transform - pos: 26.5,-14.5 - parent: 104 - - uid: 2364 - components: - - type: Transform - pos: 27.5,-14.5 - parent: 104 - - uid: 2365 - components: - - type: Transform - pos: 27.5,-13.5 - parent: 104 -- proto: CableHVStack - entities: - - uid: 209 - components: - - type: Transform - pos: 11.334893,-10.282843 - parent: 104 -- proto: CableMV - entities: - - uid: 240 - components: - - type: Transform - pos: 17.5,-13.5 - parent: 104 - - uid: 1438 - components: - - type: Transform - pos: 20.5,-12.5 - parent: 104 - - uid: 1462 - components: - - type: Transform - pos: 16.5,-13.5 - parent: 104 - - uid: 1463 - components: - - type: Transform - pos: 15.5,-13.5 - parent: 104 - - uid: 1464 - components: - - type: Transform - pos: 15.5,-12.5 - parent: 104 - - uid: 1465 - components: - - type: Transform - pos: 15.5,-11.5 - parent: 104 - - uid: 1466 - components: - - type: Transform - pos: 15.5,-10.5 - parent: 104 - - uid: 1467 - components: - - type: Transform - pos: 14.5,-10.5 - parent: 104 - - uid: 1468 - components: - - type: Transform - pos: 13.5,-10.5 - parent: 104 - - uid: 1469 - components: - - type: Transform - pos: 13.5,-9.5 - parent: 104 - - uid: 1478 - components: - - type: Transform - pos: 12.5,-10.5 - parent: 104 - - uid: 1479 - components: - - type: Transform - pos: 11.5,-10.5 - parent: 104 - - uid: 1480 - components: - - type: Transform - pos: 10.5,-10.5 - parent: 104 - - uid: 1481 - components: - - type: Transform - pos: 9.5,-10.5 - parent: 104 - - uid: 1482 - components: - - type: Transform - pos: 9.5,-9.5 - parent: 104 - - uid: 1483 - components: - - type: Transform - pos: 9.5,-8.5 - parent: 104 - - uid: 1484 - components: - - type: Transform - pos: 9.5,-7.5 - parent: 104 - - uid: 1485 - components: - - type: Transform - pos: 9.5,-6.5 - parent: 104 - - uid: 1486 - components: - - type: Transform - pos: 8.5,-6.5 - parent: 104 - - uid: 1487 - components: - - type: Transform - pos: 8.5,-5.5 - parent: 104 - - uid: 2322 - components: - - type: Transform - pos: 18.5,-13.5 - parent: 104 - - uid: 2323 - components: - - type: Transform - pos: 19.5,-13.5 - parent: 104 - - uid: 2324 - components: - - type: Transform - pos: 20.5,-13.5 - parent: 104 - - uid: 2325 - components: - - type: Transform - pos: 20.5,-12.5 - parent: 104 - - uid: 2367 - components: - - type: Transform - pos: 27.5,-13.5 - parent: 104 - - uid: 2368 - components: - - type: Transform - pos: 26.5,-13.5 - parent: 104 - - uid: 2369 - components: - - type: Transform - pos: 25.5,-13.5 - parent: 104 - - uid: 2370 - components: - - type: Transform - pos: 24.5,-13.5 - parent: 104 - - uid: 2371 - components: - - type: Transform - pos: 23.5,-13.5 - parent: 104 - - uid: 2372 - components: - - type: Transform - pos: 22.5,-13.5 - parent: 104 - - uid: 2373 - components: - - type: Transform - pos: 21.5,-13.5 - parent: 104 -- proto: CableMVStack - entities: - - uid: 210 - components: - - type: Transform - pos: 11.381768,-10.407973 - parent: 104 -- proto: CableTerminal - entities: - - uid: 2349 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 21.5,-19.5 - parent: 104 - - uid: 2350 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 22.5,-19.5 - parent: 104 - - uid: 2351 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 23.5,-19.5 - parent: 104 -- proto: Carpet - entities: - - uid: 2459 - components: - - type: Transform - pos: 14.5,-4.5 - parent: 104 - - uid: 2460 - components: - - type: Transform - pos: 14.5,-3.5 - parent: 104 -- proto: Catwalk - entities: - - uid: 217 - components: - - type: Transform - pos: -12.5,-19.5 - parent: 104 - - uid: 218 - components: - - type: Transform - pos: -26.5,-20.5 - parent: 104 - - uid: 256 - components: - - type: Transform - pos: -29.5,2.5 - parent: 104 - - uid: 264 - components: - - type: Transform - pos: -24.5,-19.5 - parent: 104 - - uid: 278 - components: - - type: Transform - pos: -21.5,-23.5 - parent: 104 - - uid: 381 - components: - - type: Transform - pos: -20.5,-21.5 - parent: 104 - - uid: 382 - components: - - type: Transform - pos: -20.5,-23.5 - parent: 104 - - uid: 383 - components: - - type: Transform - pos: -20.5,-22.5 - parent: 104 - - uid: 384 - components: - - type: Transform - pos: -26.5,-19.5 - parent: 104 - - uid: 385 - components: - - type: Transform - pos: -30.5,4.5 - parent: 104 - - uid: 386 - components: - - type: Transform - pos: -30.5,-19.5 - parent: 104 - - uid: 387 - components: - - type: Transform - pos: -14.5,-20.5 - parent: 104 - - uid: 388 - components: - - type: Transform - pos: -29.5,3.5 - parent: 104 - - uid: 389 - components: - - type: Transform - pos: -14.5,-22.5 - parent: 104 - - uid: 391 - components: - - type: Transform - pos: -33.5,3.5 - parent: 104 - - uid: 392 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -34.5,-21.5 - parent: 104 - - uid: 393 - components: - - type: Transform - pos: -19.5,-22.5 - parent: 104 - - uid: 394 - components: - - type: Transform - pos: -32.5,4.5 - parent: 104 - - uid: 395 - components: - - type: Transform - pos: -24.5,-21.5 - parent: 104 - - uid: 396 - components: - - type: Transform - pos: -10.5,-16.5 - parent: 104 - - uid: 398 - components: - - type: Transform - pos: -9.5,-15.5 - parent: 104 - - uid: 399 - components: - - type: Transform - pos: -10.5,-15.5 - parent: 104 - - uid: 400 - components: - - type: Transform - pos: -9.5,-14.5 - parent: 104 - - uid: 413 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -24.5,3.5 - parent: 104 - - uid: 414 - components: - - type: Transform - pos: -35.5,-19.5 - parent: 104 - - uid: 431 - components: - - type: Transform - pos: -9.5,-13.5 - parent: 104 - - uid: 440 - components: - - type: Transform - pos: -16.5,-21.5 - parent: 104 - - uid: 450 - components: - - type: Transform - pos: -33.5,-20.5 - parent: 104 - - uid: 451 - components: - - type: Transform - pos: -34.5,-19.5 - parent: 104 - - uid: 464 - components: - - type: Transform - pos: -34.5,-20.5 - parent: 104 - - uid: 468 - components: - - type: Transform - pos: -9.5,-21.5 - parent: 104 - - uid: 470 - components: - - type: Transform - pos: -9.5,-20.5 - parent: 104 - - uid: 490 - components: - - type: Transform - pos: -13.5,0.5 - parent: 104 - - uid: 491 - components: - - type: Transform - pos: -12.5,1.5 - parent: 104 - - uid: 494 - components: - - type: Transform - pos: -11.5,1.5 - parent: 104 - - uid: 496 - components: - - type: Transform - pos: -14.5,1.5 - parent: 104 - - uid: 499 - components: - - type: Transform - pos: -9.5,-19.5 - parent: 104 - - uid: 500 - components: - - type: Transform - pos: -9.5,-18.5 - parent: 104 - - uid: 501 - components: - - type: Transform - pos: -30.5,0.5 - parent: 104 - - uid: 503 - components: - - type: Transform - pos: -29.5,1.5 - parent: 104 - - uid: 508 - components: - - type: Transform - pos: -12.5,0.5 - parent: 104 - - uid: 510 - components: - - type: Transform - pos: -31.5,0.5 - parent: 104 - - uid: 514 - components: - - type: Transform - pos: -27.5,0.5 - parent: 104 - - uid: 515 - components: - - type: Transform - pos: -30.5,1.5 - parent: 104 - - uid: 516 - components: - - type: Transform - pos: -9.5,-17.5 - parent: 104 - - uid: 517 - components: - - type: Transform - pos: -9.5,-16.5 - parent: 104 - - uid: 518 - components: - - type: Transform - pos: -10.5,-14.5 - parent: 104 - - uid: 519 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -31.5,-21.5 - parent: 104 - - uid: 520 - components: - - type: Transform - pos: -28.5,0.5 - parent: 104 - - uid: 583 - components: - - type: Transform - pos: -26.5,1.5 - parent: 104 - - uid: 584 - components: - - type: Transform - pos: -10.5,-13.5 - parent: 104 - - uid: 616 - components: - - type: Transform - pos: -31.5,3.5 - parent: 104 - - uid: 617 - components: - - type: Transform - pos: -14.5,-23.5 - parent: 104 - - uid: 626 - components: - - type: Transform - pos: -16.5,-22.5 - parent: 104 - - uid: 631 - components: - - type: Transform - pos: -9.5,-23.5 - parent: 104 - - uid: 633 - components: - - type: Transform - pos: -9.5,-22.5 - parent: 104 - - uid: 634 - components: - - type: Transform - pos: -16.5,-23.5 - parent: 104 - - uid: 635 - components: - - type: Transform - pos: -25.5,0.5 - parent: 104 - - uid: 636 - components: - - type: Transform - pos: -27.5,1.5 - parent: 104 - - uid: 638 - components: - - type: Transform - pos: -26.5,0.5 - parent: 104 - - uid: 639 - components: - - type: Transform - pos: -24.5,1.5 - parent: 104 - - uid: 641 - components: - - type: Transform - pos: -11.5,-22.5 - parent: 104 - - uid: 642 - components: - - type: Transform - pos: -11.5,-23.5 - parent: 104 - - uid: 643 - components: - - type: Transform - pos: -10.5,-23.5 - parent: 104 - - uid: 644 - components: - - type: Transform - pos: -24.5,0.5 - parent: 104 - - uid: 645 - components: - - type: Transform - pos: -25.5,1.5 - parent: 104 - - uid: 646 - components: - - type: Transform - pos: -15.5,0.5 - parent: 104 - - uid: 647 - components: - - type: Transform - pos: -13.5,-23.5 - parent: 104 - - uid: 652 - components: - - type: Transform - pos: -14.5,0.5 - parent: 104 - - uid: 653 - components: - - type: Transform - pos: -13.5,1.5 - parent: 104 - - uid: 654 - components: - - type: Transform - pos: -29.5,0.5 - parent: 104 - - uid: 655 - components: - - type: Transform - pos: -28.5,1.5 - parent: 104 - - uid: 656 - components: - - type: Transform - pos: -35.5,-20.5 - parent: 104 - - uid: 658 - components: - - type: Transform - pos: -11.5,0.5 - parent: 104 - - uid: 661 - components: - - type: Transform - pos: -20.5,-19.5 - parent: 104 - - uid: 662 - components: - - type: Transform - pos: -18.5,0.5 - parent: 104 - - uid: 663 - components: - - type: Transform - pos: -12.5,-23.5 - parent: 104 - - uid: 664 - components: - - type: Transform - pos: -17.5,1.5 - parent: 104 - - uid: 665 - components: - - type: Transform - pos: -21.5,0.5 - parent: 104 - - uid: 667 - components: - - type: Transform - pos: -20.5,-20.5 - parent: 104 - - uid: 668 - components: - - type: Transform - pos: -32.5,1.5 - parent: 104 - - uid: 669 - components: - - type: Transform - pos: -31.5,1.5 - parent: 104 - - uid: 707 - components: - - type: Transform - pos: -32.5,0.5 - parent: 104 - - uid: 713 - components: - - type: Transform - pos: -20.5,1.5 - parent: 104 - - uid: 715 - components: - - type: Transform - pos: -10.5,-22.5 - parent: 104 - - uid: 736 - components: - - type: Transform - pos: -10.5,-21.5 - parent: 104 - - uid: 737 - components: - - type: Transform - pos: -10.5,-20.5 - parent: 104 - - uid: 738 - components: - - type: Transform - pos: -20.5,0.5 - parent: 104 - - uid: 742 - components: - - type: Transform - pos: -10.5,-19.5 - parent: 104 - - uid: 748 - components: - - type: Transform - pos: -10.5,-18.5 - parent: 104 - - uid: 749 - components: - - type: Transform - pos: -10.5,-17.5 - parent: 104 - - uid: 760 - components: - - type: Transform - pos: -14.5,-21.5 - parent: 104 - - uid: 761 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -26.5,-21.5 - parent: 104 - - uid: 769 - components: - - type: Transform - pos: -33.5,4.5 - parent: 104 - - uid: 772 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -33.5,-21.5 - parent: 104 - - uid: 799 - components: - - type: Transform - pos: -26.5,3.5 - parent: 104 - - uid: 800 - components: - - type: Transform - pos: -34.5,4.5 - parent: 104 - - uid: 812 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -32.5,-21.5 - parent: 104 - - uid: 813 - components: - - type: Transform - pos: -29.5,4.5 - parent: 104 - - uid: 815 - components: - - type: Transform - pos: -17.5,-23.5 - parent: 104 - - uid: 816 - components: - - type: Transform - pos: -34.5,1.5 - parent: 104 - - uid: 817 - components: - - type: Transform - pos: -24.5,-22.5 - parent: 104 - - uid: 818 - components: - - type: Transform - pos: -24.5,-23.5 - parent: 104 - - uid: 820 - components: - - type: Transform - pos: -21.5,-21.5 - parent: 104 - - uid: 821 - components: - - type: Transform - pos: -21.5,-22.5 - parent: 104 - - uid: 822 - components: - - type: Transform - pos: -27.5,3.5 - parent: 104 - - uid: 823 - components: - - type: Transform - pos: -26.5,4.5 - parent: 104 - - uid: 824 - components: - - type: Transform - pos: -25.5,4.5 - parent: 104 - - uid: 825 - components: - - type: Transform - pos: -25.5,2.5 - parent: 104 - - uid: 826 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -33.5,-23.5 - parent: 104 - - uid: 827 - components: - - type: Transform - pos: -27.5,4.5 - parent: 104 - - uid: 828 - components: - - type: Transform - pos: -27.5,2.5 - parent: 104 - - uid: 829 - components: - - type: Transform - pos: -26.5,2.5 - parent: 104 - - uid: 830 - components: - - type: Transform - pos: -25.5,3.5 - parent: 104 - - uid: 831 - components: - - type: Transform - pos: -19.5,-20.5 - parent: 104 - - uid: 833 - components: - - type: Transform - pos: -18.5,-21.5 - parent: 104 - - uid: 834 - components: - - type: Transform - pos: -17.5,0.5 - parent: 104 - - uid: 835 - components: - - type: Transform - pos: -15.5,1.5 - parent: 104 - - uid: 836 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -29.5,-21.5 - parent: 104 - - uid: 837 - components: - - type: Transform - pos: -16.5,1.5 - parent: 104 - - uid: 839 - components: - - type: Transform - pos: -32.5,3.5 - parent: 104 - - uid: 841 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -33.5,-22.5 - parent: 104 - - uid: 842 - components: - - type: Transform - pos: -17.5,-22.5 - parent: 104 - - uid: 843 - components: - - type: Transform - pos: -18.5,-23.5 - parent: 104 - - uid: 844 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -32.5,-22.5 - parent: 104 - - uid: 846 - components: - - type: Transform - pos: -34.5,0.5 - parent: 104 - - uid: 847 - components: - - type: Transform - pos: -19.5,-21.5 - parent: 104 - - uid: 848 - components: - - type: Transform - pos: -16.5,0.5 - parent: 104 - - uid: 849 - components: - - type: Transform - pos: -18.5,-22.5 - parent: 104 - - uid: 850 - components: - - type: Transform - pos: -17.5,-21.5 - parent: 104 - - uid: 851 - components: - - type: Transform - pos: -17.5,-19.5 - parent: 104 - - uid: 852 - components: - - type: Transform - pos: -18.5,-19.5 - parent: 104 - - uid: 853 - components: - - type: Transform - pos: -19.5,1.5 - parent: 104 - - uid: 854 - components: - - type: Transform - pos: -17.5,-20.5 - parent: 104 - - uid: 855 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -30.5,-21.5 - parent: 104 - - uid: 856 - components: - - type: Transform - pos: -31.5,2.5 - parent: 104 - - uid: 857 - components: - - type: Transform - pos: -19.5,0.5 - parent: 104 - - uid: 858 - components: - - type: Transform - pos: -30.5,3.5 - parent: 104 - - uid: 859 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -28.5,-21.5 - parent: 104 - - uid: 860 - components: - - type: Transform - pos: -33.5,1.5 - parent: 104 - - uid: 861 - components: - - type: Transform - pos: -33.5,0.5 - parent: 104 - - uid: 862 - components: - - type: Transform - pos: -31.5,-20.5 - parent: 104 - - uid: 863 - components: - - type: Transform - pos: -32.5,-19.5 - parent: 104 - - uid: 864 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -27.5,-21.5 - parent: 104 - - uid: 865 - components: - - type: Transform - pos: -28.5,2.5 - parent: 104 - - uid: 869 - components: - - type: Transform - pos: -32.5,2.5 - parent: 104 - - uid: 871 - components: - - type: Transform - pos: -31.5,4.5 - parent: 104 - - uid: 872 - components: - - type: Transform - pos: -30.5,2.5 - parent: 104 - - uid: 873 - components: - - type: Transform - pos: -18.5,1.5 - parent: 104 - - uid: 874 - components: - - type: Transform - pos: -15.5,4.5 - parent: 104 - - uid: 875 - components: - - type: Transform - pos: -15.5,3.5 - parent: 104 - - uid: 876 - components: - - type: Transform - pos: -15.5,2.5 - parent: 104 - - uid: 878 - components: - - type: Transform - pos: -23.5,-21.5 - parent: 104 - - uid: 879 - components: - - type: Transform - pos: -23.5,-22.5 - parent: 104 - - uid: 880 - components: - - type: Transform - pos: -23.5,-23.5 - parent: 104 - - uid: 881 - components: - - type: Transform - pos: -18.5,-20.5 - parent: 104 - - uid: 882 - components: - - type: Transform - pos: -22.5,-21.5 - parent: 104 - - uid: 883 - components: - - type: Transform - pos: -19.5,-19.5 - parent: 104 - - uid: 885 - components: - - type: Transform - pos: -32.5,-20.5 - parent: 104 - - uid: 886 - components: - - type: Transform - pos: -33.5,-19.5 - parent: 104 - - uid: 887 - components: - - type: Transform - pos: -28.5,4.5 - parent: 104 - - uid: 888 - components: - - type: Transform - pos: -28.5,3.5 - parent: 104 - - uid: 889 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -34.5,-23.5 - parent: 104 - - uid: 890 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -32.5,-23.5 - parent: 104 - - uid: 891 - components: - - type: Transform - pos: -33.5,2.5 - parent: 104 - - uid: 892 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -31.5,-22.5 - parent: 104 - - uid: 893 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -31.5,-23.5 - parent: 104 - - uid: 894 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -30.5,-22.5 - parent: 104 - - uid: 895 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -30.5,-23.5 - parent: 104 - - uid: 896 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -29.5,-22.5 - parent: 104 - - uid: 897 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -29.5,-23.5 - parent: 104 - - uid: 898 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -28.5,-22.5 - parent: 104 - - uid: 899 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -28.5,-23.5 - parent: 104 - - uid: 900 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -27.5,-22.5 - parent: 104 - - uid: 901 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -26.5,-22.5 - parent: 104 - - uid: 902 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -26.5,-23.5 - parent: 104 - - uid: 903 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -25.5,-22.5 - parent: 104 - - uid: 904 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -25.5,-23.5 - parent: 104 - - uid: 905 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -35.5,-22.5 - parent: 104 - - uid: 906 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -35.5,-23.5 - parent: 104 - - uid: 907 - components: - - type: Transform - pos: -19.5,-23.5 - parent: 104 - - uid: 908 - components: - - type: Transform - pos: -21.5,-19.5 - parent: 104 - - uid: 909 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -25.5,-21.5 - parent: 104 - - uid: 910 - components: - - type: Transform - pos: -31.5,-19.5 - parent: 104 - - uid: 911 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -35.5,-21.5 - parent: 104 - - uid: 912 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -34.5,-22.5 - parent: 104 - - uid: 913 - components: - - type: Transform - pos: -23.5,4.5 - parent: 104 - - uid: 914 - components: - - type: Transform - pos: -21.5,-20.5 - parent: 104 - - uid: 915 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -23.5,3.5 - parent: 104 - - uid: 916 - components: - - type: Transform - pos: -12.5,4.5 - parent: 104 - - uid: 917 - components: - - type: Transform - pos: -12.5,3.5 - parent: 104 - - uid: 918 - components: - - type: Transform - pos: -12.5,2.5 - parent: 104 - - uid: 919 - components: - - type: Transform - pos: -22.5,-22.5 - parent: 104 - - uid: 921 - components: - - type: Transform - pos: -15.5,-22.5 - parent: 104 - - uid: 922 - components: - - type: Transform - pos: -30.5,-20.5 - parent: 104 - - uid: 923 - components: - - type: Transform - pos: -13.5,4.5 - parent: 104 - - uid: 924 - components: - - type: Transform - pos: -13.5,3.5 - parent: 104 - - uid: 925 - components: - - type: Transform - pos: -13.5,2.5 - parent: 104 - - uid: 928 - components: - - type: Transform - pos: -15.5,-23.5 - parent: 104 - - uid: 930 - components: - - type: Transform - pos: -14.5,4.5 - parent: 104 - - uid: 932 - components: - - type: Transform - pos: -14.5,2.5 - parent: 104 - - uid: 935 - components: - - type: Transform - pos: -22.5,-20.5 - parent: 104 - - uid: 936 - components: - - type: Transform - pos: -16.5,4.5 - parent: 104 - - uid: 937 - components: - - type: Transform - pos: -16.5,3.5 - parent: 104 - - uid: 938 - components: - - type: Transform - pos: -16.5,2.5 - parent: 104 - - uid: 939 - components: - - type: Transform - pos: -27.5,-20.5 - parent: 104 - - uid: 940 - components: - - type: Transform - pos: -15.5,-21.5 - parent: 104 - - uid: 941 - components: - - type: Transform - pos: -13.5,-19.5 - parent: 104 - - uid: 943 - components: - - type: Transform - pos: -17.5,4.5 - parent: 104 - - uid: 944 - components: - - type: Transform - pos: -17.5,3.5 - parent: 104 - - uid: 945 - components: - - type: Transform - pos: -17.5,2.5 - parent: 104 - - uid: 946 - components: - - type: Transform - pos: -12.5,-22.5 - parent: 104 - - uid: 947 - components: - - type: Transform - pos: -22.5,-23.5 - parent: 104 - - uid: 948 - components: - - type: Transform - pos: -13.5,-21.5 - parent: 104 - - uid: 949 - components: - - type: Transform - pos: -22.5,1.5 - parent: 104 - - uid: 950 - components: - - type: Transform - pos: -18.5,4.5 - parent: 104 - - uid: 951 - components: - - type: Transform - pos: -18.5,3.5 - parent: 104 - - uid: 952 - components: - - type: Transform - pos: -18.5,2.5 - parent: 104 - - uid: 953 - components: - - type: Transform - pos: -12.5,-21.5 - parent: 104 - - uid: 954 - components: - - type: Transform - pos: -13.5,-22.5 - parent: 104 - - uid: 958 - components: - - type: Transform - pos: -19.5,3.5 - parent: 104 - - uid: 959 - components: - - type: Transform - pos: -19.5,2.5 - parent: 104 - - uid: 964 - components: - - type: Transform - pos: -11.5,4.5 - parent: 104 - - uid: 965 - components: - - type: Transform - pos: -11.5,3.5 - parent: 104 - - uid: 966 - components: - - type: Transform - pos: -11.5,2.5 - parent: 104 - - uid: 972 - components: - - type: Transform - pos: -11.5,-21.5 - parent: 104 - - uid: 984 - components: - - type: Transform - pos: -9.5,-9.5 - parent: 104 - - uid: 985 - components: - - type: Transform - pos: -9.5,-10.5 - parent: 104 - - uid: 986 - components: - - type: Transform - pos: -9.5,-11.5 - parent: 104 - - uid: 987 - components: - - type: Transform - pos: -9.5,-12.5 - parent: 104 - - uid: 988 - components: - - type: Transform - pos: -10.5,4.5 - parent: 104 - - uid: 989 - components: - - type: Transform - pos: -10.5,3.5 - parent: 104 - - uid: 990 - components: - - type: Transform - pos: -10.5,2.5 - parent: 104 - - uid: 991 - components: - - type: Transform - pos: -10.5,1.5 - parent: 104 - - uid: 992 - components: - - type: Transform - pos: -10.5,0.5 - parent: 104 - - uid: 993 - components: - - type: Transform - pos: -10.5,-0.5 - parent: 104 - - uid: 994 - components: - - type: Transform - pos: -10.5,-1.5 - parent: 104 - - uid: 995 - components: - - type: Transform - pos: -10.5,-2.5 - parent: 104 - - uid: 996 - components: - - type: Transform - pos: -10.5,-3.5 - parent: 104 - - uid: 997 - components: - - type: Transform - pos: -10.5,-4.5 - parent: 104 - - uid: 998 - components: - - type: Transform - pos: -10.5,-5.5 - parent: 104 - - uid: 999 - components: - - type: Transform - pos: -10.5,-6.5 - parent: 104 - - uid: 1000 - components: - - type: Transform - pos: -10.5,-7.5 - parent: 104 - - uid: 1001 - components: - - type: Transform - pos: -10.5,-8.5 - parent: 104 - - uid: 1002 - components: - - type: Transform - pos: -10.5,-9.5 - parent: 104 - - uid: 1003 - components: - - type: Transform - pos: -10.5,-10.5 - parent: 104 - - uid: 1004 - components: - - type: Transform - pos: -10.5,-11.5 - parent: 104 - - uid: 1005 - components: - - type: Transform - pos: -10.5,-12.5 - parent: 104 - - uid: 1006 - components: - - type: Transform - pos: -9.5,4.5 - parent: 104 - - uid: 1007 - components: - - type: Transform - pos: -9.5,3.5 - parent: 104 - - uid: 1008 - components: - - type: Transform - pos: -9.5,2.5 - parent: 104 - - uid: 1009 - components: - - type: Transform - pos: -9.5,1.5 - parent: 104 - - uid: 1010 - components: - - type: Transform - pos: -9.5,0.5 - parent: 104 - - uid: 1011 - components: - - type: Transform - pos: -9.5,-0.5 - parent: 104 - - uid: 1012 - components: - - type: Transform - pos: -9.5,-1.5 - parent: 104 - - uid: 1013 - components: - - type: Transform - pos: -9.5,-2.5 - parent: 104 - - uid: 1014 - components: - - type: Transform - pos: -9.5,-3.5 - parent: 104 - - uid: 1015 - components: - - type: Transform - pos: -9.5,-4.5 - parent: 104 - - uid: 1016 - components: - - type: Transform - pos: -9.5,-5.5 - parent: 104 - - uid: 1017 - components: - - type: Transform - pos: -9.5,-6.5 - parent: 104 - - uid: 1018 - components: - - type: Transform - pos: -9.5,-7.5 - parent: 104 - - uid: 1019 - components: - - type: Transform - pos: -9.5,-8.5 - parent: 104 - - uid: 1086 - components: - - type: Transform - pos: -14.5,3.5 - parent: 104 - - uid: 1087 - components: - - type: Transform - pos: -19.5,4.5 - parent: 104 - - uid: 1202 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -24.5,4.5 - parent: 104 - - uid: 1203 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -27.5,-23.5 - parent: 104 - - uid: 1222 - components: - - type: Transform - pos: -34.5,2.5 - parent: 104 - - uid: 1223 - components: - - type: Transform - pos: -34.5,3.5 - parent: 104 - - uid: 1280 - components: - - type: Transform - pos: -23.5,-20.5 - parent: 104 - - uid: 1284 - components: - - type: Transform - pos: -25.5,-19.5 - parent: 104 - - uid: 1289 - components: - - type: Transform - pos: -28.5,-20.5 - parent: 104 - - uid: 1290 - components: - - type: Transform - pos: -22.5,0.5 - parent: 104 - - uid: 1362 - components: - - type: Transform - pos: -4.5,-2.5 - parent: 104 - - uid: 1363 - components: - - type: Transform - pos: -2.5,-2.5 - parent: 104 - - uid: 1364 - components: - - type: Transform - pos: -3.5,-2.5 - parent: 104 - - uid: 1365 - components: - - type: Transform - pos: -4.5,-4.5 - parent: 104 - - uid: 1366 - components: - - type: Transform - pos: -3.5,-4.5 - parent: 104 - - uid: 1377 - components: - - type: Transform - pos: -2.5,-4.5 - parent: 104 - - uid: 1428 - components: - - type: Transform - pos: -16.5,-19.5 - parent: 104 - - uid: 1535 - components: - - type: Transform - pos: -13.5,-20.5 - parent: 104 - - uid: 1554 - components: - - type: Transform - pos: -29.5,-19.5 - parent: 104 - - uid: 1674 - components: - - type: Transform - pos: -28.5,-19.5 - parent: 104 - - uid: 1785 - components: - - type: Transform - pos: -11.5,-19.5 - parent: 104 - - uid: 1786 - components: - - type: Transform - pos: -22.5,-19.5 - parent: 104 - - uid: 1787 - components: - - type: Transform - pos: -11.5,-20.5 - parent: 104 - - uid: 1788 - components: - - type: Transform - pos: -27.5,-19.5 - parent: 104 - - uid: 1789 - components: - - type: Transform - pos: -24.5,-20.5 - parent: 104 - - uid: 1791 - components: - - type: Transform - pos: -25.5,-20.5 - parent: 104 - - uid: 1799 - components: - - type: Transform - pos: -29.5,-20.5 - parent: 104 - - uid: 1825 - components: - - type: Transform - pos: -12.5,-20.5 - parent: 104 - - uid: 1886 - components: - - type: Transform - pos: -15.5,-20.5 - parent: 104 - - uid: 1887 - components: - - type: Transform - pos: -14.5,-19.5 - parent: 104 - - uid: 1947 - components: - - type: Transform - pos: -15.5,-19.5 - parent: 104 - - uid: 1949 - components: - - type: Transform - pos: -23.5,-19.5 - parent: 104 - - uid: 2046 - components: - - type: Transform - pos: -16.5,-20.5 - parent: 104 - - uid: 2048 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -23.5,2.5 - parent: 104 - - uid: 2050 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -24.5,2.5 - parent: 104 - - uid: 2052 - components: - - type: Transform - pos: -23.5,1.5 - parent: 104 - - uid: 2188 - components: - - type: Transform - pos: -21.5,1.5 - parent: 104 - - uid: 2405 - components: - - type: Transform - pos: 23.5,-19.5 - parent: 104 - - uid: 2406 - components: - - type: Transform - pos: 22.5,-19.5 - parent: 104 - - uid: 2407 - components: - - type: Transform - pos: 21.5,-19.5 - parent: 104 - - uid: 2408 - components: - - type: Transform - pos: 21.5,-17.5 - parent: 104 - - uid: 2409 - components: - - type: Transform - pos: 22.5,-17.5 - parent: 104 - - uid: 2410 - components: - - type: Transform - pos: 23.5,-17.5 - parent: 104 - - uid: 2505 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -22.5,3.5 - parent: 104 - - uid: 2507 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -20.5,4.5 - parent: 104 - - uid: 2508 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -21.5,2.5 - parent: 104 - - uid: 2514 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -20.5,3.5 - parent: 104 - - uid: 2515 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -21.5,4.5 - parent: 104 - - uid: 2521 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -20.5,2.5 - parent: 104 - - uid: 2526 - components: - - type: Transform - pos: -23.5,0.5 - parent: 104 - - uid: 2553 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -22.5,4.5 - parent: 104 - - uid: 2554 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -22.5,2.5 - parent: 104 - - uid: 2557 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -21.5,3.5 - parent: 104 -- proto: Chair - entities: - - uid: 487 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-20.5 - parent: 104 - - uid: 1367 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -3.5,-4.5 - parent: 104 - - uid: 1376 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -4.5,-4.5 - parent: 104 - - uid: 1878 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-21.5 - parent: 104 - - uid: 1879 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-22.5 - parent: 104 - - uid: 2312 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-13.5 - parent: 104 - - uid: 2313 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 7.5,-14.5 - parent: 104 -- proto: ChairFolding - entities: - - uid: 544 - components: - - type: Transform - pos: 20.5,8.5 - parent: 104 - - uid: 1062 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 18.5,4.5 - parent: 104 - - uid: 1063 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 18.5,2.5 - parent: 104 -- proto: ChairOfficeDark - entities: - - uid: 1298 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 13.5,-17.5 - parent: 104 - - uid: 1304 - components: - - type: Transform - pos: 11.5,-17.5 - parent: 104 - - uid: 1764 - components: - - type: Transform - pos: -13.5,6.5 - parent: 104 - - uid: 2427 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 19.5,-15.5 - parent: 104 - - uid: 2428 - components: - - type: Transform - pos: 25.5,-15.5 - parent: 104 -- proto: ChairOfficeLight - entities: - - uid: 751 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -0.5,-16.5 - parent: 104 - - uid: 1124 - components: - - type: Transform - pos: -0.5,-12.5 - parent: 104 - - uid: 1296 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 4.5,-11.5 - parent: 104 - - uid: 1297 - components: - - type: Transform - pos: 4.5,-16.5 - parent: 104 -- proto: ChairPilotSeat - entities: - - uid: 326 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 20.5,-6.5 - parent: 104 - - uid: 327 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 19.5,-7.5 - parent: 104 - - uid: 328 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 21.5,-7.5 - parent: 104 - - uid: 329 - components: - - type: Transform - pos: 20.5,-8.5 - parent: 104 - - uid: 330 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 18.5,-6.5 - parent: 104 - - uid: 331 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 19.5,-5.5 - parent: 104 - - uid: 332 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 21.5,-5.5 - parent: 104 - - uid: 333 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 22.5,-6.5 - parent: 104 - - uid: 334 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 22.5,-8.5 - parent: 104 - - uid: 335 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 21.5,-9.5 - parent: 104 - - uid: 336 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 19.5,-9.5 - parent: 104 - - uid: 337 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 18.5,-8.5 - parent: 104 -- proto: ChairWood - entities: - - uid: 2429 - components: - - type: Transform - pos: -0.5,-1.5 - parent: 104 -- proto: chem_master - entities: - - uid: 2444 - components: - - type: Transform - pos: 5.5,-10.5 - parent: 104 -- proto: ChemicalPayload - entities: - - uid: 2380 - components: - - type: Transform - pos: 3.3003616,-17.381208 - parent: 104 - - uid: 3110 - components: - - type: Transform - pos: 3.6284866,-17.287361 - parent: 104 -- proto: ChemistryHotplate - entities: - - uid: 1383 - components: - - type: Transform - pos: 4.5,-12.5 - parent: 104 -- proto: ChessBoard - entities: - - uid: 354 - components: - - type: Transform - pos: 13.499366,-8.433407 - parent: 104 -- proto: ClosetEmergencyFilledRandom - entities: - - uid: 1359 - components: - - type: Transform - pos: -2.5,-2.5 - parent: 104 -- proto: ClosetL3Filled - entities: - - uid: 1153 - components: - - type: Transform - pos: 7.5,-17.5 - parent: 104 -- proto: ClosetRadiationSuitFilled - entities: - - uid: 1114 - components: - - type: Transform - pos: 6.5,-17.5 - parent: 104 -- proto: ClosetToolFilled - entities: - - uid: 2417 - components: - - type: Transform - pos: 20.5,-14.5 - parent: 104 - - uid: 2418 - components: - - type: Transform - pos: 24.5,-14.5 - parent: 104 -- proto: ClothingBackpackDuffelSyndicateFilledMedical - entities: - - uid: 2437 - components: - - type: Transform - pos: 0.5,-12.5 - parent: 104 -- proto: ClothingBackpackWaterTank - entities: - - uid: 151 - components: - - type: Transform - pos: 9.36168,-4.462339 - parent: 104 - - type: SolutionAmmoProvider - maxShots: 200 -- proto: ClothingBeltUtilityFilled - entities: - - uid: 212 - components: - - type: Transform - pos: 13.897393,-10.470539 - parent: 104 -- proto: ClothingEyesGlassesChemical - entities: - - uid: 1738 - components: - - type: Transform - pos: 3.5009227,-11.519134 - parent: 104 -- proto: ClothingNeckScarfStripedRed - entities: - - uid: 1150 - components: - - type: Transform - pos: -2.530845,-16.461567 - parent: 104 -- proto: ClothingNeckStethoscope - entities: - - uid: 1336 - components: - - type: Transform - pos: 7.5027137,-12.301802 - parent: 104 -- proto: ClothingShoesBootsSalvage - entities: - - uid: 542 - components: - - type: Transform - pos: 19.574894,7.069533 - parent: 104 - - uid: 545 - components: - - type: Transform - pos: 19.246769,6.507033 - parent: 104 -- proto: CombatKnife - entities: - - uid: 1694 - components: - - type: Transform - pos: 5.483225,11.368477 - parent: 104 -- proto: ComfyChair - entities: - - uid: 21 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 12.5,-8.5 - parent: 104 - - uid: 352 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 14.5,-8.5 - parent: 104 -- proto: computerBodyScanner - entities: - - uid: 2436 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-14.5 - parent: 104 -- proto: ComputerPowerMonitoring - entities: - - uid: 1730 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 27.5,-14.5 - parent: 104 -- proto: ComputerSurveillanceWirelessCameraMonitor - entities: - - uid: 1127 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -14.5,6.5 - parent: 104 -- proto: CyberPen - entities: - - uid: 696 - components: - - type: Transform - pos: -14.478909,9.585995 - parent: 104 -- proto: DresserFilled - entities: - - uid: 349 - components: - - type: Transform - pos: 13.5,-3.5 - parent: 104 -- proto: DrinkAbsintheBottleFull - entities: - - uid: 15 - components: - - type: Transform - pos: 3.9415665,-8.34479 - parent: 104 -- proto: DrinkBeerBottleFull - entities: - - uid: 157 - components: - - type: Transform - pos: 2.5961013,-2.2186527 - parent: 104 - - uid: 170 - components: - - type: Transform - pos: 3.6117263,-3.1874027 - parent: 104 -- proto: DrinkGinBottleFull - entities: - - uid: 134 - components: - - type: Transform - pos: 3.2228165,-8.4229965 - parent: 104 -- proto: DrinkWhiskeyBottleFull - entities: - - uid: 25 - components: - - type: Transform - pos: -0.5303154,-6.2851996 - parent: 104 -- proto: EmpGrenade - entities: - - uid: 2456 - components: - - type: Transform - pos: 14.40774,-17.385033 - parent: 104 - - uid: 2457 - components: - - type: Transform - pos: 14.62649,-17.385033 - parent: 104 -- proto: ExtinguisherCabinetFilled - entities: - - uid: 139 - components: - - type: Transform - pos: 28.5,-14.5 - parent: 104 - - uid: 141 - components: - - type: Transform - pos: 14.5,-0.5 - parent: 104 - - uid: 1812 - components: - - type: Transform - pos: -1.5,-2.5 - parent: 104 - - uid: 1816 - components: - - type: Transform - pos: 8.5,-10.5 - parent: 104 - - uid: 1822 - components: - - type: Transform - pos: 15.5,-17.5 - parent: 104 -- proto: FaxMachineSyndie - entities: - - uid: 81 - components: - - type: Transform - pos: 7.5,-4.5 - parent: 104 -- proto: filingCabinetDrawer - entities: - - uid: 1140 - components: - - type: Transform - pos: -12.5,7.5 - parent: 104 -- proto: filingCabinetDrawerRandom - entities: - - uid: 108 - components: - - type: Transform - pos: 7.5,-5.5 - parent: 104 -- proto: FirelockGlass - entities: - - uid: 1765 - components: - - type: Transform - pos: -13.5,5.5 - parent: 104 -- proto: Fireplace - entities: - - uid: 1273 - components: - - type: Transform - pos: 1.5,0.5 - parent: 104 -- proto: FloorDrain - entities: - - uid: 1361 - components: - - type: Transform - pos: -4.5,-2.5 - parent: 104 - - type: Fixtures - fixtures: {} - - uid: 2438 - components: - - type: Transform - pos: 10.5,-3.5 - parent: 104 - - type: Fixtures - fixtures: {} - - uid: 2443 - components: - - type: Transform - pos: 4.5,-11.5 - parent: 104 - - type: Fixtures - fixtures: {} -- proto: FloraRockSolid01 - entities: - - uid: 1707 - components: - - type: Transform - pos: 5.5565968,16.377468 - parent: 104 -- proto: FloraRockSolid02 - entities: - - uid: 1708 - components: - - type: Transform - pos: -3.1481876,-0.15203857 - parent: 104 - - uid: 1711 - components: - - type: Transform - pos: -1.3676796,16.13784 - parent: 104 -- proto: FloraRockSolid03 - entities: - - uid: 1706 - components: - - type: Transform - pos: 13.550529,12.489372 - parent: 104 -- proto: FloraTreeConifer01 - entities: - - uid: 261 - components: - - type: Transform - pos: 7.7892694,4.987089 - parent: 104 - - uid: 275 - components: - - type: Transform - pos: -3.80645,4.798395 - parent: 104 - - uid: 281 - components: - - type: Transform - pos: -1.0279694,6.890992 - parent: 104 - - uid: 1619 - components: - - type: Transform - pos: 16.10984,7.3249645 - parent: 104 - - uid: 1622 - components: - - type: Transform - pos: 7.42463,16.597832 - parent: 104 - - uid: 1626 - components: - - type: Transform - pos: 11.605486,16.944057 - parent: 104 -- proto: FloraTreeConifer02 - entities: - - uid: 219 - components: - - type: Transform - pos: 5.5,4.5 - parent: 104 - - uid: 260 - components: - - type: Transform - pos: 7.7267694,7.536619 - parent: 104 - - uid: 267 - components: - - type: Transform - pos: 12.789269,3.6888618 - parent: 104 - - uid: 268 - components: - - type: Transform - pos: -0.17814255,7.5043383 - parent: 104 - - uid: 273 - components: - - type: Transform - pos: -2.591959,2.9214401 - parent: 104 - - uid: 280 - components: - - type: Transform - pos: 1.5111885,4.4898434 - parent: 104 - - uid: 1618 - components: - - type: Transform - pos: 11.32859,10.093473 - parent: 104 - - uid: 1620 - components: - - type: Transform - pos: 15.85984,13.16621 - parent: 104 - - uid: 1621 - components: - - type: Transform - pos: 14.582346,17.286049 - parent: 104 -- proto: FloraTreeConifer03 - entities: - - uid: 271 - components: - - type: Transform - pos: -1.1000175,4.26659 - parent: 104 - - uid: 1035 - components: - - type: Transform - pos: 0.74747276,19.424063 - parent: 104 - - uid: 1673 - components: - - type: Transform - pos: -3.5,15.5 - parent: 104 - - uid: 1830 - components: - - type: Transform - pos: 8.497473,19.862019 - parent: 104 -- proto: FloraTreeSnow01 - entities: - - uid: 262 - components: - - type: Transform - pos: 9.630766,6.7434845 - parent: 104 -- proto: FloraTreeSnow02 - entities: - - uid: 270 - components: - - type: Transform - pos: -2.3114033,5.0917635 - parent: 104 - - uid: 1630 - components: - - type: Transform - pos: 4.558771,16.696045 - parent: 104 -- proto: FloraTreeSnow03 - entities: - - uid: 274 - components: - - type: Transform - pos: -3.3325882,6.8936405 - parent: 104 - - uid: 1629 - components: - - type: Transform - pos: 9.924105,15.382175 - parent: 104 -- proto: FloraTreeSnow04 - entities: - - uid: 265 - components: - - type: Transform - pos: 0.24603653,5.7335367 - parent: 104 - - uid: 276 - components: - - type: Transform - pos: 5.8604717,5.2799397 - parent: 104 - - uid: 1624 - components: - - type: Transform - pos: -3.1344147,13.722986 - parent: 104 -- proto: FloraTreeSnow05 - entities: - - uid: 269 - components: - - type: Transform - pos: 0.30623245,3.9068413 - parent: 104 - - uid: 1627 - components: - - type: Transform - pos: 13.331816,13.977352 - parent: 104 - - uid: 1628 - components: - - type: Transform - pos: 15.066191,11.052429 - parent: 104 - - uid: 1631 - components: - - type: Transform - pos: 2.1525211,16.977589 - parent: 104 - - uid: 1672 - components: - - type: Transform - pos: -3.5,16.5 - parent: 104 -- proto: FoodBoxDonkpocket - entities: - - uid: 11 - components: - - type: Transform - pos: 12.527094,-6.308407 - parent: 104 -- proto: FoodBoxDonkpocketTeriyaki - entities: - - uid: 2475 - components: - - type: Transform - pos: 12.636917,-6.187726 - parent: 104 -- proto: FoodBoxDonut - entities: - - uid: 2445 - components: - - type: Transform - pos: 2.5389562,-6.3072195 - parent: 104 -- proto: FoodDonutJellySlugcat - entities: - - uid: 3327 - components: - - type: Transform - pos: -5.7255287,20.539352 - parent: 104 -- proto: GeneratorBasic15kW - entities: - - uid: 2326 - components: - - type: Transform - pos: 27.5,-19.5 - parent: 104 - - uid: 2328 - components: - - type: Transform - pos: 17.5,-17.5 - parent: 104 - - uid: 2329 - components: - - type: Transform - pos: 17.5,-19.5 - parent: 104 - - uid: 2330 - components: - - type: Transform - pos: 27.5,-17.5 - parent: 104 -- proto: GeneratorRTG - entities: - - uid: 2415 - components: - - type: Transform - pos: 17.5,-18.5 - parent: 104 - - uid: 2416 - components: - - type: Transform - pos: 27.5,-18.5 - parent: 104 -- proto: GravityGenerator - entities: - - uid: 2327 - components: - - type: Transform - pos: 22.5,-15.5 - parent: 104 - - type: GravityGenerator - charge: 100 - - type: PointLight - radius: 175.75 -- proto: GrenadeFlashBang - entities: - - uid: 2309 - components: - - type: Transform - pos: 11.399388,-16.363453 - parent: 104 - - uid: 2310 - components: - - type: Transform - pos: 11.571263,-16.363453 - parent: 104 -- proto: Grille - entities: - - uid: 95 - components: - - type: Transform - pos: 0.5,1.5 - parent: 104 - - uid: 115 - components: - - type: Transform - pos: -1.5,-0.5 - parent: 104 - - uid: 118 - components: - - type: Transform - pos: 2.5,1.5 - parent: 104 - - uid: 147 - components: - - type: Transform - pos: 11.5,1.5 - parent: 104 - - uid: 231 - components: - - type: Transform - pos: 2.5,2.5 - parent: 104 - - uid: 232 - components: - - type: Transform - pos: 2.5,3.5 - parent: 104 - - uid: 233 - components: - - type: Transform - pos: 4.5,2.5 - parent: 104 - - uid: 234 - components: - - type: Transform - pos: 4.5,3.5 - parent: 104 - - uid: 303 - components: - - type: Transform - pos: 16.5,-6.5 - parent: 104 - - uid: 304 - components: - - type: Transform - pos: 16.5,-8.5 - parent: 104 - - uid: 526 - components: - - type: Transform - pos: 0.5,21.5 - parent: 104 - - uid: 527 - components: - - type: Transform - pos: 6.5,21.5 - parent: 104 - - uid: 528 - components: - - type: Transform - pos: 1.5,21.5 - parent: 104 - - uid: 1034 - components: - - type: Transform - pos: 5.5,21.5 - parent: 104 - - uid: 1112 - components: - - type: Transform - pos: -1.5,-17.5 - parent: 104 - - uid: 1120 - components: - - type: Transform - pos: 2.5,-15.5 - parent: 104 - - uid: 1121 - components: - - type: Transform - pos: 2.5,-13.5 - parent: 104 - - uid: 1146 - components: - - type: Transform - pos: -12.5,5.5 - parent: 104 - - uid: 1148 - components: - - type: Transform - pos: -14.5,5.5 - parent: 104 - - uid: 1241 - components: - - type: Transform - pos: 5.5,-9.5 - parent: 104 - - uid: 1243 - components: - - type: Transform - pos: 7.5,-9.5 - parent: 104 - - uid: 1389 - components: - - type: Transform - pos: 4.5,1.5 - parent: 104 - - uid: 1410 - components: - - type: Transform - pos: 8.5,-17.5 - parent: 104 - - uid: 1823 - components: - - type: Transform - pos: 8.5,20.5 - parent: 104 - - uid: 1824 - components: - - type: Transform - pos: 4.5,21.5 - parent: 104 - - uid: 1831 - components: - - type: Transform - pos: 8.5,21.5 - parent: 104 - - uid: 1832 - components: - - type: Transform - pos: 3.5,21.5 - parent: 104 - - uid: 1840 - components: - - type: Transform - pos: 11.5,-23.5 - parent: 104 - - uid: 1841 - components: - - type: Transform - pos: 10.5,-23.5 - parent: 104 - - uid: 1847 - components: - - type: Transform - pos: 12.5,-23.5 - parent: 104 - - uid: 1854 - components: - - type: Transform - pos: 13.5,-23.5 - parent: 104 - - uid: 1860 - components: - - type: Transform - pos: 14.5,-23.5 - parent: 104 - - uid: 1931 - components: - - type: Transform - pos: 0.5,20.5 - parent: 104 - - uid: 1932 - components: - - type: Transform - pos: 7.5,21.5 - parent: 104 - - uid: 1933 - components: - - type: Transform - pos: 2.5,21.5 - parent: 104 - - uid: 2189 - components: - - type: Transform - pos: -0.5,-17.5 - parent: 104 - - uid: 2296 - components: - - type: Transform - pos: 0.5,-17.5 - parent: 104 - - uid: 2318 - components: - - type: Transform - pos: 11.5,-8.5 - parent: 104 - - uid: 2319 - components: - - type: Transform - pos: 11.5,-6.5 - parent: 104 -- proto: KitchenKnife - entities: - - uid: 1061 - components: - - type: Transform - pos: 18.918644,6.663283 - parent: 104 -- proto: KitchenMicrowave - entities: - - uid: 10 - components: - - type: Transform - pos: 13.5,-6.5 - parent: 104 -- proto: KitchenReagentGrinder - entities: - - uid: 1257 - components: - - type: Transform - pos: 3.5,-10.5 - parent: 104 -- proto: LargeBeaker - entities: - - uid: 2021 - components: - - type: Transform - pos: 5.039936,-12.489611 - parent: 104 -- proto: LightPostSmall - entities: - - uid: 221 - components: - - type: Transform - pos: 15.5,16.5 - parent: 104 - - uid: 222 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -2.5,3.5 - parent: 104 - - uid: 291 - components: - - type: Transform - pos: 17.5,3.5 - parent: 104 - - uid: 540 - components: - - type: Transform - pos: 27.5,4.5 - parent: 104 - - uid: 541 - components: - - type: Transform - pos: 18.5,8.5 - parent: 104 - - uid: 629 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,8.5 - parent: 104 - - uid: 1226 - components: - - type: Transform - pos: -4.5,-0.5 - parent: 104 - - uid: 1276 - components: - - type: Transform - pos: -34.5,4.5 - parent: 104 - - uid: 1279 - components: - - type: Transform - pos: -35.5,-23.5 - parent: 104 - - uid: 1413 - components: - - type: Transform - pos: 0.5,-22.5 - parent: 104 - - uid: 1668 - components: - - type: Transform - pos: 5.5,17.5 - parent: 104 - - uid: 1669 - components: - - type: Transform - pos: 10.5,11.5 - parent: 104 - - uid: 1671 - components: - - type: Transform - pos: -3.5,17.5 - parent: 104 - - uid: 1675 - components: - - type: Transform - pos: 10.5,2.5 - parent: 104 -- proto: LockerSyndicatePersonalFilled - entities: - - uid: 178 - components: - - type: Transform - pos: 9.5,-13.5 - parent: 104 - - uid: 196 - components: - - type: Transform - pos: 11.5,-14.5 - parent: 104 - - uid: 197 - components: - - type: Transform - pos: 11.5,-13.5 - parent: 104 - - uid: 198 - components: - - type: Transform - pos: 9.5,-14.5 - parent: 104 - - uid: 1271 - components: - - type: Transform - pos: 13.5,-13.5 - parent: 104 - - uid: 1375 - components: - - type: Transform - pos: 13.5,-14.5 - parent: 104 - - uid: 2476 - components: - - type: Transform - pos: 15.5,-6.5 - parent: 104 -- proto: MachineCentrifuge - entities: - - uid: 1728 - components: - - type: Transform - pos: 3.5,-11.5 - parent: 104 -- proto: MachineElectrolysisUnit - entities: - - uid: 1718 - components: - - type: Transform - pos: 3.5,-12.5 - parent: 104 -- proto: MagazinePistolSubMachineGunPractice - entities: - - uid: 1648 - components: - - type: Transform - pos: 5.7430096,13.989216 - parent: 104 - - type: BallisticAmmoProvider - unspawnedCount: 35 -- proto: MedicalBed - entities: - - uid: 1868 - components: - - type: Transform - pos: 1.5,-16.5 - parent: 104 - - uid: 1874 - components: - - type: Transform - pos: -1.5,-12.5 - parent: 104 - - uid: 1890 - components: - - type: Transform - pos: 1.5,-12.5 - parent: 104 - - uid: 1891 - components: - - type: Transform - pos: -1.5,-16.5 - parent: 104 -- proto: MedkitAdvancedFilled - entities: - - uid: 447 - components: - - type: Transform - pos: 7.5183387,-10.279964 - parent: 104 -- proto: MedkitBruteFilled - entities: - - uid: 138 - components: - - type: Transform - pos: 7.5183387,-10.576839 - parent: 104 -- proto: MedkitBurnFilled - entities: - - uid: 1894 - components: - - type: Transform - pos: 7.5339637,-10.889339 - parent: 104 -- proto: MedkitFilled - entities: - - uid: 770 - components: - - type: Transform - pos: 0.48478004,-16.399067 - parent: 104 -- proto: MedkitOxygenFilled - entities: - - uid: 1895 - components: - - type: Transform - pos: 7.5339637,-11.186214 - parent: 104 -- proto: MedkitRadiationFilled - entities: - - uid: 448 - components: - - type: Transform - pos: 7.5495887,-11.733089 - parent: 104 -- proto: MedkitToxinFilled - entities: - - uid: 1388 - components: - - type: Transform - pos: 7.5339637,-11.436214 - parent: 104 -- proto: Mirror - entities: - - uid: 142 - components: - - type: Transform - pos: 9.5,1.5 - parent: 104 - - uid: 146 - components: - - type: Transform - pos: 10.5,1.5 - parent: 104 -- proto: ModularGrenade - entities: - - uid: 1326 - components: - - type: Transform - pos: 4.001747,-17.289776 - parent: 104 - - uid: 1328 - components: - - type: Transform - pos: 4.204872,-17.508755 - parent: 104 -- proto: MopBucket - entities: - - uid: 149 - components: - - type: Transform - pos: 11.5,-3.5 - parent: 104 -- proto: MopItem - entities: - - uid: 150 - components: - - type: Transform - pos: 11.5,-3.5 - parent: 104 -- proto: Multitool - entities: - - uid: 1555 - components: - - type: Transform - pos: 11.895346,-10.413807 - parent: 104 -- proto: NukeDiskFake - entities: - - uid: 282 - components: - - type: Transform - pos: 12.149857,15.427643 - parent: 104 -- proto: OperatingTable - entities: - - uid: 2435 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 104 -- proto: Paper - entities: - - uid: 612 - components: - - type: Transform - pos: -14.166584,9.634687 - parent: 104 -- proto: PartRodMetal - entities: - - uid: 2425 - components: - - type: Transform - pos: 24.5,-16.5 - parent: 104 - - uid: 2426 - components: - - type: Transform - pos: 24.5,-16.5 - parent: 104 -- proto: PhoneInstrumentSyndicate - entities: - - uid: 1684 - components: - - type: Transform - pos: 1.4424791,-6.340748 - parent: 104 -- proto: PianoInstrument - entities: - - uid: 2430 - components: - - type: Transform - pos: -0.5,-2.5 - parent: 104 -- proto: PlasmaReinforcedWindowDirectional - entities: - - uid: 1817 - components: - - type: Transform - pos: 13.5,-18.5 - parent: 104 - - uid: 1818 - components: - - type: Transform - pos: 12.5,-18.5 - parent: 104 - - uid: 1819 - components: - - type: Transform - pos: 11.5,-18.5 - parent: 104 - - uid: 1858 - components: - - type: Transform - pos: 14.5,-18.5 - parent: 104 -- proto: PlushieBee - entities: - - uid: 2317 - components: - - type: Transform - pos: 1.5,-12.5 - parent: 104 -- proto: PlushieLizard - entities: - - uid: 1266 - components: - - type: Transform - pos: -15.435635,22.055351 - parent: 104 - - uid: 2315 - components: - - type: Transform - pos: -1.5,-16.5 - parent: 104 - - uid: 3367 - components: - - type: Transform - pos: -4.605839,20.481245 - parent: 104 -- proto: PlushieNuke - entities: - - uid: 163 - components: - - type: Transform - pos: 1.8113766,-6.337348 - parent: 104 - - uid: 283 - components: - - type: Transform - pos: 12.912951,15.575253 - parent: 104 -- proto: PlushieRouny - entities: - - uid: 2316 - components: - - type: Transform - pos: -1.5,-12.5 - parent: 104 -- proto: PlushieVox - entities: - - uid: 2314 - components: - - type: Transform - pos: 1.5,-16.5 - parent: 104 -- proto: PortableScrubber - entities: - - uid: 811 - components: - - type: Transform - pos: 10.5,-10.5 - parent: 104 -- proto: PosterBroken - entities: - - uid: 227 - components: - - type: Transform - pos: -0.5,10.5 - parent: 104 -- proto: PosterContrabandC20r - entities: - - uid: 1556 - components: - - type: Transform - pos: 10.5,-9.5 - parent: 104 -- proto: PosterContrabandClown - entities: - - uid: 1681 - components: - - type: Transform - pos: 12.5,1.5 - parent: 104 -- proto: PosterContrabandCybersun600 - entities: - - uid: 2466 - components: - - type: Transform - pos: 8.5,-9.5 - parent: 104 -- proto: PosterContrabandDonk - entities: - - uid: 12 - components: - - type: Transform - pos: 12.5,-5.5 - parent: 104 - - uid: 2465 - components: - - type: Transform - pos: -10.5,8.5 - parent: 104 -- proto: PosterContrabandDonutCorp - entities: - - uid: 2450 - components: - - type: Transform - pos: -3.5,-1.5 - parent: 104 -- proto: PosterContrabandEnergySwords - entities: - - uid: 1384 - components: - - type: Transform - pos: 14.5,-9.5 - parent: 104 -- proto: PosterContrabandEnlistGorlex - entities: - - uid: 2461 - components: - - type: Transform - pos: -0.5,0.5 - parent: 104 -- proto: PosterContrabandInterdyne - entities: - - uid: 2464 - components: - - type: Transform - pos: 8.5,-12.5 - parent: 104 -- proto: PosterContrabandLustyExomorph - entities: - - uid: 754 - components: - - type: Transform - pos: -3.5,-13.5 - parent: 104 - - uid: 2447 - components: - - type: Transform - pos: 15.5,-15.5 - parent: 104 -- proto: PosterContrabandMoth - entities: - - uid: 2462 - components: - - type: Transform - pos: 8.5,-2.5 - parent: 104 -- proto: PosterContrabandNuclearDeviceInformational - entities: - - uid: 2298 - components: - - type: Transform - pos: -1.5,-5.5 - parent: 104 -- proto: PosterContrabandPower - entities: - - uid: 1563 - components: - - type: Transform - pos: 19.5,-12.5 - parent: 104 -- proto: PosterContrabandPunchShit - entities: - - uid: 2451 - components: - - type: Transform - pos: 25.5,-12.5 - parent: 104 -- proto: PosterContrabandRebelsUnite - entities: - - uid: 2446 - components: - - type: Transform - pos: 8.5,-3.5 - parent: 104 -- proto: PosterContrabandRedRum - entities: - - uid: 1387 - components: - - type: Transform - pos: 2.5,-9.5 - parent: 104 -- proto: PosterContrabandRevolver - entities: - - uid: 2452 - components: - - type: Transform - pos: -1.5,-7.5 - parent: 104 -- proto: PosterContrabandRobustSoftdrinks - entities: - - uid: 1245 - components: - - type: Transform - pos: -9.5,8.5 - parent: 104 -- proto: PosterContrabandSyndicatePistol - entities: - - uid: 1386 - components: - - type: Transform - pos: 12.5,-9.5 - parent: 104 -- proto: PosterContrabandSyndicateRecruitment - entities: - - uid: 1242 - components: - - type: Transform - pos: -15.5,7.5 - parent: 104 - - uid: 1561 - components: - - type: Transform - pos: 16.5,-11.5 - parent: 104 - - uid: 2453 - components: - - type: Transform - pos: -3.5,-5.5 - parent: 104 - - uid: 2454 - components: - - type: Transform - pos: 12.5,-15.5 - parent: 104 -- proto: PosterContrabandTheBigGasTruth - entities: - - uid: 2455 - components: - - type: Transform - pos: 10.5,-15.5 - parent: 104 -- proto: PosterContrabandVoteWeh - entities: - - uid: 1113 - components: - - type: Transform - pos: -3.5,-15.5 - parent: 104 - - uid: 1227 - components: - - type: Transform - pos: -17.5,24.5 - parent: 104 - - uid: 1228 - components: - - type: Transform - pos: -16.5,24.5 - parent: 104 - - uid: 1229 - components: - - type: Transform - pos: -15.5,24.5 - parent: 104 - - uid: 1230 - components: - - type: Transform - pos: -14.5,24.5 - parent: 104 - - uid: 1231 - components: - - type: Transform - pos: -13.5,24.5 - parent: 104 - - uid: 1248 - components: - - type: Transform - pos: -12.5,24.5 - parent: 104 - - uid: 1251 - components: - - type: Transform - pos: -12.5,23.5 - parent: 104 - - uid: 1252 - components: - - type: Transform - pos: -12.5,22.5 - parent: 104 - - uid: 1253 - components: - - type: Transform - pos: -12.5,21.5 - parent: 104 - - uid: 1254 - components: - - type: Transform - pos: -12.5,20.5 - parent: 104 - - uid: 1255 - components: - - type: Transform - pos: -12.5,19.5 - parent: 104 - - uid: 1256 - components: - - type: Transform - pos: -13.5,19.5 - parent: 104 - - uid: 1258 - components: - - type: Transform - pos: -14.5,19.5 - parent: 104 - - uid: 1259 - components: - - type: Transform - pos: -15.5,19.5 - parent: 104 - - uid: 1260 - components: - - type: Transform - pos: -16.5,19.5 - parent: 104 - - uid: 1261 - components: - - type: Transform - pos: -17.5,19.5 - parent: 104 - - uid: 1262 - components: - - type: Transform - pos: -17.5,20.5 - parent: 104 - - uid: 1263 - components: - - type: Transform - pos: -17.5,21.5 - parent: 104 - - uid: 1264 - components: - - type: Transform - pos: -17.5,22.5 - parent: 104 - - uid: 1265 - components: - - type: Transform - pos: -17.5,23.5 - parent: 104 - - uid: 1565 - components: - - type: Transform - pos: 12.5,-3.5 - parent: 104 -- proto: PosterContrabandWaffleCorp - entities: - - uid: 2463 - components: - - type: Transform - pos: 14.5,-2.5 - parent: 104 -- proto: PosterLegitAnatomyPoster - entities: - - uid: 2320 - components: - - type: Transform - pos: 2.5,-11.5 - parent: 104 -- proto: PosterLegitCleanliness - entities: - - uid: 955 - components: - - type: Transform - pos: 0.5,-11.5 - parent: 104 -- proto: PosterLegitIan - entities: - - uid: 1551 - components: - - type: Transform - pos: -0.5,12.5 - parent: 104 -- proto: PosterLegitIonRifle - entities: - - uid: 1558 - components: - - type: Transform - pos: 16.5,-14.5 - parent: 104 -- proto: PosterLegitNanotrasenLogo - entities: - - uid: 237 - components: - - type: Transform - pos: -0.5,14.5 - parent: 104 -- proto: PosterMapSplit - entities: - - uid: 350 - components: - - type: Transform - pos: 0.5,-9.5 - parent: 104 -- proto: PosterMapWaystation - entities: - - uid: 2449 - components: - - type: Transform - pos: 10.5,-5.5 - parent: 104 -- proto: PottedPlant24 - entities: - - uid: 1143 - components: - - type: Transform - pos: -14.5,7.5 - parent: 104 -- proto: PottedPlantBioluminscent - entities: - - uid: 7 - components: - - type: Transform - pos: 15.481667,-8.824032 - parent: 104 -- proto: PottedPlantRandom - entities: - - uid: 1656 - components: - - type: Transform - pos: 9.5,-1.5 - parent: 104 -- proto: PottedPlantRandomPlastic - entities: - - uid: 1654 - components: - - type: Transform - pos: 8.5,-6.5 - parent: 104 - - uid: 1655 - components: - - type: Transform - pos: 5.5,-8.5 - parent: 104 -- proto: Poweredlight - entities: - - uid: 1333 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 13.5,-0.5 - parent: 104 -- proto: PoweredLightPostSmall - entities: - - uid: 2525 - components: - - type: Transform - pos: -10.5,0.5 - parent: 104 - - uid: 2527 - components: - - type: Transform - pos: -31.5,0.5 - parent: 104 - - uid: 2528 - components: - - type: Transform - pos: -27.5,0.5 - parent: 104 - - uid: 2529 - components: - - type: Transform - pos: -23.5,0.5 - parent: 104 - - uid: 2530 - components: - - type: Transform - pos: -19.5,0.5 - parent: 104 - - uid: 2531 - components: - - type: Transform - pos: -15.5,0.5 - parent: 104 - - uid: 2532 - components: - - type: Transform - pos: -10.5,-19.5 - parent: 104 - - uid: 2533 - components: - - type: Transform - pos: -15.5,-19.5 - parent: 104 - - uid: 2534 - components: - - type: Transform - pos: -19.5,-19.5 - parent: 104 - - uid: 2535 - components: - - type: Transform - pos: -23.5,-19.5 - parent: 104 - - uid: 2536 - components: - - type: Transform - pos: -27.5,-19.5 - parent: 104 - - uid: 2537 - components: - - type: Transform - pos: -31.5,-19.5 - parent: 104 -- proto: PoweredSmallLight - entities: - - uid: 23 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 15.5,-1.5 - parent: 104 -- proto: Rack - entities: - - uid: 472 - components: - - type: Transform - pos: 11.5,-16.5 - parent: 104 - - uid: 1360 - components: - - type: Transform - pos: -2.5,-4.5 - parent: 104 -- proto: Railing - entities: - - uid: 253 - components: - - type: Transform - pos: -31.5,0.5 - parent: 104 - - uid: 446 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-6.5 - parent: 104 - - uid: 456 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-2.5 - parent: 104 - - uid: 457 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-3.5 - parent: 104 - - uid: 458 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-4.5 - parent: 104 - - uid: 459 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -8.5,5.5 - parent: 104 - - uid: 461 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-5.5 - parent: 104 - - uid: 481 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -8.5,6.5 - parent: 104 - - uid: 523 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -8.5,7.5 - parent: 104 - - uid: 585 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-22.5 - parent: 104 - - uid: 942 - components: - - type: Transform - pos: -20.5,0.5 - parent: 104 - - uid: 956 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -34.5,1.5 - parent: 104 - - uid: 1080 - components: - - type: Transform - pos: -34.5,-23.5 - parent: 104 - - uid: 1111 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-0.5 - parent: 104 - - uid: 1159 - components: - - type: Transform - pos: -22.5,-23.5 - parent: 104 - - uid: 1301 - components: - - type: Transform - pos: -28.5,-23.5 - parent: 104 - - uid: 1302 - components: - - type: Transform - pos: -21.5,-23.5 - parent: 104 - - uid: 1303 - components: - - type: Transform - pos: -26.5,-23.5 - parent: 104 - - uid: 1305 - components: - - type: Transform - pos: -24.5,-23.5 - parent: 104 - - uid: 1306 - components: - - type: Transform - pos: -20.5,-23.5 - parent: 104 - - uid: 1307 - components: - - type: Transform - pos: -19.5,-23.5 - parent: 104 - - uid: 1308 - components: - - type: Transform - pos: -18.5,-23.5 - parent: 104 - - uid: 1309 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -26.5,4.5 - parent: 104 - - uid: 1310 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -24.5,4.5 - parent: 104 - - uid: 1311 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -23.5,4.5 - parent: 104 - - uid: 1312 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -22.5,4.5 - parent: 104 - - uid: 1313 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -25.5,4.5 - parent: 104 - - uid: 1314 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -30.5,4.5 - parent: 104 - - uid: 1315 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -32.5,4.5 - parent: 104 - - uid: 1316 - components: - - type: Transform - pos: -32.5,-23.5 - parent: 104 - - uid: 1317 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -29.5,4.5 - parent: 104 - - uid: 1319 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -31.5,4.5 - parent: 104 - - uid: 1323 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -34.5,3.5 - parent: 104 - - uid: 1352 - components: - - type: Transform - pos: -23.5,-23.5 - parent: 104 - - uid: 1353 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -33.5,4.5 - parent: 104 - - uid: 1354 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -28.5,4.5 - parent: 104 - - uid: 1355 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -27.5,4.5 - parent: 104 - - uid: 1357 - components: - - type: Transform - pos: -27.5,-23.5 - parent: 104 - - uid: 1373 - components: - - type: Transform - pos: -31.5,-23.5 - parent: 104 - - uid: 1374 - components: - - type: Transform - pos: -29.5,-23.5 - parent: 104 - - uid: 1378 - components: - - type: Transform - pos: -25.5,-23.5 - parent: 104 - - uid: 1379 - components: - - type: Transform - pos: -30.5,-23.5 - parent: 104 - - uid: 1414 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-1.5 - parent: 104 - - uid: 1432 - components: - - type: Transform - pos: -33.5,-23.5 - parent: 104 - - uid: 1800 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-21.5 - parent: 104 - - uid: 1838 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-18.5 - parent: 104 - - uid: 1839 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-17.5 - parent: 104 - - uid: 1845 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-16.5 - parent: 104 - - uid: 1846 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-14.5 - parent: 104 - - uid: 1851 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-13.5 - parent: 104 - - uid: 1852 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-15.5 - parent: 104 - - uid: 1857 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-7.5 - parent: 104 - - uid: 1882 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -19.5,4.5 - parent: 104 - - uid: 1902 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -20.5,4.5 - parent: 104 - - uid: 1903 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -21.5,4.5 - parent: 104 - - uid: 1916 - components: - - type: Transform - pos: -15.5,-23.5 - parent: 104 - - uid: 1917 - components: - - type: Transform - pos: -14.5,-23.5 - parent: 104 - - uid: 1918 - components: - - type: Transform - pos: -13.5,-23.5 - parent: 104 - - uid: 1919 - components: - - type: Transform - pos: -12.5,-23.5 - parent: 104 - - uid: 1920 - components: - - type: Transform - pos: -11.5,-23.5 - parent: 104 - - uid: 1921 - components: - - type: Transform - pos: -17.5,-23.5 - parent: 104 - - uid: 1922 - components: - - type: Transform - pos: -16.5,-23.5 - parent: 104 - - uid: 1945 - components: - - type: Transform - pos: -10.5,-23.5 - parent: 104 - - uid: 1946 - components: - - type: Transform - pos: -9.5,-23.5 - parent: 104 - - uid: 1951 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -34.5,2.5 - parent: 104 - - uid: 2045 - components: - - type: Transform - pos: -32.5,0.5 - parent: 104 - - uid: 2083 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-9.5 - parent: 104 - - uid: 2089 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-12.5 - parent: 104 - - uid: 2097 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-8.5 - parent: 104 - - uid: 2126 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-10.5 - parent: 104 - - uid: 2135 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -10.5,-11.5 - parent: 104 - - uid: 2482 - components: - - type: Transform - pos: -30.5,0.5 - parent: 104 - - uid: 2483 - components: - - type: Transform - pos: -28.5,0.5 - parent: 104 - - uid: 2484 - components: - - type: Transform - pos: -27.5,0.5 - parent: 104 - - uid: 2485 - components: - - type: Transform - pos: -26.5,0.5 - parent: 104 - - uid: 2486 - components: - - type: Transform - pos: -24.5,0.5 - parent: 104 - - uid: 2487 - components: - - type: Transform - pos: -23.5,0.5 - parent: 104 - - uid: 2488 - components: - - type: Transform - pos: -22.5,0.5 - parent: 104 - - uid: 2489 - components: - - type: Transform - pos: -19.5,0.5 - parent: 104 - - uid: 2490 - components: - - type: Transform - pos: -18.5,0.5 - parent: 104 - - uid: 2491 - components: - - type: Transform - pos: -16.5,0.5 - parent: 104 - - uid: 2492 - components: - - type: Transform - pos: -15.5,0.5 - parent: 104 - - uid: 2493 - components: - - type: Transform - pos: -14.5,0.5 - parent: 104 - - uid: 2494 - components: - - type: Transform - pos: -12.5,0.5 - parent: 104 - - uid: 2495 - components: - - type: Transform - pos: -11.5,0.5 - parent: 104 - - uid: 2497 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -11.5,-19.5 - parent: 104 - - uid: 2498 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -12.5,-19.5 - parent: 104 - - uid: 2499 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -14.5,-19.5 - parent: 104 - - uid: 2500 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -15.5,-19.5 - parent: 104 - - uid: 2501 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -16.5,-19.5 - parent: 104 - - uid: 2502 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -18.5,-19.5 - parent: 104 - - uid: 2503 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -19.5,-19.5 - parent: 104 - - uid: 2504 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -20.5,-19.5 - parent: 104 - - uid: 2506 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -22.5,-19.5 - parent: 104 - - uid: 2509 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -23.5,-19.5 - parent: 104 - - uid: 2510 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -24.5,-19.5 - parent: 104 - - uid: 2511 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -26.5,-19.5 - parent: 104 - - uid: 2512 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -27.5,-19.5 - parent: 104 - - uid: 2513 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -28.5,-19.5 - parent: 104 - - uid: 2516 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -30.5,-19.5 - parent: 104 - - uid: 2517 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -31.5,-19.5 - parent: 104 - - uid: 2518 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -32.5,-19.5 - parent: 104 - - uid: 2519 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -34.5,-19.5 - parent: 104 - - uid: 2520 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-20.5 - parent: 104 -- proto: RailingCorner - entities: - - uid: 1790 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -35.5,-23.5 - parent: 104 - - uid: 1950 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -34.5,4.5 - parent: 104 - - uid: 2496 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -34.5,0.5 - parent: 104 - - uid: 2522 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -35.5,-19.5 - parent: 104 -- proto: RailingCornerSmall - entities: - - uid: 1285 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -8.5,-23.5 - parent: 104 - - uid: 1761 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -8.5,8.5 - parent: 104 - - uid: 1869 - components: - - type: Transform - pos: -8.5,-24.5 - parent: 104 - - uid: 1873 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -9.5,-24.5 - parent: 104 - - uid: 1883 - components: - - type: Transform - pos: -18.5,4.5 - parent: 104 - - uid: 2523 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -10.5,0.5 - parent: 104 - - uid: 2524 - components: - - type: Transform - pos: -10.5,-19.5 - parent: 104 -- proto: ReinforcedPlasmaWindow - entities: - - uid: 1842 - components: - - type: Transform - pos: 12.5,-23.5 - parent: 104 - - uid: 1848 - components: - - type: Transform - pos: 14.5,-23.5 - parent: 104 - - uid: 1849 - components: - - type: Transform - pos: 11.5,-23.5 - parent: 104 - - uid: 1853 - components: - - type: Transform - pos: 13.5,-23.5 - parent: 104 - - uid: 1855 - components: - - type: Transform - pos: 10.5,-23.5 - parent: 104 -- proto: ReinforcedWindow - entities: - - uid: 52 - components: - - type: Transform - pos: 16.5,-6.5 - parent: 104 - - uid: 73 - components: - - type: Transform - pos: 11.5,1.5 - parent: 104 - - uid: 89 - components: - - type: Transform - pos: 7.5,-9.5 - parent: 104 - - uid: 99 - components: - - type: Transform - pos: 0.5,1.5 - parent: 104 - - uid: 103 - components: - - type: Transform - pos: 4.5,1.5 - parent: 104 - - uid: 127 - components: - - type: Transform - pos: 11.5,-8.5 - parent: 104 - - uid: 128 - components: - - type: Transform - pos: 11.5,-6.5 - parent: 104 - - uid: 129 - components: - - type: Transform - pos: 16.5,-8.5 - parent: 104 - - uid: 155 - components: - - type: Transform - pos: 2.5,3.5 - parent: 104 - - uid: 223 - components: - - type: Transform - pos: 4.5,2.5 - parent: 104 - - uid: 224 - components: - - type: Transform - pos: 4.5,3.5 - parent: 104 - - uid: 353 - components: - - type: Transform - pos: 2.5,2.5 - parent: 104 - - uid: 498 - components: - - type: Transform - pos: -1.5,-17.5 - parent: 104 - - uid: 530 - components: - - type: Transform - pos: 8.5,20.5 - parent: 104 - - uid: 649 - components: - - type: Transform - pos: -12.5,5.5 - parent: 104 - - uid: 1037 - components: - - type: Transform - pos: 1.5,21.5 - parent: 104 - - uid: 1038 - components: - - type: Transform - pos: 6.5,21.5 - parent: 104 - - uid: 1039 - components: - - type: Transform - pos: 8.5,21.5 - parent: 104 - - uid: 1040 - components: - - type: Transform - pos: 7.5,21.5 - parent: 104 - - uid: 1102 - components: - - type: Transform - pos: 8.5,-17.5 - parent: 104 - - uid: 1103 - components: - - type: Transform - pos: 2.5,21.5 - parent: 104 - - uid: 1118 - components: - - type: Transform - pos: 0.5,-17.5 - parent: 104 - - uid: 1142 - components: - - type: Transform - pos: -14.5,5.5 - parent: 104 - - uid: 1186 - components: - - type: Transform - pos: 2.5,-15.5 - parent: 104 - - uid: 1196 - components: - - type: Transform - pos: -0.5,-17.5 - parent: 104 - - uid: 1198 - components: - - type: Transform - pos: 2.5,-13.5 - parent: 104 - - uid: 1382 - components: - - type: Transform - pos: -1.5,-0.5 - parent: 104 - - uid: 1385 - components: - - type: Transform - pos: 2.5,1.5 - parent: 104 - - uid: 1820 - components: - - type: Transform - pos: 5.5,21.5 - parent: 104 - - uid: 1821 - components: - - type: Transform - pos: 0.5,21.5 - parent: 104 - - uid: 1828 - components: - - type: Transform - pos: 4.5,21.5 - parent: 104 - - uid: 1829 - components: - - type: Transform - pos: 0.5,20.5 - parent: 104 - - uid: 1929 - components: - - type: Transform - pos: 3.5,21.5 - parent: 104 - - uid: 2081 - components: - - type: Transform - pos: 5.5,-9.5 - parent: 104 -- proto: RemoteSignaller - entities: - - uid: 1322 - components: - - type: Transform - pos: 5.611122,-17.316748 - parent: 104 - - uid: 1332 - components: - - type: Transform - pos: 5.267372,-17.316748 - parent: 104 -- proto: Screwdriver - entities: - - uid: 1325 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 4.954872,-17.44188 - parent: 104 -- proto: SheetGlass - entities: - - uid: 2421 - components: - - type: Transform - pos: 20.5,-16.5 - parent: 104 - - uid: 2422 - components: - - type: Transform - pos: 20.5,-16.5 - parent: 104 -- proto: SheetPlasteel - entities: - - uid: 2423 - components: - - type: Transform - pos: 24.5,-15.5 - parent: 104 - - uid: 2424 - components: - - type: Transform - pos: 24.5,-15.5 - parent: 104 -- proto: SheetPlastic - entities: - - uid: 460 - components: - - type: Transform - pos: 12.60107,-16.44067 - parent: 104 -- proto: SheetSteel - entities: - - uid: 286 - components: - - type: Transform - pos: 13.50732,-16.456295 - parent: 104 - - uid: 1350 - components: - - type: Transform - pos: 3.460312,-15.657375 - parent: 104 - - uid: 2419 - components: - - type: Transform - pos: 20.5,-15.5 - parent: 104 - - uid: 2420 - components: - - type: Transform - pos: 20.5,-15.5 - parent: 104 -- proto: SignalTrigger - entities: - - uid: 1330 - components: - - type: Transform - pos: 3.6284866,-16.955233 - parent: 104 - - uid: 1331 - components: - - type: Transform - pos: 3.3003616,-17.06472 - parent: 104 -- proto: SignDirectionalBar - entities: - - uid: 2297 - components: - - type: Transform - pos: -1.5,-4.5 - parent: 104 -- proto: SignEscapePods - entities: - - uid: 1737 - components: - - type: MetaData - desc: steel rain babey - name: drop pod sign - - type: Transform - rot: 1.5707963267948966 rad - pos: 16.5,-8.5 - parent: 104 -- proto: SignMedical - entities: - - uid: 351 - components: - - type: Transform - pos: 4.5,-9.5 - parent: 104 -- proto: SignSurgery - entities: - - uid: 266 - components: - - type: Transform - pos: 2.5,-12.5 - parent: 104 -- proto: SignToxins2 - entities: - - uid: 1318 - components: - - type: Transform - pos: 8.5,-15.5 - parent: 104 -- proto: SinkStemlessWater - entities: - - uid: 22 - components: - - type: Transform - pos: 15.5,-1.5 - parent: 104 -- proto: SinkWide - entities: - - uid: 144 - components: - - type: Transform - pos: 9.5,0.5 - parent: 104 - - uid: 145 - components: - - type: Transform - pos: 10.5,0.5 - parent: 104 - - uid: 1246 - components: - - type: Transform - pos: 9.5,-3.5 - parent: 104 - - uid: 1358 - components: - - type: Transform - pos: -4.5,-2.5 - parent: 104 - - uid: 2311 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-7.5 - parent: 104 -- proto: SmallLight - entities: - - uid: 484 - components: - - type: Transform - pos: -12.5,7.5 - parent: 104 - - uid: 1024 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-6.5 - parent: 104 - - uid: 1141 - components: - - type: Transform - pos: -13.5,9.5 - parent: 104 - - uid: 1766 - components: - - type: Transform - pos: 11.5,-16.5 - parent: 104 -- proto: SMESBasic - entities: - - uid: 2331 - components: - - type: Transform - pos: 21.5,-18.5 - parent: 104 - - uid: 2332 - components: - - type: Transform - pos: 22.5,-18.5 - parent: 104 - - uid: 2333 - components: - - type: Transform - pos: 23.5,-18.5 - parent: 104 -- proto: soda_dispenser - entities: - - uid: 225 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-8.5 - parent: 104 -- proto: SpaceVillainArcadeFilled - entities: - - uid: 1652 - components: - - type: Transform - pos: 10.5,-6.5 - parent: 104 - - uid: 1653 - components: - - type: Transform - pos: 9.5,-6.5 - parent: 104 -- proto: SpawnMobLizard - entities: - - uid: 2473 - components: - - type: Transform - pos: 5.5,-1.5 - parent: 104 -- proto: SpawnPointNukies - entities: - - uid: 167 - components: - - type: Transform - pos: 3.5,-5.5 - parent: 104 - - uid: 172 - components: - - type: Transform - pos: 3.5,-7.5 - parent: 104 - - uid: 173 - components: - - type: Transform - pos: 0.5,-5.5 - parent: 104 - - uid: 2477 - components: - - type: Transform - pos: 4.5,-3.5 - parent: 104 - - uid: 2478 - components: - - type: Transform - pos: 3.5,-1.5 - parent: 104 - - uid: 2479 - components: - - type: Transform - pos: 2.5,-1.5 - parent: 104 - - uid: 2480 - components: - - type: Transform - pos: 1.5,-2.5 - parent: 104 -- proto: Spear - entities: - - uid: 3366 - components: - - type: Transform - pos: -5.3505287,20.508072 - parent: 104 -- proto: SprayBottle - entities: - - uid: 1699 - components: - - type: Transform - pos: 14.060668,-16.439913 - parent: 104 -- proto: StasisBed - entities: - - uid: 1130 - components: - - type: Transform - pos: -2.5,-14.5 - parent: 104 -- proto: Stool - entities: - - uid: 156 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-3.5 - parent: 104 - - uid: 158 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-2.5 - parent: 104 - - uid: 164 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-3.5 - parent: 104 - - uid: 166 - components: - - type: Transform - pos: 2.5,-1.5 - parent: 104 - - uid: 169 - components: - - type: Transform - pos: 3.5,-1.5 - parent: 104 - - uid: 194 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 10.5,-13.5 - parent: 104 - - uid: 195 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 12.5,-14.5 - parent: 104 - - uid: 200 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 10.5,-14.5 - parent: 104 - - uid: 507 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 11.5,-1.5 - parent: 104 - - uid: 1334 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 12.5,-1.5 - parent: 104 - - uid: 1369 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 14.5,-13.5 - parent: 104 - - uid: 1371 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 12.5,-13.5 - parent: 104 - - uid: 1372 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 14.5,-14.5 - parent: 104 -- proto: StoolBar - entities: - - uid: 1 - components: - - type: Transform - pos: -0.5,-5.5 - parent: 104 - - uid: 2 - components: - - type: Transform - pos: 3.5,-5.5 - parent: 104 - - uid: 3 - components: - - type: Transform - pos: 0.5,-5.5 - parent: 104 - - uid: 4 - components: - - type: Transform - pos: 1.5,-5.5 - parent: 104 - - uid: 5 - components: - - type: Transform - pos: 2.5,-5.5 - parent: 104 - - uid: 6 - components: - - type: Transform - pos: 4.5,-5.5 - parent: 104 -- proto: SubstationBasic - entities: - - uid: 2366 - components: - - type: Transform - pos: 27.5,-13.5 - parent: 104 -- proto: SurveillanceCameraGeneral - entities: - - uid: 85 - components: - - type: Transform - pos: 1.5,-8.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraGeneral - nameSet: True - id: Bar - - uid: 1144 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -6.5,-16.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraGeneral - nameSet: True - id: Dock - - uid: 1185 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -10.5,7.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraGeneral - nameSet: True - id: Surveillance Shack - - uid: 1268 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 15.5,-12.5 - parent: 104 - - uid: 1269 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-12.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraGeneral - nameSet: True - id: Medbay - - uid: 1270 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 7.5,-12.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraGeneral - nameSet: True - id: Chem and Bombs - - uid: 1272 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 13.5,-0.5 - parent: 104 - - uid: 1274 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -3.5,-2.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraGeneral - nameSet: True - id: Entrance - - uid: 1320 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 11.5,-4.5 - parent: 104 - - uid: 1321 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 14.5,-3.5 - parent: 104 -- proto: SurveillanceCameraRouterGeneral - entities: - - uid: 620 - components: - - type: Transform - pos: -15.5,9.5 - parent: 104 -- proto: SurveillanceCameraWirelessRouterEntertainment - entities: - - uid: 978 - components: - - type: Transform - pos: -15.5,8.5 - parent: 104 -- proto: SurveillanceWirelessCameraAnchoredEntertainment - entities: - - uid: 1023 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -13.5,21.5 - parent: 104 - - type: SurveillanceCamera - setupAvailableNetworks: - - SurveillanceCameraEntertainment - nameSet: True - id: Weeh -- proto: SyndicatePersonalAI - entities: - - uid: 160 - components: - - type: Transform - pos: 1.4711013,-3.6405277 - parent: 104 -- proto: SyndieFlag - entities: - - uid: 819 - components: - - type: Transform - pos: 11.5,-9.5 - parent: 104 -- proto: SyndieHandyFlag - entities: - - uid: 1810 - components: - - type: Transform - pos: 3.5076954,-6.413422 - parent: 104 -- proto: SyringeInaprovaline - entities: - - uid: 1151 - components: - - type: Transform - pos: -2.530845,-12.414692 - parent: 104 -- proto: Table - entities: - - uid: 16 - components: - - type: Transform - pos: 12.5,-6.5 - parent: 104 - - uid: 107 - components: - - type: Transform - pos: 7.5,-4.5 - parent: 104 - - uid: 1090 - components: - - type: Transform - pos: 7.5,-11.5 - parent: 104 - - uid: 1193 - components: - - type: Transform - pos: 7.5,-12.5 - parent: 104 - - uid: 1300 - components: - - type: Transform - pos: 7.5,-10.5 - parent: 104 - - uid: 1813 - components: - - type: Transform - pos: 12.5,-16.5 - parent: 104 - - uid: 1814 - components: - - type: Transform - pos: 13.5,-16.5 - parent: 104 - - uid: 1815 - components: - - type: Transform - pos: 14.5,-16.5 - parent: 104 - - uid: 1859 - components: - - type: Transform - pos: 14.5,-17.5 - parent: 104 - - uid: 2321 - components: - - type: Transform - pos: 13.5,-6.5 - parent: 104 -- proto: TableCarpet - entities: - - uid: 9 - components: - - type: Transform - pos: 13.5,-8.5 - parent: 104 -- proto: TableGlass - entities: - - uid: 980 - components: - - type: Transform - pos: -2.5,-16.5 - parent: 104 - - uid: 1125 - components: - - type: Transform - pos: -2.5,-12.5 - parent: 104 - - uid: 2090 - components: - - type: Transform - pos: 0.5,-16.5 - parent: 104 - - uid: 2092 - components: - - type: Transform - pos: 0.5,-12.5 - parent: 104 -- proto: TablePlasmaGlass - entities: - - uid: 2411 - components: - - type: Transform - pos: 20.5,-16.5 - parent: 104 - - uid: 2412 - components: - - type: Transform - pos: 20.5,-15.5 - parent: 104 - - uid: 2413 - components: - - type: Transform - pos: 24.5,-16.5 - parent: 104 - - uid: 2414 - components: - - type: Transform - pos: 24.5,-15.5 - parent: 104 - - uid: 2431 - components: - - type: Transform - pos: 17.5,-16.5 - parent: 104 - - uid: 2432 - components: - - type: Transform - pos: 17.5,-15.5 - parent: 104 - - uid: 2433 - components: - - type: Transform - pos: 27.5,-16.5 - parent: 104 - - uid: 2434 - components: - - type: Transform - pos: 27.5,-15.5 - parent: 104 -- proto: TableReinforced - entities: - - uid: 201 - components: - - type: Transform - pos: 13.5,-10.5 - parent: 104 - - uid: 202 - components: - - type: Transform - pos: 12.5,-10.5 - parent: 104 - - uid: 203 - components: - - type: Transform - pos: 14.5,-10.5 - parent: 104 - - uid: 208 - components: - - type: Transform - pos: 11.5,-10.5 - parent: 104 - - uid: 226 - components: - - type: Transform - pos: 5.5,10.5 - parent: 104 - - uid: 229 - components: - - type: Transform - pos: 5.5,11.5 - parent: 104 - - uid: 239 - components: - - type: Transform - pos: 5.5,13.5 - parent: 104 - - uid: 241 - components: - - type: Transform - pos: 5.5,14.5 - parent: 104 - - uid: 285 - components: - - type: Transform - pos: 5.5,12.5 - parent: 104 - - uid: 697 - components: - - type: Transform - pos: -13.5,5.5 - parent: 104 - - uid: 1129 - components: - - type: Transform - pos: -14.5,9.5 - parent: 104 - - uid: 1149 - components: - - type: Transform - pos: -13.5,9.5 - parent: 104 -- proto: TableReinforcedGlass - entities: - - uid: 838 - components: - - type: Transform - pos: 5.5,-17.5 - parent: 104 - - uid: 975 - components: - - type: Transform - pos: 4.5,-17.5 - parent: 104 - - uid: 976 - components: - - type: Transform - pos: 3.5,-17.5 - parent: 104 - - uid: 1192 - components: - - type: Transform - pos: 3.5,-16.5 - parent: 104 - - uid: 1351 - components: - - type: Transform - pos: 3.5,-15.5 - parent: 104 - - uid: 1739 - components: - - type: Transform - pos: 5.5,-12.5 - parent: 104 - - uid: 2016 - components: - - type: Transform - pos: 4.5,-12.5 - parent: 104 - - uid: 2022 - components: - - type: Transform - pos: 3.5,-10.5 - parent: 104 - - uid: 2027 - components: - - type: Transform - pos: 3.5,-11.5 - parent: 104 - - uid: 2028 - components: - - type: Transform - pos: 3.5,-12.5 - parent: 104 -- proto: TableWood - entities: - - uid: 8 - components: - - type: Transform - pos: 3.5,-3.5 - parent: 104 - - uid: 28 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 2.5,-8.5 - parent: 104 - - uid: 30 - components: - - type: Transform - pos: 3.5,-8.5 - parent: 104 - - uid: 31 - components: - - type: Transform - pos: 4.5,-8.5 - parent: 104 - - uid: 32 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 4.5,-6.5 - parent: 104 - - uid: 33 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 3.5,-6.5 - parent: 104 - - uid: 34 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 2.5,-6.5 - parent: 104 - - uid: 35 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-6.5 - parent: 104 - - uid: 36 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-6.5 - parent: 104 - - uid: 37 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-6.5 - parent: 104 - - uid: 159 - components: - - type: Transform - pos: 2.5,-3.5 - parent: 104 - - uid: 162 - components: - - type: Transform - pos: 2.5,-2.5 - parent: 104 - - uid: 355 - components: - - type: Transform - pos: 3.5,-2.5 - parent: 104 -- proto: TargetClown - entities: - - uid: 1291 - components: - - type: Transform - pos: 0.5,12.5 - parent: 104 -- proto: TargetHuman - entities: - - uid: 1293 - components: - - type: Transform - pos: 0.5,14.5 - parent: 104 - - uid: 2308 - components: - - type: Transform - pos: 12.5,-21.5 - parent: 104 -- proto: TargetStrange - entities: - - uid: 1294 - components: - - type: Transform - pos: 0.5,10.5 - parent: 104 -- proto: TearGasGrenade - entities: - - uid: 306 - components: - - type: Transform - pos: 14.638793,-16.908663 - parent: 104 - - uid: 1703 - components: - - type: Transform - pos: 14.341918,-16.877413 - parent: 104 -- proto: Telecrystal1 - entities: - - uid: 2474 - components: - - type: Transform - pos: 2.9398513,-3.4842777 - parent: 104 -- proto: TimerTrigger - entities: - - uid: 1225 - components: - - type: Transform - pos: 3.7048721,-16.445147 - parent: 104 - - uid: 1329 - components: - - type: Transform - pos: 3.3003616,-16.470352 - parent: 104 -- proto: ToiletEmpty - entities: - - uid: 293 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 15.5,-1.5 - parent: 104 -- proto: ToolboxElectricalFilled - entities: - - uid: 206 - components: - - type: Transform - pos: 13.241143,-10.4236145 - parent: 104 -- proto: ToolboxEmergencyFilled - entities: - - uid: 1368 - components: - - type: Transform - pos: -2.4700382,-4.362167 - parent: 104 -- proto: ToolboxSyndicateFilled - entities: - - uid: 205 - components: - - type: Transform - pos: 12.366143,-10.392332 - parent: 104 -- proto: ToyFigurineCaptain - entities: - - uid: 2470 - components: - - type: Transform - pos: 2.5657434,-3.260709 - parent: 104 -- proto: ToyFigurineNukie - entities: - - uid: 14 - components: - - type: Transform - pos: 3.4242263,-2.6717777 - parent: 104 - - uid: 161 - components: - - type: Transform - pos: 3.5492263,-2.4374027 - parent: 104 - - uid: 2468 - components: - - type: Transform - pos: 3.6429763,-2.6717777 - parent: 104 -- proto: ToyFigurineNukieCommander - entities: - - uid: 2471 - components: - - type: Transform - pos: 3.1898513,-2.9530277 - parent: 104 -- proto: ToyFigurineNukieElite - entities: - - uid: 2467 - components: - - type: Transform - pos: 3.1117263,-2.6874027 - parent: 104 -- proto: ToyMauler - entities: - - uid: 2469 - components: - - type: Transform - pos: 3.0179763,-2.3124027 - parent: 104 -- proto: ToyNuke - entities: - - uid: 26 - components: - - type: Transform - pos: 4.4214306,-6.3946886 - parent: 104 - - uid: 284 - components: - - type: Transform - pos: 12.453509,14.985125 - parent: 104 -- proto: ToySkeleton - entities: - - uid: 17 - components: - - type: Transform - pos: 3.5882664,-8.344303 - parent: 104 -- proto: VariantCubeBox - entities: - - uid: 1826 - components: - - type: Transform - pos: 14.485744,-16.427467 - parent: 104 -- proto: VendingMachineBooze - entities: - - uid: 1380 - components: - - type: Transform - pos: -0.5,-8.5 - parent: 104 -- proto: VendingMachineChang - entities: - - uid: 1208 - components: - - type: Transform - pos: -10.5,7.5 - parent: 104 -- proto: VendingMachineChemicalsSyndicate - entities: - - uid: 1335 - components: - - type: Transform - pos: 4.5,-10.5 - parent: 104 -- proto: VendingMachineCigs - entities: - - uid: 117 - components: - - type: MetaData - name: cigarette machine - - type: Transform - pos: 7.5,0.5 - parent: 104 -- proto: VendingMachineClothing - entities: - - uid: 1617 - components: - - type: Transform - pos: 13.5,0.5 - parent: 104 -- proto: VendingMachineCola - entities: - - uid: 116 - components: - - type: Transform - pos: 5.5,0.5 - parent: 104 - - uid: 1244 - components: - - type: Transform - pos: -9.5,7.5 - parent: 104 -- proto: VendingMachineSnack - entities: - - uid: 2442 - components: - - type: Transform - pos: 6.5,0.5 - parent: 104 -- proto: VendingMachineSyndieDrobe - entities: - - uid: 1299 - components: - - type: Transform - pos: 11.5,0.5 - parent: 104 -- proto: VendingMachineTheater - entities: - - uid: 1680 - components: - - type: Transform - pos: 12.5,0.5 - parent: 104 -- proto: VendingMachineYouTool - entities: - - uid: 204 - components: - - type: Transform - pos: 15.5,-10.5 - parent: 104 -- proto: WallIce - entities: - - uid: 236 - components: - - type: Transform - pos: -0.5,12.5 - parent: 104 - - uid: 356 - components: - - type: Transform - pos: -6.5,21.5 - parent: 104 - - uid: 357 - components: - - type: Transform - pos: -3.5,21.5 - parent: 104 - - uid: 416 - components: - - type: Transform - pos: -4.5,21.5 - parent: 104 - - uid: 727 - components: - - type: Transform - pos: -6.5,19.5 - parent: 104 - - uid: 1132 - components: - - type: Transform - pos: -4.5,22.5 - parent: 104 - - uid: 1133 - components: - - type: Transform - pos: -5.5,22.5 - parent: 104 - - uid: 1134 - components: - - type: Transform - pos: -6.5,22.5 - parent: 104 - - uid: 1221 - components: - - type: Transform - pos: -6.5,20.5 - parent: 104 - - uid: 1549 - components: - - type: Transform - pos: -0.5,10.5 - parent: 104 - - uid: 1670 - components: - - type: Transform - pos: -0.5,14.5 - parent: 104 -- proto: WallmountTelevision - entities: - - uid: 977 - components: - - type: Transform - pos: 6.5,1.5 - parent: 104 -- proto: WallPlastitanium - entities: - - uid: 38 - components: - - type: Transform - pos: 12.5,-4.5 - parent: 104 - - uid: 39 - components: - - type: Transform - pos: 12.5,-3.5 - parent: 104 - - uid: 40 - components: - - type: Transform - pos: 9.5,-5.5 - parent: 104 - - uid: 41 - components: - - type: Transform - pos: 10.5,-5.5 - parent: 104 - - uid: 42 - components: - - type: Transform - pos: 11.5,-5.5 - parent: 104 - - uid: 43 - components: - - type: Transform - pos: 12.5,-5.5 - parent: 104 - - uid: 44 - components: - - type: Transform - pos: 12.5,-5.5 - parent: 104 - - uid: 45 - components: - - type: Transform - pos: 16.5,-5.5 - parent: 104 - - uid: 46 - components: - - type: Transform - pos: 20.5,-2.5 - parent: 104 - - uid: 47 - components: - - type: Transform - pos: 22.5,-2.5 - parent: 104 - - uid: 50 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-12.5 - parent: 104 - - uid: 51 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 19.5,-12.5 - parent: 104 - - uid: 53 - components: - - type: Transform - pos: 17.5,-2.5 - parent: 104 - - uid: 54 - components: - - type: Transform - pos: 18.5,-2.5 - parent: 104 - - uid: 57 - components: - - type: Transform - pos: 13.5,-5.5 - parent: 104 - - uid: 59 - components: - - type: Transform - pos: 8.5,-5.5 - parent: 104 - - uid: 60 - components: - - type: Transform - pos: 8.5,-4.5 - parent: 104 - - uid: 61 - components: - - type: Transform - pos: 8.5,-3.5 - parent: 104 - - uid: 62 - components: - - type: Transform - pos: 12.5,-9.5 - parent: 104 - - uid: 63 - components: - - type: Transform - pos: 11.5,-9.5 - parent: 104 - - uid: 64 - components: - - type: Transform - pos: 10.5,-9.5 - parent: 104 - - uid: 66 - components: - - type: Transform - pos: 8.5,-1.5 - parent: 104 - - uid: 67 - components: - - type: Transform - pos: 8.5,-2.5 - parent: 104 - - uid: 68 - components: - - type: Transform - pos: 9.5,-2.5 - parent: 104 - - uid: 69 - components: - - type: Transform - pos: 11.5,-2.5 - parent: 104 - - uid: 70 - components: - - type: Transform - pos: 14.5,1.5 - parent: 104 - - uid: 71 - components: - - type: Transform - pos: 13.5,1.5 - parent: 104 - - uid: 72 - components: - - type: Transform - pos: 12.5,1.5 - parent: 104 - - uid: 74 - components: - - type: Transform - pos: 10.5,1.5 - parent: 104 - - uid: 75 - components: - - type: Transform - pos: 9.5,1.5 - parent: 104 - - uid: 76 - components: - - type: Transform - pos: 8.5,0.5 - parent: 104 - - uid: 77 - components: - - type: Transform - pos: 8.5,1.5 - parent: 104 - - uid: 78 - components: - - type: Transform - pos: 7.5,1.5 - parent: 104 - - uid: 79 - components: - - type: Transform - pos: 8.5,-11.5 - parent: 104 - - uid: 80 - components: - - type: Transform - pos: 8.5,-9.5 - parent: 104 - - uid: 82 - components: - - type: Transform - pos: 1.5,-9.5 - parent: 104 - - uid: 83 - components: - - type: Transform - pos: 0.5,-9.5 - parent: 104 - - uid: 84 - components: - - type: Transform - pos: -0.5,-9.5 - parent: 104 - - uid: 86 - components: - - type: Transform - pos: 2.5,-9.5 - parent: 104 - - uid: 87 - components: - - type: Transform - pos: 3.5,-9.5 - parent: 104 - - uid: 88 - components: - - type: Transform - pos: 4.5,-9.5 - parent: 104 - - uid: 90 - components: - - type: Transform - pos: -1.5,-8.5 - parent: 104 - - uid: 91 - components: - - type: Transform - pos: -1.5,-7.5 - parent: 104 - - uid: 92 - components: - - type: Transform - pos: -1.5,-6.5 - parent: 104 - - uid: 93 - components: - - type: Transform - pos: -1.5,-5.5 - parent: 104 - - uid: 94 - components: - - type: Transform - pos: -1.5,-1.5 - parent: 104 - - uid: 97 - components: - - type: Transform - pos: 6.5,1.5 - parent: 104 - - uid: 98 - components: - - type: Transform - pos: 5.5,1.5 - parent: 104 - - uid: 101 - components: - - type: Transform - pos: -0.5,0.5 - parent: 104 - - uid: 102 - components: - - type: Transform - pos: -0.5,1.5 - parent: 104 - - uid: 105 - components: - - type: Transform - pos: -1.5,0.5 - parent: 104 - - uid: 106 - components: - - type: Transform - pos: 14.5,0.5 - parent: 104 - - uid: 110 - components: - - type: Transform - pos: 12.5,-2.5 - parent: 104 - - uid: 113 - components: - - type: Transform - pos: 8.5,-10.5 - parent: 104 - - uid: 114 - components: - - type: Transform - pos: 16.5,-1.5 - parent: 104 - - uid: 119 - components: - - type: Transform - pos: 24.5,-2.5 - parent: 104 - - uid: 121 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 18.5,-12.5 - parent: 104 - - uid: 123 - components: - - type: Transform - pos: 21.5,-2.5 - parent: 104 - - uid: 124 - components: - - type: Transform - pos: 19.5,-2.5 - parent: 104 - - uid: 125 - components: - - type: Transform - pos: 16.5,-3.5 - parent: 104 - - uid: 126 - components: - - type: Transform - pos: 16.5,-4.5 - parent: 104 - - uid: 132 - components: - - type: Transform - pos: 23.5,-2.5 - parent: 104 - - uid: 133 - components: - - type: Transform - pos: 1.5,1.5 - parent: 104 - - uid: 137 - components: - - type: Transform - pos: 8.5,-12.5 - parent: 104 - - uid: 168 - components: - - type: Transform - pos: 14.5,-9.5 - parent: 104 - - uid: 171 - components: - - type: Transform - pos: 13.5,-9.5 - parent: 104 - - uid: 174 - components: - - type: Transform - pos: 15.5,-9.5 - parent: 104 - - uid: 175 - components: - - type: Transform - pos: 16.5,-9.5 - parent: 104 - - uid: 176 - components: - - type: Transform - pos: 15.5,-5.5 - parent: 104 - - uid: 177 - components: - - type: Transform - pos: 8.5,-13.5 - parent: 104 - - uid: 179 - components: - - type: Transform - pos: 8.5,-14.5 - parent: 104 - - uid: 180 - components: - - type: Transform - pos: 8.5,-15.5 - parent: 104 - - uid: 181 - components: - - type: Transform - pos: 9.5,-15.5 - parent: 104 - - uid: 182 - components: - - type: Transform - pos: 10.5,-15.5 - parent: 104 - - uid: 183 - components: - - type: Transform - pos: 11.5,-15.5 - parent: 104 - - uid: 184 - components: - - type: Transform - pos: 12.5,-15.5 - parent: 104 - - uid: 185 - components: - - type: Transform - pos: 13.5,-15.5 - parent: 104 - - uid: 186 - components: - - type: Transform - pos: 14.5,-15.5 - parent: 104 - - uid: 187 - components: - - type: Transform - pos: 15.5,-15.5 - parent: 104 - - uid: 188 - components: - - type: Transform - pos: 16.5,-15.5 - parent: 104 - - uid: 189 - components: - - type: Transform - pos: 16.5,-14.5 - parent: 104 - - uid: 191 - components: - - type: Transform - pos: 16.5,-12.5 - parent: 104 - - uid: 192 - components: - - type: Transform - pos: 16.5,-11.5 - parent: 104 - - uid: 193 - components: - - type: Transform - pos: 16.5,-10.5 - parent: 104 - - uid: 199 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-17.5 - parent: 104 - - uid: 215 - components: - - type: Transform - pos: 16.5,-0.5 - parent: 104 - - uid: 242 - components: - - type: Transform - pos: -4.5,-5.5 - parent: 104 - - uid: 252 - components: - - type: Transform - pos: -3.5,-15.5 - parent: 104 - - uid: 288 - components: - - type: Transform - pos: 14.5,-0.5 - parent: 104 - - uid: 290 - components: - - type: Transform - pos: 15.5,-0.5 - parent: 104 - - uid: 298 - components: - - type: Transform - pos: 13.5,-2.5 - parent: 104 - - uid: 299 - components: - - type: Transform - pos: 14.5,-2.5 - parent: 104 - - uid: 300 - components: - - type: Transform - pos: 15.5,-2.5 - parent: 104 - - uid: 301 - components: - - type: Transform - pos: 16.5,-2.5 - parent: 104 - - uid: 307 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 23.5,-8.5 - parent: 104 - - uid: 308 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 19.5,-10.5 - parent: 104 - - uid: 309 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-11.5 - parent: 104 - - uid: 310 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-12.5 - parent: 104 - - uid: 311 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 24.5,-12.5 - parent: 104 - - uid: 312 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 21.5,-12.5 - parent: 104 - - uid: 313 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 23.5,-12.5 - parent: 104 - - uid: 314 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 22.5,-12.5 - parent: 104 - - uid: 315 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 20.5,-12.5 - parent: 104 - - uid: 316 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 23.5,-6.5 - parent: 104 - - uid: 317 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 21.5,-4.5 - parent: 104 - - uid: 318 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 17.5,-6.5 - parent: 104 - - uid: 319 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 19.5,-4.5 - parent: 104 - - uid: 320 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 17.5,-8.5 - parent: 104 - - uid: 321 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 21.5,-10.5 - parent: 104 - - uid: 322 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 20.5,-7.5 - parent: 104 - - uid: 339 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-10.5 - parent: 104 - - uid: 340 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-9.5 - parent: 104 - - uid: 341 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-8.5 - parent: 104 - - uid: 342 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-7.5 - parent: 104 - - uid: 343 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-6.5 - parent: 104 - - uid: 344 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-5.5 - parent: 104 - - uid: 345 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-4.5 - parent: 104 - - uid: 346 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-3.5 - parent: 104 - - uid: 347 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 25.5,-2.5 - parent: 104 - - uid: 378 - components: - - type: Transform - pos: -3.5,-16.5 - parent: 104 - - uid: 379 - components: - - type: Transform - pos: -3.5,-13.5 - parent: 104 - - uid: 380 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-11.5 - parent: 104 - - uid: 418 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -12.5,8.5 - parent: 104 - - uid: 424 - components: - - type: Transform - pos: -10.5,8.5 - parent: 104 - - uid: 427 - components: - - type: Transform - pos: -1.5,-9.5 - parent: 104 - - uid: 441 - components: - - type: Transform - pos: -2.5,-1.5 - parent: 104 - - uid: 443 - components: - - type: Transform - pos: -3.5,-1.5 - parent: 104 - - uid: 462 - components: - - type: Transform - pos: -5.5,-5.5 - parent: 104 - - uid: 463 - components: - - type: Transform - pos: -4.5,-1.5 - parent: 104 - - uid: 474 - components: - - type: Transform - pos: -5.5,-1.5 - parent: 104 - - uid: 482 - components: - - type: Transform - pos: -9.5,8.5 - parent: 104 - - uid: 486 - components: - - type: Transform - pos: -16.5,8.5 - parent: 104 - - uid: 493 - components: - - type: Transform - pos: -3.5,-12.5 - parent: 104 - - uid: 495 - components: - - type: Transform - pos: -2.5,-5.5 - parent: 104 - - uid: 497 - components: - - type: Transform - pos: -2.5,-17.5 - parent: 104 - - uid: 591 - components: - - type: Transform - pos: 15.5,-23.5 - parent: 104 - - uid: 592 - components: - - type: Transform - pos: 9.5,-20.5 - parent: 104 - - uid: 593 - components: - - type: Transform - pos: 9.5,-22.5 - parent: 104 - - uid: 594 - components: - - type: Transform - pos: 9.5,-18.5 - parent: 104 - - uid: 611 - components: - - type: Transform - pos: -5.5,-6.5 - parent: 104 - - uid: 613 - components: - - type: Transform - pos: -13.5,19.5 - parent: 104 - - uid: 623 - components: - - type: Transform - pos: -17.5,19.5 - parent: 104 - - uid: 651 - components: - - type: Transform - pos: -17.5,23.5 - parent: 104 - - uid: 671 - components: - - type: Transform - pos: -12.5,24.5 - parent: 104 - - uid: 675 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -12.5,9.5 - parent: 104 - - uid: 683 - components: - - type: Transform - pos: -15.5,10.5 - parent: 104 - - uid: 698 - components: - - type: Transform - pos: -14.5,10.5 - parent: 104 - - uid: 710 - components: - - type: Transform - pos: -13.5,10.5 - parent: 104 - - uid: 730 - components: - - type: Transform - pos: -12.5,10.5 - parent: 104 - - uid: 731 - components: - - type: Transform - pos: -12.5,22.5 - parent: 104 - - uid: 732 - components: - - type: Transform - pos: -15.5,19.5 - parent: 104 - - uid: 739 - components: - - type: Transform - pos: -17.5,21.5 - parent: 104 - - uid: 743 - components: - - type: Transform - pos: -16.5,9.5 - parent: 104 - - uid: 744 - components: - - type: Transform - pos: -16.5,7.5 - parent: 104 - - uid: 750 - components: - - type: Transform - pos: -3.5,-14.5 - parent: 104 - - uid: 767 - components: - - type: Transform - pos: -16.5,10.5 - parent: 104 - - uid: 773 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-11.5 - parent: 104 - - uid: 1025 - components: - - type: Transform - pos: 3.5,-18.5 - parent: 104 - - uid: 1026 - components: - - type: Transform - pos: 4.5,-18.5 - parent: 104 - - uid: 1091 - components: - - type: Transform - pos: -5.5,-2.5 - parent: 104 - - uid: 1104 - components: - - type: Transform - pos: 9.5,-19.5 - parent: 104 - - uid: 1110 - components: - - type: Transform - pos: -5.5,-4.5 - parent: 104 - - uid: 1123 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-11.5 - parent: 104 - - uid: 1126 - components: - - type: Transform - pos: -17.5,22.5 - parent: 104 - - uid: 1128 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -11.5,8.5 - parent: 104 - - uid: 1136 - components: - - type: Transform - pos: -11.5,5.5 - parent: 104 - - uid: 1137 - components: - - type: Transform - pos: -15.5,7.5 - parent: 104 - - uid: 1138 - components: - - type: Transform - pos: -15.5,6.5 - parent: 104 - - uid: 1139 - components: - - type: Transform - pos: -15.5,5.5 - parent: 104 - - uid: 1147 - components: - - type: Transform - pos: -11.5,7.5 - parent: 104 - - uid: 1181 - components: - - type: Transform - pos: 2.5,-16.5 - parent: 104 - - uid: 1184 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-11.5 - parent: 104 - - uid: 1188 - components: - - type: Transform - pos: 2.5,-18.5 - parent: 104 - - uid: 1190 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -2.5,-11.5 - parent: 104 - - uid: 1197 - components: - - type: Transform - pos: 7.5,-18.5 - parent: 104 - - uid: 1199 - components: - - type: Transform - pos: 5.5,-18.5 - parent: 104 - - uid: 1201 - components: - - type: Transform - pos: 6.5,-18.5 - parent: 104 - - uid: 1205 - components: - - type: Transform - pos: 2.5,-17.5 - parent: 104 - - uid: 1206 - components: - - type: Transform - pos: 8.5,-18.5 - parent: 104 - - uid: 1207 - components: - - type: Transform - pos: -17.5,24.5 - parent: 104 - - uid: 1209 - components: - - type: Transform - pos: -17.5,20.5 - parent: 104 - - uid: 1210 - components: - - type: Transform - pos: -14.5,19.5 - parent: 104 - - uid: 1211 - components: - - type: Transform - pos: -12.5,23.5 - parent: 104 - - uid: 1212 - components: - - type: Transform - pos: -16.5,19.5 - parent: 104 - - uid: 1213 - components: - - type: Transform - pos: -12.5,19.5 - parent: 104 - - uid: 1214 - components: - - type: Transform - pos: -16.5,24.5 - parent: 104 - - uid: 1215 - components: - - type: Transform - pos: -12.5,20.5 - parent: 104 - - uid: 1216 - components: - - type: Transform - pos: -15.5,24.5 - parent: 104 - - uid: 1218 - components: - - type: Transform - pos: -14.5,24.5 - parent: 104 - - uid: 1219 - components: - - type: Transform - pos: -12.5,21.5 - parent: 104 - - uid: 1220 - components: - - type: Transform - pos: -13.5,24.5 - parent: 104 - - uid: 1381 - components: - - type: Transform - pos: 16.5,-16.5 - parent: 104 - - uid: 1409 - components: - - type: Transform - pos: 9.5,-21.5 - parent: 104 - - uid: 1411 - components: - - type: Transform - pos: 9.5,-23.5 - parent: 104 - - uid: 1412 - components: - - type: Transform - pos: 15.5,-16.5 - parent: 104 - - uid: 1426 - components: - - type: Transform - pos: -3.5,-5.5 - parent: 104 - - uid: 1783 - components: - - type: Transform - pos: -3.5,-11.5 - parent: 104 - - uid: 1784 - components: - - type: Transform - pos: -3.5,-17.5 - parent: 104 - - uid: 1811 - components: - - type: Transform - pos: 15.5,-19.5 - parent: 104 - - uid: 1833 - components: - - type: Transform - pos: 15.5,-17.5 - parent: 104 - - uid: 1834 - components: - - type: Transform - pos: 15.5,-18.5 - parent: 104 - - uid: 1843 - components: - - type: Transform - pos: 15.5,-22.5 - parent: 104 - - uid: 1850 - components: - - type: Transform - pos: 15.5,-21.5 - parent: 104 - - uid: 1856 - components: - - type: Transform - pos: 15.5,-20.5 - parent: 104 - - uid: 1861 - components: - - type: Transform - pos: 26.5,-12.5 - parent: 104 - - uid: 1862 - components: - - type: Transform - pos: 27.5,-12.5 - parent: 104 - - uid: 1863 - components: - - type: Transform - pos: 28.5,-12.5 - parent: 104 - - uid: 1864 - components: - - type: Transform - pos: 28.5,-14.5 - parent: 104 - - uid: 1866 - components: - - type: Transform - pos: 28.5,-13.5 - parent: 104 - - uid: 1871 - components: - - type: Transform - pos: 28.5,-19.5 - parent: 104 - - uid: 1876 - components: - - type: Transform - pos: 28.5,-15.5 - parent: 104 - - uid: 1881 - components: - - type: Transform - pos: 28.5,-16.5 - parent: 104 - - uid: 1885 - components: - - type: Transform - pos: 28.5,-17.5 - parent: 104 - - uid: 1889 - components: - - type: Transform - pos: 28.5,-18.5 - parent: 104 - - uid: 1893 - components: - - type: Transform - pos: 28.5,-20.5 - parent: 104 - - uid: 1897 - components: - - type: Transform - pos: 27.5,-20.5 - parent: 104 - - uid: 1901 - components: - - type: Transform - pos: 26.5,-20.5 - parent: 104 - - uid: 1905 - components: - - type: Transform - pos: 18.5,-20.5 - parent: 104 - - uid: 1906 - components: - - type: Transform - pos: 17.5,-20.5 - parent: 104 - - uid: 1907 - components: - - type: Transform - pos: 16.5,-20.5 - parent: 104 - - uid: 1908 - components: - - type: Transform - pos: 16.5,-19.5 - parent: 104 - - uid: 1914 - components: - - type: Transform - pos: 25.5,-20.5 - parent: 104 - - uid: 1915 - components: - - type: Transform - pos: 16.5,-17.5 - parent: 104 - - uid: 1943 - components: - - type: Transform - pos: 24.5,-20.5 - parent: 104 - - uid: 1944 - components: - - type: Transform - pos: 16.5,-18.5 - parent: 104 - - uid: 1968 - components: - - type: Transform - pos: 23.5,-20.5 - parent: 104 - - uid: 1969 - components: - - type: Transform - pos: 22.5,-20.5 - parent: 104 - - uid: 1970 - components: - - type: Transform - pos: 21.5,-20.5 - parent: 104 - - uid: 1971 - components: - - type: Transform - pos: 20.5,-20.5 - parent: 104 - - uid: 1972 - components: - - type: Transform - pos: 19.5,-20.5 - parent: 104 - - uid: 2017 - components: - - type: Transform - pos: 2.5,-10.5 - parent: 104 - - uid: 2018 - components: - - type: Transform - pos: 2.5,-11.5 - parent: 104 - - uid: 2019 - components: - - type: Transform - pos: 2.5,-12.5 - parent: 104 - - uid: 2299 - components: - - type: Transform - pos: -1.5,-2.5 - parent: 104 - - uid: 2300 - components: - - type: Transform - pos: -1.5,-4.5 - parent: 104 -- proto: WallPlastitaniumDiagonal - entities: - - uid: 323 - components: - - type: Transform - pos: 18.5,-4.5 - parent: 104 - - uid: 324 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 22.5,-5.5 - parent: 104 - - uid: 325 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 23.5,-9.5 - parent: 104 - - uid: 338 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-9.5 - parent: 104 - - uid: 348 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 18.5,-9.5 - parent: 104 - - uid: 650 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 18.5,-10.5 - parent: 104 - - uid: 1088 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 18.5,-5.5 - parent: 104 - - uid: 1089 - components: - - type: Transform - pos: 17.5,-5.5 - parent: 104 - - uid: 1119 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 22.5,-10.5 - parent: 104 - - uid: 1689 - components: - - type: Transform - pos: 22.5,-9.5 - parent: 104 - - uid: 1690 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 23.5,-5.5 - parent: 104 - - uid: 1697 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 22.5,-4.5 - parent: 104 -- proto: WaterTankFull - entities: - - uid: 1547 - components: - - type: Transform - pos: 11.5,-4.5 - parent: 104 -- proto: WeaponPistolCobra - entities: - - uid: 1705 - components: - - type: Transform - pos: 5.4793262,12.166912 - parent: 104 -- proto: WeaponShotgunSawn - entities: - - uid: 228 - components: - - type: Transform - pos: 5.347948,13.528896 - parent: 104 - - type: BallisticAmmoProvider - unspawnedCount: 2 -- proto: WeaponSprayNozzle - entities: - - uid: 152 - components: - - type: Transform - pos: 9.67418,-4.571714 - parent: 104 -- proto: WeaponSubMachineGunC20r - entities: - - uid: 1704 - components: - - type: Transform - pos: 5.497257,12.817707 - parent: 104 -- proto: WelderIndustrial - entities: - - uid: 207 - components: - - type: Transform - pos: 14.5,-10.5 - parent: 104 -- proto: Windoor - entities: - - uid: 27 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 4.5,-7.5 - parent: 104 -- proto: WindoorSecure - entities: - - uid: 1145 - components: - - type: Transform - pos: -13.5,5.5 - parent: 104 - - uid: 1827 - components: - - type: Transform - pos: 10.5,-18.5 - parent: 104 -- proto: WindowReinforcedDirectional - entities: - - uid: 766 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-12.5 - parent: 104 - - uid: 1135 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-16.5 - parent: 104 - - uid: 1217 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 7.5,9.5 - parent: 104 - - uid: 1247 - components: - - type: Transform - pos: 4.5,-12.5 - parent: 104 - - uid: 1370 - components: - - type: Transform - pos: 3.5,-12.5 - parent: 104 - - uid: 2082 - components: - - type: Transform - pos: 7.5,15.5 - parent: 104 - - uid: 2182 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 5.5,-12.5 - parent: 104 - - uid: 2258 - components: - - type: Transform - pos: 5.5,-12.5 - parent: 104 - - uid: 2290 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,9.5 - parent: 104 - - uid: 2293 - components: - - type: Transform - pos: 5.5,15.5 - parent: 104 - - uid: 2294 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 5.5,-10.5 - parent: 104 -... +meta: + format: 6 + postmapinit: false +tilemap: + 0: Space + 14: FloorBar + 20: FloorCarpetClown + 29: FloorDark + 44: FloorFreezer + 59: FloorIce + 77: FloorReinforced + 79: FloorRockVault + 80: FloorShowroom + 84: FloorShuttleOrange + 86: FloorShuttleRed + 89: FloorSnow + 91: FloorSteel + 106: FloorTechMaint + 110: FloorWhite + 120: FloorWood + 123: Lattice + 124: Plating +entities: +- proto: "" + entities: + - uid: 104 + components: + - type: MetaData + name: Syndicate Outpost + - type: Transform + parent: 1295 + - type: FTLDestination + whitelist: + tags: + - Syndicate + - type: MapGrid + chunks: + -1,-1: + ind: -1,-1 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAKWQAAAAAAfAAAAAAAHQAAAAACbgAAAAADbgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABbgAAAAADWwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAAAbgAAAAACbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAAAHQAAAAADHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAADWQAAAAAEWQAAAAAAWQAAAAAEfAAAAAAAeAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAeAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAeAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAADgAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAADgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAIfAAAAAAADgAAAAAB + version: 6 + 0,-1: + ind: 0,-1 + tiles: bgAAAAAAbgAAAAADfAAAAAAAbgAAAAAAbgAAAAACbgAAAAACbgAAAAADbgAAAAABHQAAAAAAHQAAAAABHQAAAAABHQAAAAABHQAAAAABHQAAAAAAHQAAAAADHQAAAAACWwAAAAAAbgAAAAADbgAAAAADbgAAAAADbgAAAAACbgAAAAAAbgAAAAADbgAAAAADHQAAAAADVgAAAAAAHQAAAAACVgAAAAAAHQAAAAADVgAAAAAAHQAAAAADHQAAAAADbgAAAAAAbgAAAAACfAAAAAAAbgAAAAADbgAAAAACbgAAAAADbgAAAAACbgAAAAACHQAAAAADVgAAAAAAHQAAAAADVgAAAAAAHQAAAAABVgAAAAAAHQAAAAABHQAAAAAAHQAAAAABHQAAAAADHQAAAAAAWwAAAAADWwAAAAACWwAAAAADbgAAAAADbgAAAAACHQAAAAABHQAAAAAAHQAAAAAAHQAAAAABHQAAAAABHQAAAAABHQAAAAAAHQAAAAADfAAAAAAAfAAAAAAAfAAAAAAAWwAAAAABWwAAAAABWwAAAAACbgAAAAADbgAAAAACDgAAAAACHQAAAAADHQAAAAAAHQAAAAADHQAAAAADHQAAAAACHQAAAAAAHQAAAAADWQAAAAAAWQAAAAAAbgAAAAAAWwAAAAABWwAAAAAAWwAAAAAAbgAAAAACbgAAAAAAWQAAAAAAHQAAAAADHQAAAAADVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAAVAAAAAAADgAAAAABDgAAAAADDgAAAAACDgAAAAAADgAAAAADfAAAAAAAHQAAAAABfAAAAAAADgAAAAADDgAAAAACDgAAAAAADgAAAAACDgAAAAAAHQAAAAABHQAAAAADHQAAAAABVgAAAAAAVgAAAAAAeAAAAAACeAAAAAACeAAAAAADHQAAAAAAHQAAAAABHQAAAAABHQAAAAAAHQAAAAADHQAAAAACfAAAAAAAHQAAAAACHQAAAAACHQAAAAADHQAAAAABeAAAAAAAeAAAAAACeAAAAAACeAAAAAADeAAAAAADHQAAAAAAHQAAAAAAHQAAAAACHQAAAAAAHQAAAAACHQAAAAACHQAAAAABHQAAAAADHQAAAAABHQAAAAACHQAAAAABeAAAAAAAeAAAAAABeAAAAAAAeAAAAAADeAAAAAADHQAAAAAAHQAAAAAAHQAAAAADHQAAAAAAHQAAAAABHQAAAAADfAAAAAAAHQAAAAACHQAAAAABHQAAAAABHQAAAAACDgAAAAACDgAAAAAADgAAAAACDgAAAAADDgAAAAACDgAAAAABDgAAAAACDgAAAAAADgAAAAAADgAAAAACDgAAAAADDgAAAAAADgAAAAABHQAAAAAAHQAAAAACHQAAAAACDgAAAAAADgAAAAABDgAAAAACDgAAAAAADgAAAAABDgAAAAABDgAAAAADDgAAAAAADgAAAAAALAAAAAAALAAAAAAALAAAAAAADgAAAAACeAAAAAAAeAAAAAADeAAAAAABDgAAAAACDgAAAAACDgAAAAAADgAAAAAADgAAAAADDgAAAAAADgAAAAAAVgAAAAAADgAAAAACLAAAAAAALAAAAAAALAAAAAAADgAAAAADeAAAAAABeAAAAAABeAAAAAADDgAAAAADDgAAAAABDgAAAAADDgAAAAACDgAAAAADDgAAAAAADgAAAAAAVgAAAAAADgAAAAAAUAAAAAAALAAAAAAADgAAAAAADgAAAAACHQAAAAAAHQAAAAACfAAAAAAADgAAAAACDgAAAAACDgAAAAACDgAAAAAADgAAAAADDgAAAAABDgAAAAACVgAAAAAADgAAAAADUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAADgAAAAAADgAAAAADDgAAAAACDgAAAAACDgAAAAACDgAAAAABDgAAAAABDgAAAAAADgAAAAACUAAAAAAAUAAAAAAALAAAAAAAUAAAAAAAUAAAAAAAfAAAAAAAfAAAAAAA + version: 6 + 0,0: + ind: 0,0 + tiles: HQAAAAADHQAAAAACHQAAAAACDgAAAAAADgAAAAABDgAAAAABDgAAAAABDgAAAAACDgAAAAADUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAfAAAAAAAWQAAAAAGfAAAAAAAfAAAAAAAfAAAAAAAHQAAAAACfAAAAAAADgAAAAAADgAAAAABDgAAAAABDgAAAAACDgAAAAABDgAAAAAADgAAAAACDgAAAAACWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATwAAAAAAfAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAfAAAAAAATwAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAWQAAAAAIWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAIWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAKWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAGWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAMWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAACWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAJWQAAAAAAWQAAAAAFTwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAHWQAAAAAKWQAAAAAFWQAAAAAAWQAAAAALWQAAAAAA + version: 6 + -1,0: + ind: -1,0 + tiles: ewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAAWQAAAAAKWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAKWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAGHQAAAAADHQAAAAADHQAAAAAAfAAAAAAAHQAAAAAAHQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAIfAAAAAAAHQAAAAABHQAAAAACHQAAAAACHQAAAAADHQAAAAACHQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAfAAAAAAAHQAAAAABHQAAAAABWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAGTwAAAAAAHQAAAAABHQAAAAAAfAAAAAAAfAAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAADWQAAAAAATwAAAAAATwAAAAAATwAAAAAAfAAAAAAAfAAAAAAATwAAAAAATwAAAAAATwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAATwAAAAAATwAAAAAATwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKTwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAA + version: 6 + 1,-1: + ind: 1,-1 + tiles: HQAAAAADHQAAAAADHQAAAAACHQAAAAACHQAAAAAATQAAAAAATQAAAAAATQAAAAAAHQAAAAADHQAAAAACHQAAAAACHQAAAAACfAAAAAAAWQAAAAAAWQAAAAABWQAAAAACHQAAAAADHQAAAAADHQAAAAADHQAAAAAAHQAAAAADTQAAAAAATQAAAAAATQAAAAAAHQAAAAADHQAAAAACHQAAAAADHQAAAAACfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAHQAAAAABHQAAAAADHQAAAAAAHQAAAAABHQAAAAACHQAAAAAAHQAAAAADHQAAAAADHQAAAAAAHQAAAAADHQAAAAABHQAAAAACfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAHQAAAAADHQAAAAAAHQAAAAAAHQAAAAADHQAAAAADHQAAAAABHQAAAAACHQAAAAADHQAAAAADHQAAAAABfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAMWQAAAAAHWQAAAAAAHQAAAAABHQAAAAAAHQAAAAADHQAAAAABHQAAAAACHQAAAAAAHQAAAAADHQAAAAACHQAAAAACHQAAAAABWQAAAAAAWQAAAAADWQAAAAAAWQAAAAACWQAAAAAAAAAAAAAAHQAAAAAAHQAAAAABHQAAAAABHQAAAAADHQAAAAABHQAAAAABHQAAAAABHQAAAAADHQAAAAABHQAAAAACWQAAAAAFWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAHQAAAAADHQAAAAABVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAACHQAAAAACHQAAAAACWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAACAAAAAAAAfAAAAAAAHQAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAADHQAAAAADHQAAAAACWQAAAAAETwAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAAAAAAAAAHQAAAAAAHQAAAAABVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAACHQAAAAADHQAAAAACWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAACVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAAAHQAAAAADHQAAAAACWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAfAAAAAAAHQAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAVgAAAAAAHQAAAAAAHQAAAAABHQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABHQAAAAADHQAAAAAAHQAAAAAAHQAAAAABHQAAAAAAHQAAAAACHQAAAAABHQAAAAABWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABHQAAAAADHQAAAAACHQAAAAACHQAAAAADHQAAAAADHQAAAAACHQAAAAAAHQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAHQAAAAAAHQAAAAACHQAAAAACHQAAAAAAHQAAAAABHQAAAAADHQAAAAAAHQAAAAABWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAGfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAWQAAAAAATwAAAAAAWQAAAAAEWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAA + version: 6 + 0,-2: + ind: 0,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWwAAAAAAWwAAAAAAWwAAAAABWwAAAAAAWwAAAAACfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAbgAAAAAAbgAAAAADbgAAAAADbgAAAAADbgAAAAAAfAAAAAAAagAAAAAAWwAAAAACWwAAAAADWwAAAAABWwAAAAABWwAAAAADfAAAAAAAHQAAAAAAHQAAAAABWQAAAAAAbgAAAAABbgAAAAABbgAAAAADbgAAAAADbgAAAAAAagAAAAAAagAAAAAAWwAAAAADWwAAAAAAWwAAAAABWwAAAAABWwAAAAAAfAAAAAAA + version: 6 + -1,-2: + ind: -1,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAFWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAHQAAAAABHQAAAAAAHQAAAAAC + version: 6 + -1,1: + ind: -1,1 + tiles: WQAAAAAIWQAAAAAAWQAAAAALWQAAAAAAWQAAAAADWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAFWQAAAAADWQAAAAAEWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAEWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFTwAAAAAATwAAAAAATwAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAACWQAAAAAAWQAAAAADTwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAFAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 0,1: + ind: 0,1 + tiles: WQAAAAAAWQAAAAAEWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAJWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAABWQAAAAALWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAIWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAACWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAADWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAA + version: 6 + 1,1: + ind: 1,1 + tiles: WQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAGWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAABWQAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 1,0: + ind: 1,0 + tiles: WQAAAAAATwAAAAAATwAAAAAAWQAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAALWQAAAAAAWQAAAAAMWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAFWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAOwAAAAAAOwAAAAAAOwAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAHWQAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEWQAAAAAJWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAACWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAABWQAAAAAATwAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 1,-2: + ind: 1,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAABWQAAAAAAWQAAAAAAAAAAAAAAWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAATwAAAAAAWQAAAAAMWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAADWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAACWQAAAAAMWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAagAAAAAAHQAAAAACHQAAAAADHQAAAAACfAAAAAAAfAAAAAAAfAAAAAAAHQAAAAAAHQAAAAACHQAAAAABagAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJfAAAAAAAagAAAAAAHQAAAAADHQAAAAACHQAAAAADHQAAAAACHQAAAAACHQAAAAADHQAAAAABHQAAAAABHQAAAAABagAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAagAAAAAAHQAAAAAAHQAAAAABHQAAAAAAfAAAAAAAfAAAAAAAfAAAAAAAHQAAAAACHQAAAAADHQAAAAACagAAAAAAfAAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAEHQAAAAACHQAAAAACHQAAAAABHQAAAAABTQAAAAAATQAAAAAATQAAAAAAHQAAAAABHQAAAAADHQAAAAACHQAAAAAAfAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAD + version: 6 + -2,0: + ind: -2,0 + tiles: ewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAAWQAAAAABTwAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAATwAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAETwAAAAAATwAAAAAATwAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAALWQAAAAAAWQAAAAALWQAAAAABWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAALWQAAAAAAWQAAAAAAWQAAAAABWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAA + version: 6 + -2,1: + ind: -2,1 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAATwAAAAAAWQAAAAAKFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAJWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAHWQAAAAAKWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAGWQAAAAAJWQAAAAALWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAALWQAAAAAIWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAGFAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAADWQAAAAAEWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEWQAAAAAAWQAAAAAMWQAAAAACWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAEWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAIWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAFWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAA + version: 6 + -2,-2: + ind: -2,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 2,0: + ind: 2,0 + tiles: TwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 2,-1: + ind: 2,-1 + tiles: TwAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 2,-2: + ind: 2,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAIWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATwAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + -2,2: + ind: -2,2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + -3,-2: + ind: -3,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 0,2: + ind: 0,2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAFWQAAAAAEWQAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 1,2: + ind: 1,2 + tiles: WQAAAAAAWQAAAAAFWQAAAAACWQAAAAAJWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + -3,0: + ind: -3,0 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAewAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + - type: Broadphase + - type: Physics + bodyStatus: InAir + angularDamping: 0.05 + linearDamping: 0.05 + fixedRotation: False + bodyType: Dynamic + - type: Fixtures + fixtures: {} + - type: Gravity + gravityShakeSound: !type:SoundPathSpecifier + path: /Audio/Effects/alert.ogg + - type: DecalGrid + chunkCollection: + version: 2 + nodes: + - node: + angle: -3.141592653589793 rad + color: '#FFFFFFFF' + id: Arrows + decals: + 311: 27,-15 + - node: + color: '#FFFFFFFF' + id: Arrows + decals: + 309: 18,-19 + 310: 26,-19 + - node: + color: '#FFFFFFFF' + id: Basalt1 + decals: + 368: 26.834131,4.97881 + - node: + color: '#FFFFFFFF' + id: Bot + decals: + 295: 21,-16 + 296: 22,-15 + 297: 23,-16 + 302: 22,-17 + 303: 17,-19 + 304: 27,-19 + - node: + color: '#FFFFFFFF' + id: BotLeft + decals: + 194: 5,-11 + 195: 4,-11 + 300: 23,-15 + 301: 21,-17 + 305: 27,-18 + 306: 17,-20 + - node: + color: '#FFFFFFFF' + id: BotRight + decals: + 298: 23,-17 + 299: 21,-15 + 307: 27,-20 + 308: 17,-18 + - node: + color: '#FFFFFFFF' + id: Box + decals: + 206: 12,-22 + - node: + color: '#FFFFFFFF' + id: BrickTileDarkLineN + decals: + 326: 2,-1 + 327: 1,-1 + 328: 0,-1 + - node: + color: '#52B4E996' + id: BrickTileSteelBox + decals: + 238: -3,-15 + 239: -2,-17 + 240: 1,-17 + 241: 1,-13 + 242: -2,-13 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerNe + decals: + 143: 15,-12 + 149: 10,-11 + 186: 5,-11 + 332: 10,-7 + 339: 15,-7 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerNw + decals: + 150: 9,-11 + 187: 3,-11 + 340: 12,-7 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerSe + decals: + 141: 15,-15 + 188: 5,-13 + 259: 27,-17 + 264: 26,-20 + 329: 10,-9 + 342: 15,-9 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerSw + decals: + 127: 14,-15 + 138: 9,-13 + 189: 3,-13 + 256: 17,-17 + 263: 18,-20 + 335: 5,-9 + 341: 12,-9 + - node: + color: '#DE3A3A96' + id: BrickTileSteelEndS + decals: + 125: 10,-15 + 126: 12,-15 + - node: + color: '#DE3A3A96' + id: BrickTileSteelInnerNe + decals: + 148: 10,-12 + 250: -3,-17 + - node: + color: '#DE3A3A96' + id: BrickTileSteelInnerSe + decals: + 139: 10,-13 + 140: 12,-13 + 249: -3,-13 + 260: 26,-17 + 289: 20,-14 + - node: + color: '#DE3A3A96' + id: BrickTileSteelInnerSw + decals: + 133: 10,-13 + 134: 12,-13 + 135: 14,-13 + 261: 18,-17 + 285: 24,-14 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineE + decals: + 128: 12,-14 + 129: 10,-14 + 142: 15,-13 + 191: 5,-12 + 247: -3,-16 + 248: -3,-14 + 257: 26,-19 + 258: 26,-18 + 265: 20,-17 + 266: 20,-16 + 267: 20,-15 + 290: 20,-19 + 312: 27,-16 + 313: 27,-15 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineN + decals: + 144: 14,-12 + 145: 13,-12 + 146: 12,-12 + 147: 11,-12 + 192: 4,-11 + 243: 0,-17 + 244: -1,-17 + 274: 26,-14 + 275: 25,-14 + 276: 23,-14 + 277: 22,-14 + 278: 21,-14 + 279: 20,-14 + 280: 18,-14 + 281: 19,-14 + 284: 24,-14 + 333: 9,-7 + 334: 8,-7 + 345: 13,-7 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineS + decals: + 136: 13,-13 + 137: 11,-13 + 190: 4,-13 + 245: 0,-13 + 246: -1,-13 + 272: 25,-20 + 273: 19,-20 + 286: 23,-14 + 287: 22,-14 + 288: 21,-14 + 330: 8,-9 + 331: 7,-9 + 343: 14,-9 + 344: 13,-9 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineW + decals: + 130: 14,-14 + 131: 12,-14 + 132: 10,-14 + 151: 9,-12 + 193: 3,-12 + 253: 18,-18 + 254: 17,-16 + 255: 17,-15 + 262: 18,-19 + 268: 24,-19 + 269: 24,-17 + 270: 24,-16 + 271: 24,-15 + 336: 5,-8 + 359: 5,-7 + - node: + color: '#9FED5896' + id: BrickTileWhiteCornerNe + decals: + 159: 7,-11 + - node: + color: '#9FED5896' + id: BrickTileWhiteCornerNw + decals: + 160: 6,-11 + 178: 3,-14 + - node: + color: '#9FED5896' + id: BrickTileWhiteCornerSe + decals: + 171: 7,-18 + - node: + color: '#9FED5896' + id: BrickTileWhiteCornerSw + decals: + 174: 3,-18 + - node: + color: '#9FED5896' + id: BrickTileWhiteInnerNw + decals: + 162: 6,-14 + - node: + color: '#9FED5896' + id: BrickTileWhiteLineE + decals: + 165: 7,-12 + 166: 7,-13 + 167: 7,-14 + 168: 7,-15 + 169: 7,-16 + 170: 7,-17 + - node: + color: '#9FED5896' + id: BrickTileWhiteLineN + decals: + 163: 5,-14 + 164: 4,-14 + - node: + color: '#9FED5896' + id: BrickTileWhiteLineS + decals: + 172: 6,-18 + 173: 5,-18 + 175: 4,-18 + - node: + color: '#9FED5896' + id: BrickTileWhiteLineW + decals: + 161: 6,-13 + 176: 3,-17 + 177: 3,-16 + - node: + color: '#A4610647' + id: CheckerNWSE + decals: + 196: -13,6 + 197: -14,6 + 198: -15,6 + 199: -15,7 + 200: -14,7 + 201: -13,7 + 202: -14,8 + 203: -15,8 + - node: + color: '#D381C996' + id: CheckerNWSE + decals: + 207: 10,-19 + 208: 11,-19 + 209: 12,-19 + 210: 13,-19 + 211: 14,-19 + 212: 14,-18 + 213: 14,-17 + 214: 13,-17 + 215: 13,-18 + 216: 12,-18 + 217: 12,-17 + 218: 11,-17 + 219: 11,-18 + 220: 10,-18 + 221: 10,-17 + - node: + color: '#FFFFFFFF' + id: Delivery + decals: + 204: -16,8 + 205: -16,9 + 251: 7,-18 + 252: 6,-18 + - node: + color: '#DE3A3A96' + id: DeliveryGreyscale + decals: + 283: 16,-14 + - node: + color: '#9FED5896' + id: FullTileOverlayGreyscale + decals: + 181: 5,-17 + 182: 5,-16 + 183: 5,-15 + 184: 6,-16 + 185: 4,-16 + - node: + color: '#52B4E996' + id: HalfTileOverlayGreyscale + decals: + 232: 1,-14 + 233: 0,-14 + 234: -1,-14 + - node: + color: '#9FED5896' + id: HalfTileOverlayGreyscale + decals: + 324: 11,-2 + - node: + color: '#52B4E996' + id: HalfTileOverlayGreyscale180 + decals: + 229: 1,-16 + 230: 0,-16 + 231: -1,-16 + - node: + color: '#9FED5896' + id: HalfTileOverlayGreyscale180 + decals: + 325: 11,0 + - node: + color: '#52B4E996' + id: HalfTileOverlayGreyscale270 + decals: + 235: -2,-15 + - node: + color: '#9FED5896' + id: HalfTileOverlayGreyscale270 + decals: + 323: 12,-1 + - node: + color: '#9FED5896' + id: HalfTileOverlayGreyscale90 + decals: + 322: 10,-1 + - node: + color: '#9FED5896' + id: QuarterTileOverlayGreyscale + decals: + 314: 9,0 + 315: 10,0 + - node: + color: '#9FED5896' + id: QuarterTileOverlayGreyscale180 + decals: + 318: 13,-2 + 319: 12,-2 + - node: + color: '#9FED5896' + id: QuarterTileOverlayGreyscale270 + decals: + 320: 9,-2 + 321: 10,-2 + - node: + color: '#9FED5896' + id: QuarterTileOverlayGreyscale90 + decals: + 316: 13,0 + 317: 12,0 + - node: + color: '#FFFFFFFF' + id: Rock06 + decals: + 75: 9.232971,15.9332485 + - node: + color: '#52B4E996' + id: ThreeQuarterTileOverlayGreyscale + decals: + 237: -2,-14 + - node: + color: '#52B4E996' + id: ThreeQuarterTileOverlayGreyscale270 + decals: + 236: -2,-16 + - node: + color: '#FFFFFFFF' + id: WarnBox + decals: + 124: -5,-3 + - node: + angle: -1.5707963267948966 rad + color: '#FFFFFFFF' + id: WarnCorner + decals: + 21: 18,-5 + - node: + color: '#FFFFFFFF' + id: WarnCorner + decals: + 0: 17,-10 + 1: 18,-11 + 2: 19,-11 + - node: + angle: 4.71238898038469 rad + color: '#FFFFFFFF' + id: WarnCorner + decals: + 14: 17,-6 + - node: + color: '#FFFFFFFF' + id: WarnCornerFlipped + decals: + 7: 23,-10 + 13: 16,-6 + 19: 22,-11 + - node: + angle: 1.5707963267948966 rad + color: '#FFFFFFFF' + id: WarnCornerFlipped + decals: + 11: 23,-6 + 12: 22,-5 + - node: + color: '#DE3A3A96' + id: WarnCornerGreyscaleNW + decals: + 282: 17,-14 + - node: + color: '#FFFFFFFF' + id: WarnCornerSE + decals: + 292: 20,-20 + - node: + color: '#FFFFFFFF' + id: WarnCornerSW + decals: + 226: 10,-19 + 291: 24,-20 + - node: + color: '#FFFFFFFF' + id: WarnEndE + decals: + 154: 1,-9 + - node: + color: '#FFFFFFFF' + id: WarnEndN + decals: + 156: 7,-2 + - node: + color: '#FFFFFFFF' + id: WarnEndS + decals: + 123: -6,21 + 155: 7,-4 + - node: + color: '#FFFFFFFF' + id: WarnEndW + decals: + 153: 0,-9 + - node: + color: '#DE3A3A96' + id: WarnFullGreyscale + decals: + 352: 16,-8 + 353: 11,-8 + 354: 9,-10 + 355: 6,-10 + - node: + color: '#FFFFFFFF' + id: WarnLineE + decals: + 158: 7,-3 + 294: 20,-18 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleE + decals: + 152: 15,-14 + 349: 10,-8 + 350: 15,-8 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleN + decals: + 351: 14,-7 + 356: 7,-7 + 357: 6,-7 + 358: 5,-7 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleS + decals: + 346: 9,-9 + 347: 6,-9 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleW + decals: + 348: 12,-8 + - node: + color: '#FFFFFFFF' + id: WarnLineGreyscaleW + decals: + 179: 3,-15 + 180: 6,-12 + - node: + color: '#FFFFFFFF' + id: WarnLineN + decals: + 222: 14,-19 + 223: 13,-19 + 224: 12,-19 + 225: 11,-19 + - node: + color: '#FFFFFFFF' + id: WarnLineS + decals: + 94: -9,4 + 95: -9,3 + 96: -9,2 + 97: -9,1 + 98: -9,0 + 99: -9,-1 + 100: -9,-2 + 101: -9,-3 + 102: -9,-4 + 103: -9,-5 + 104: -9,-6 + 105: -9,-7 + 106: -9,-8 + 107: -9,-9 + 108: -9,-10 + 109: -9,-11 + 110: -9,-12 + 111: -9,-13 + 112: -9,-14 + 113: -9,-15 + 114: -9,-16 + 115: -9,-17 + 116: -9,-18 + 117: -9,-19 + 118: -9,-20 + 119: -9,-21 + 120: -9,-22 + 121: -9,-23 + 122: -9,-24 + 157: 7,-3 + 227: 10,-18 + 228: 10,-17 + 293: 24,-18 + - node: + color: '#FFFFFFFF' + id: WarningLine + decals: + 15: 19,-4 + 16: 20,-4 + 17: 21,-4 + - node: + angle: 3.141592653589793 rad + color: '#FFFFFFFF' + id: WarningLine + decals: + 3: 19,-11 + 4: 19,-12 + 5: 20,-12 + 6: 21,-12 + - node: + angle: 4.71238898038469 rad + color: '#FFFFFFFF' + id: WarningLine + decals: + 8: 24,-9 + 9: 24,-8 + 10: 24,-7 + - node: + color: '#FFFFFFFF' + id: WarningLineCornerFlipped + decals: + 20: 22,-4 + - node: + angle: 4.71238898038469 rad + color: '#FFFFFFFF' + id: WarningLineCornerFlipped + decals: + 18: 24,-10 + - node: + color: '#FFFFFFFF' + id: WoodTrimThinInnerSw + decals: + 363: 5,-6 + - node: + color: '#FFFFFFFF' + id: WoodTrimThinLineS + decals: + 361: 0,-6 + 362: -1,-6 + 364: 4,-6 + 365: 3,-6 + 366: 2,-6 + 367: 1,-6 + - node: + color: '#FFFFFFFF' + id: WoodTrimThinLineW + decals: + 337: 5,-9 + 338: 5,-8 + 360: 5,-7 + - node: + color: '#FFFFFFFF' + id: bushsnowa1 + decals: + 67: 12.033197,7.701807 + 69: 14.310135,6.6772776 + - node: + color: '#FFFFFFFF' + id: bushsnowa2 + decals: + 68: -1.884201,10.692345 + 70: 13.896899,12.679136 + 73: -2.6609726,10.251652 + - node: + color: '#FFFFFFFF' + id: bushsnowb2 + decals: + 71: 12.943774,14.853275 + - node: + color: '#FFFFFFFF' + id: bushsnowb3 + decals: + 72: 0.2452774,15.588417 + 74: -0.9488945,8.937782 + - node: + color: '#AB0000FF' + id: d + decals: + 373: 21.412256,2.9944348 + - node: + color: '#FFFFFFFF' + id: grasssnowa1 + decals: + 30: 9.251883,16.971285 + 41: 4.707512,15.727119 + 47: -2.8426914,5.7681084 + 84: 16.072964,17.452217 + 85: -4.644718,19.141476 + 369: 26.896631,2.9944348 + - node: + color: '#FFFFFFFF' + id: grasssnowa2 + decals: + 22: -3.6290493,-9.771207 + 25: 15.159365,5.392831 + 42: 2.707512,15.930454 + 53: 1.6412125,14.139866 + 62: 14.449558,12.269236 + 86: -6.332218,17.702477 + 87: -7.488468,14.82448 + 91: -5.738468,8.348984 + - node: + color: '#FFFFFFFF' + id: grasssnowa3 + decals: + 44: -1.3739414,16.399694 + 54: 0.43808746,13.654987 + 57: 3.1880875,12.434965 + 58: 2.1568375,9.893942 + 63: 16.060982,2.673603 + 90: -6.207218,16.0445 + - node: + color: '#FFFFFFFF' + id: grasssnowb1 + decals: + 51: 0.79449654,2.152192 + 55: 0.18808746,9.666458 + 79: 4.997473,19.080475 + - node: + color: '#FFFFFFFF' + id: grasssnowb2 + decals: + 36: 6.1287537,4.4684677 + 38: 13.017959,2.0284252 + 61: 8.19574,16.289047 + 64: 3.43816,14.798347 + 78: 2.4505978,18.17328 + 92: -5.832218,13.979851 + 93: 4.1790752,7.8279076 + - node: + color: '#FFFFFFFF' + id: grasssnowb3 + decals: + 23: -3.3477993,-19.991972 + 31: 9.061624,11.550716 + 35: 9.066255,4.2786684 + 50: -3.1864414,-0.9828911 + 52: -0.88188744,15.563225 + 65: 0.5527668,14.892197 + 83: 17.104214,9.229881 + - node: + color: '#FFFFFFFF' + id: grasssnowc1 + decals: + 24: -3.9999733,-6.081314 + 26: 15.819311,9.398571 + 32: 12.170999,10.565315 + 39: 10.861709,2.2630453 + 40: 8.471084,13.624069 + 45: -2.1864414,7.911888 + 48: -0.6551914,1.8769035 + 49: -3.7333164,0.34661865 + 56: 4.1724625,13.733192 + 76: 2.5912228,19.753052 + 82: 17.947964,3.1297789 + 88: -7.175968,10.382353 + - node: + color: '#FFFFFFFF' + id: grasssnowc2 + decals: + 27: 14.194311,14.208315 + 29: 12.819311,16.642818 + 33: 8.01938,8.972565 + 81: 19.322964,7.2277966 + 89: -5.675968,10.1320915 + - node: + color: '#FFFFFFFF' + id: grasssnowc3 + decals: + 28: 15.694311,15.172535 + 34: 10.48813,7.3443623 + 37: 9.0946245,2.0440664 + 43: -2.8895664,16.211998 + 46: -4.0458164,7.6303444 + 59: 0.45371246,11.270374 + 60: 9.166802,10.222887 + 66: -1.9470882,14.409473 + 77: 6.122473,18.001226 + 80: 18.166714,4.631343 + - node: + color: '#AB0000FF' + id: i + decals: + 374: 21.677881,2.8381848 + - node: + color: '#AB0000FF' + id: k + decals: + 377: 23.427881,2.8069348 + - node: + color: '#AB0000FF' + id: n + decals: + 372: 20.943506,2.9006848 + 378: 22.959131,2.9631848 + - node: + color: '#AB0000FF' + id: r + decals: + 375: 22.099756,2.7913098 + - node: + color: '#AB0000FF' + id: s + decals: + 370: 20.224756,2.9788098 + - node: + color: '#AB0000FF' + id: shortline + decals: + 376: 22.552881,2.8381848 + - node: + color: '#AB0000FF' + id: y + decals: + 371: 20.568506,2.9319348 + - type: GridAtmosphere + version: 2 + data: + tiles: + -1,-2: + 0: 65535 + -1,-1: + 0: 65503 + 1: 32 + 0,-1: + 0: 65535 + -2,-4: + 0: 65535 + -2,-3: + 0: 65535 + -2,-2: + 0: 65535 + -2,-1: + 0: 65535 + -1,-4: + 0: 65535 + -1,-3: + 0: 65535 + 0,-4: + 0: 65535 + 0,-3: + 0: 65535 + 0,-2: + 0: 65535 + 1,-4: + 0: 57343 + 1: 8192 + 1,-3: + 0: 65535 + 1,-2: + 0: 65535 + 1,-1: + 0: 65535 + 2,-4: + 0: 62815 + 1: 2720 + 2,-3: + 0: 65535 + 2,-2: + 0: 32767 + 1: 32768 + 2,-1: + 0: 65533 + 1: 2 + 3,-4: + 0: 64991 + 1: 544 + 3,-3: + 0: 65535 + 3,-2: + 0: 57343 + 1: 8192 + 3,-1: + 0: 65535 + 0,0: + 0: 65535 + 0,1: + 0: 65535 + 0,2: + 0: 65535 + 0,3: + 0: 65535 + 1,0: + 0: 65535 + 1,1: + 0: 65535 + 1,2: + 0: 65535 + 1,3: + 0: 65535 + 2,0: + 0: 65535 + 2,1: + 0: 65535 + 2,2: + 0: 65535 + 2,3: + 0: 65535 + 3,0: + 0: 65535 + 3,1: + 0: 65535 + 3,2: + 0: 65535 + 3,3: + 0: 65535 + -2,0: + 0: 65535 + -2,1: + 0: 65535 + -2,2: + 0: 65535 + -2,3: + 0: 65535 + -1,0: + 0: 65535 + -1,1: + 0: 65535 + -1,2: + 0: 65535 + -1,3: + 0: 65535 + 4,-4: + 0: 65535 + 4,-3: + 0: 65535 + 4,-2: + 0: 65535 + 4,-1: + 0: 65535 + 5,-4: + 0: 65519 + 1: 16 + 5,-3: + 0: 65535 + 5,-2: + 0: 65535 + 5,-1: + 0: 65535 + 6,-4: + 0: 65519 + 1: 16 + 6,-3: + 0: 65535 + 6,-2: + 0: 65535 + 6,-1: + 0: 65535 + 7,-4: + 0: 65535 + 7,-3: + 0: 65535 + 7,-2: + 0: 65535 + 7,-1: + 0: 65535 + 0,-6: + 0: 65535 + 0,-5: + 0: 65535 + 1,-6: + 0: 65535 + 1,-5: + 0: 62463 + 1: 3072 + 2,-6: + 0: 65535 + 2,-5: + 0: 65535 + 3,-6: + 0: 65535 + 3,-5: + 0: 65535 + -2,-6: + 0: 65535 + -2,-5: + 0: 65535 + -1,-6: + 0: 65535 + -1,-5: + 0: 65535 + -2,4: + 0: 65535 + -1,4: + 0: 65535 + 0,4: + 0: 65535 + 1,4: + 0: 65535 + 2,4: + 0: 65535 + 3,4: + 0: 65535 + 4,4: + 0: 65535 + 4,0: + 0: 65535 + 4,1: + 0: 65535 + 4,2: + 0: 65535 + 4,3: + 0: 65535 + 5,0: + 0: 65535 + 6,0: + 0: 65535 + 7,0: + 0: 65535 + 4,-6: + 0: 65535 + 4,-5: + 0: 65535 + 5,-6: + 0: 65535 + 5,-5: + 0: 65535 + 6,-6: + 0: 65535 + 6,-5: + 0: 65535 + 7,-6: + 0: 65535 + 7,-5: + 0: 65535 + -4,-4: + 0: 65535 + -4,-3: + 0: 4095 + -3,-4: + 0: 65535 + -3,-3: + 0: 61439 + -3,-2: + 0: 61166 + -3,-1: + 0: 61166 + -4,0: + 0: 65520 + 2: 15 + -4,1: + 0: 65535 + -4,2: + 0: 65535 + -4,3: + 0: 65535 + -3,0: + 0: 65534 + 2: 1 + -3,1: + 0: 65535 + -3,2: + 0: 65535 + -3,3: + 0: 65535 + 0,-7: + 0: 65535 + 1,-7: + 0: 61441 + 2: 3840 + 2,-7: + 0: 4096 + 2: 57600 + 3,-7: + 0: 32768 + 2: 28672 + -4,-7: + 0: 65535 + -4,-6: + 0: 65535 + -4,-5: + 0: 65535 + -3,-7: + 0: 65535 + -3,-6: + 0: 65535 + -3,-5: + 0: 65535 + -1,-7: + 0: 65535 + -4,4: + 0: 65535 + -4,5: + 0: 65535 + -3,4: + 0: 65535 + -3,5: + 0: 65535 + -2,5: + 0: 65535 + -2,6: + 0: 65535 + -1,5: + 0: 65535 + -1,6: + 0: 13107 + 0,5: + 0: 8191 + 1,5: + 0: 4095 + 2,5: + 0: 65535 + 3,5: + 0: 65535 + 4,5: + 0: 65535 + 5,4: + 0: 65535 + 5,5: + 0: 30719 + 6,4: + 0: 4915 + 5,1: + 0: 65535 + 5,2: + 0: 65535 + 5,3: + 0: 65535 + 6,1: + 0: 65535 + 6,2: + 0: 30719 + 6,3: + 0: 13107 + 7,1: + 0: 16383 + 7,2: + 0: 51 + 4,-7: + 0: 61440 + 5,-7: + 0: 61440 + 6,-7: + 0: 61440 + 7,-7: + 0: 28672 + -8,-4: + 0: 12 + -7,-4: + 0: 32783 + -6,-4: + 0: 61679 + -5,-4: + 0: 65535 + -5,-3: + 0: 4095 + -6,3: + 0: 65534 + -6,2: + 0: 52428 + -5,0: + 0: 65520 + 2: 15 + -5,1: + 0: 65535 + -5,2: + 0: 65535 + -5,3: + 0: 65535 + -7,7: + 0: 52424 + -7,5: + 0: 34944 + -7,6: + 0: 34952 + -6,4: + 0: 61183 + -6,5: + 0: 65535 + -6,6: + 0: 65535 + -6,7: + 0: 65535 + -5,4: + 0: 65535 + -5,5: + 0: 65535 + -8,-6: + 0: 65535 + -8,-5: + 0: 57343 + -7,-5: + 0: 65535 + -7,-6: + 0: 65535 + -6,-6: + 0: 65535 + -6,-5: + 0: 65535 + -6,-7: + 0: 64704 + -5,-7: + 0: 65528 + -5,-6: + 0: 65535 + -5,-5: + 0: 65535 + 8,0: + 0: 17 + 8,-4: + 0: 13107 + 8,-1: + 0: 4369 + 8,-6: + 0: 13072 + 8,-5: + 0: 13107 + -7,8: + 0: 12 + -6,8: + 0: 3 + -12,-6: + 0: 65535 + -12,-5: + 0: 65535 + -11,-6: + 0: 65533 + -11,-5: + 0: 65535 + -10,-6: + 0: 65535 + -10,-5: + 0: 65535 + -9,-6: + 0: 65535 + -9,-5: + 0: 65535 + -15,-4: + 0: 65535 + -15,-3: + 0: 65535 + -15,-2: + 0: 65535 + -15,-1: + 0: 65535 + -14,-4: + 0: 65535 + -14,-3: + 0: 29495 + -14,-2: + 0: 30583 + -14,-1: + 0: 13175 + -13,-4: + 0: 4607 + -12,-4: + 0: 255 + -11,-4: + 0: 255 + -10,-4: + 0: 255 + -9,-4: + 0: 119 + -15,-5: + 0: 65535 + -14,-5: + 0: 65535 + -14,-6: + 0: 65535 + -13,-6: + 0: 65535 + -13,-5: + 0: 65535 + -15,0: + 0: 65518 + -15,1: + 0: 65535 + -15,2: + 0: 65535 + -15,3: + 0: 65535 + -14,0: + 0: 13171 + -14,1: + 0: 13075 + -14,2: + 0: 13107 + -14,3: + 0: 29491 + -15,4: + 0: 65535 + -15,5: + 0: 65535 + -15,6: + 0: 35007 + -14,4: + 0: 65535 + -14,5: + 0: 65535 + -14,6: + 0: 16383 + -2,-7: + 0: 65535 + -4,6: + 0: 65535 + -4,7: + 0: 4095 + -3,6: + 0: 65535 + -3,7: + 0: 511 + -2,7: + 0: 255 + -1,7: + 0: 51 + 2,6: + 0: 52428 + 2,7: + 0: 52428 + 3,6: + 0: 65535 + 3,7: + 0: 65535 + 4,6: + 0: 65535 + 4,7: + 0: 65535 + 5,6: + 0: 13107 + 5,7: + 0: 4915 + -7,-3: + 0: 2184 + -6,-3: + 0: 4095 + -7,0: + 0: 65520 + 2: 15 + -7,1: + 0: 15 + -6,0: + 0: 65520 + 2: 15 + -6,1: + 0: 15 + 2: 51328 + -5,6: + 0: 65535 + -5,7: + 0: 32767 + -8,-7: + 0: 63744 + -7,-7: + 0: 65407 + -12,-7: + 0: 65519 + -11,-7: + 0: 4983 + -10,-7: + 0: 60622 + -9,-7: + 0: 65535 + -16,-4: + 0: 65535 + -16,-3: + 0: 65535 + -16,-2: + 0: 65535 + -16,-1: + 0: 65535 + -16,-7: + 0: 13107 + -16,-6: + 0: 65535 + -16,-5: + 0: 65535 + -15,-6: + 0: 65535 + -15,-7: + 0: 52360 + -14,-7: + 0: 65535 + -13,-7: + 0: 63251 + -16,0: + 0: 65535 + -16,1: + 0: 65535 + -16,2: + 0: 65535 + -16,3: + 0: 65535 + -16,4: + 0: 65535 + -16,5: + 0: 65535 + -16,6: + 0: 255 + -18,0: + 0: 65535 + -18,3: + 0: 61440 + -18,1: + 0: 34956 + -17,0: + 0: 65535 + -17,1: + 0: 65535 + -17,2: + 0: 65519 + -17,3: + 0: 65535 + -18,4: + 0: 52991 + -18,5: + 0: 65532 + -18,6: + 0: 3823 + -17,4: + 0: 65535 + -17,5: + 0: 65535 + -17,6: + 0: 4095 + -18,-4: + 0: 65535 + -18,-3: + 0: 52479 + -18,-1: + 0: 65484 + -18,-2: + 0: 52428 + -17,-4: + 0: 65535 + -17,-3: + 0: 65535 + -17,-2: + 0: 65535 + -17,-1: + 0: 65535 + -18,-6: + 0: 65484 + -18,-5: + 0: 65535 + -18,-7: + 0: 52224 + -17,-7: + 0: 65484 + -17,-6: + 0: 65535 + -17,-5: + 0: 65535 + 2,8: + 0: 12 + 3,8: + 0: 15 + 4,8: + 0: 15 + 5,8: + 0: 1 + -8,0: + 0: 65520 + 2: 15 + -8,1: + 0: 15 + -9,0: + 0: 61152 + 2: 14 + -9,1: + 0: 14 + uniqueMixes: + - volume: 2500 + temperature: 293.15 + moles: + - 21.824879 + - 82.10312 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.15 + moles: + - 21.213781 + - 79.80423 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.15 + moles: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + chunkSize: 4 + - type: RadiationGridResistance + - type: OccluderTree + - type: Shuttle + - type: GravityShake + shakeTimes: 10 + - type: GasTileOverlay + - type: SpreaderGrid + - type: GridPathfinding + - type: BecomesStation + id: SyndicateOutpost + - uid: 1295 + components: + - type: MetaData + - type: Transform + - type: Map + - type: PhysicsMap + - type: Broadphase + - type: OccluderTree + - type: Parallax + parallax: Sky + - type: MapAtmosphere + space: False + mixture: + volume: 2500 + temperature: 248.15 + moles: + - 21.824879 + - 82.10312 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - type: LoadedMap + - type: GridTree + - type: MovedGrids +- proto: AirlockExternalGlassNukeopLocked + entities: + - uid: 49 + components: + - type: Transform + pos: 3.5,1.5 + parent: 104 + - uid: 55 + components: + - type: Transform + pos: -1.5,-3.5 + parent: 104 + - uid: 56 + components: + - type: Transform + pos: -5.5,-3.5 + parent: 104 + - uid: 58 + components: + - type: Transform + pos: 3.5,3.5 + parent: 104 +- proto: AirlockGlassShuttle + entities: + - uid: 2053 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -33.5,-19.5 + parent: 104 + - uid: 2057 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -29.5,-19.5 + parent: 104 + - uid: 2062 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -25.5,-19.5 + parent: 104 + - uid: 2185 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -21.5,-19.5 + parent: 104 + - uid: 2186 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -17.5,-19.5 + parent: 104 + - uid: 2187 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -13.5,-19.5 + parent: 104 + - uid: 2190 + components: + - type: Transform + pos: -13.5,0.5 + parent: 104 + - uid: 2191 + components: + - type: Transform + pos: -17.5,0.5 + parent: 104 + - uid: 2193 + components: + - type: Transform + pos: -21.5,0.5 + parent: 104 + - uid: 2448 + components: + - type: Transform + pos: -25.5,0.5 + parent: 104 + - uid: 2472 + components: + - type: Transform + pos: -29.5,0.5 + parent: 104 + - type: GridFill + addComponents: + - type: NukeOpsShuttle + path: /Maps/Shuttles/infiltrator.yml + - uid: 2481 + components: + - type: Transform + pos: -33.5,0.5 + parent: 104 +- proto: AirlockSyndicateNukeopGlassLocked + entities: + - uid: 48 + components: + - type: Transform + pos: -11.5,6.5 + parent: 104 + - uid: 65 + components: + - type: Transform + pos: 2.5,-14.5 + parent: 104 + - uid: 96 + components: + - type: Transform + pos: 8.5,-16.5 + parent: 104 + - uid: 112 + components: + - type: Transform + pos: 16.5,-7.5 + parent: 104 + - uid: 122 + components: + - type: Transform + pos: 9.5,-9.5 + parent: 104 + - uid: 130 + components: + - type: Transform + pos: 11.5,-7.5 + parent: 104 + - uid: 131 + components: + - type: Transform + pos: 8.5,-0.5 + parent: 104 + - uid: 165 + components: + - type: Transform + pos: 10.5,-2.5 + parent: 104 + - uid: 190 + components: + - type: Transform + pos: 16.5,-13.5 + parent: 104 + - uid: 302 + components: + - type: Transform + pos: 14.5,-5.5 + parent: 104 + - uid: 305 + components: + - type: Transform + pos: 6.5,-9.5 + parent: 104 +- proto: AirlockSyndicateNukeopLocked + entities: + - uid: 109 + components: + - type: Transform + pos: 14.5,-1.5 + parent: 104 + - uid: 120 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 23.5,-7.5 + parent: 104 + - uid: 136 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 17.5,-7.5 + parent: 104 + - uid: 148 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 20.5,-10.5 + parent: 104 + - uid: 272 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 20.5,-4.5 + parent: 104 +- proto: AlwaysPoweredLightExterior + entities: + - uid: 1267 + components: + - type: Transform + pos: -15.5,23.5 + parent: 104 +- proto: AlwaysPoweredLightLED + entities: + - uid: 135 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -6.5,-1.5 + parent: 104 + - uid: 974 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-17.5 + parent: 104 + - uid: 1286 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -6.5,-5.5 + parent: 104 + - uid: 1287 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -6.5,-11.5 + parent: 104 + - uid: 1288 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -6.5,-20.5 + parent: 104 +- proto: AlwaysPoweredWallLight + entities: + - uid: 258 + components: + - type: Transform + pos: -3.5,-2.5 + parent: 104 + - uid: 1356 + components: + - type: Transform + pos: 1.5,-12.5 + parent: 104 + - uid: 1470 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 12.5,-14.5 + parent: 104 + - uid: 1471 + components: + - type: Transform + pos: 12.5,-10.5 + parent: 104 + - uid: 1532 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 10.5,-8.5 + parent: 104 + - uid: 1534 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-1.5 + parent: 104 + - uid: 1536 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-2.5 + parent: 104 + - uid: 1537 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-8.5 + parent: 104 + - uid: 1544 + components: + - type: Transform + pos: 15.5,-6.5 + parent: 104 + - uid: 1545 + components: + - type: Transform + pos: 14.5,-3.5 + parent: 104 + - uid: 1546 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 10.5,-4.5 + parent: 104 + - uid: 1570 + components: + - type: Transform + pos: 19.5,-5.5 + parent: 104 + - uid: 1571 + components: + - type: Transform + pos: 21.5,-5.5 + parent: 104 + - uid: 1572 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 19.5,-9.5 + parent: 104 + - uid: 1573 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 21.5,-9.5 + parent: 104 + - uid: 1758 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -10.5,5.5 + parent: 104 + - uid: 1759 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,12.5 + parent: 104 + - uid: 1760 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 14.5,-22.5 + parent: 104 + - uid: 1763 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 10.5,-22.5 + parent: 104 + - uid: 1867 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -2.5,-16.5 + parent: 104 + - uid: 2257 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,10.5 + parent: 104 + - uid: 2291 + components: + - type: Transform + pos: 5.5,14.5 + parent: 104 + - uid: 2397 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 24.5,-11.5 + parent: 104 + - uid: 2398 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 24.5,-3.5 + parent: 104 + - uid: 2399 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 17.5,-3.5 + parent: 104 + - uid: 2400 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 17.5,-11.5 + parent: 104 + - uid: 2401 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 17.5,-15.5 + parent: 104 + - uid: 2402 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 27.5,-15.5 + parent: 104 + - uid: 2403 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 25.5,-19.5 + parent: 104 + - uid: 2404 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 19.5,-19.5 + parent: 104 + - uid: 3375 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 3.5,-11.5 + parent: 104 + - uid: 3377 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-11.5 + parent: 104 +- proto: APCHyperCapacity + entities: + - uid: 1431 + components: + - type: Transform + pos: 20.5,-12.5 + parent: 104 + - uid: 1445 + components: + - type: Transform + pos: 13.5,-9.5 + parent: 104 + - uid: 1477 + components: + - type: Transform + pos: 8.5,-5.5 + parent: 104 + - uid: 1574 + components: + - type: Transform + pos: 21.5,-4.5 + parent: 104 +- proto: AsteroidRock + entities: + - uid: 19 + components: + - type: Transform + pos: 16.5,1.5 + parent: 104 + - uid: 20 + components: + - type: Transform + pos: -3.5,-10.5 + parent: 104 + - uid: 216 + components: + - type: Transform + pos: -4.5,-9.5 + parent: 104 + - uid: 238 + components: + - type: Transform + pos: -4.5,-13.5 + parent: 104 + - uid: 243 + components: + - type: Transform + pos: -4.5,-6.5 + parent: 104 + - uid: 244 + components: + - type: Transform + pos: -4.5,-8.5 + parent: 104 + - uid: 246 + components: + - type: Transform + pos: -3.5,-8.5 + parent: 104 + - uid: 247 + components: + - type: Transform + pos: -4.5,-12.5 + parent: 104 + - uid: 248 + components: + - type: Transform + pos: -4.5,-7.5 + parent: 104 + - uid: 249 + components: + - type: Transform + pos: -8.5,9.5 + parent: 104 + - uid: 250 + components: + - type: Transform + pos: 0.5,-10.5 + parent: 104 + - uid: 251 + components: + - type: Transform + pos: 15.5,1.5 + parent: 104 + - uid: 255 + components: + - type: Transform + pos: -3.5,-7.5 + parent: 104 + - uid: 257 + components: + - type: Transform + pos: 15.5,0.5 + parent: 104 + - uid: 259 + components: + - type: Transform + pos: -4.5,-10.5 + parent: 104 + - uid: 263 + components: + - type: Transform + pos: -4.5,-11.5 + parent: 104 + - uid: 277 + components: + - type: Transform + pos: -10.5,22.5 + parent: 104 + - uid: 292 + components: + - type: Transform + pos: -9.5,21.5 + parent: 104 + - uid: 294 + components: + - type: Transform + pos: -10.5,21.5 + parent: 104 + - uid: 295 + components: + - type: Transform + pos: -11.5,22.5 + parent: 104 + - uid: 296 + components: + - type: Transform + pos: -20.5,9.5 + parent: 104 + - uid: 358 + components: + - type: Transform + pos: -22.5,21.5 + parent: 104 + - uid: 359 + components: + - type: Transform + pos: -22.5,15.5 + parent: 104 + - uid: 360 + components: + - type: Transform + pos: -22.5,16.5 + parent: 104 + - uid: 361 + components: + - type: Transform + pos: -21.5,17.5 + parent: 104 + - uid: 362 + components: + - type: Transform + pos: -21.5,16.5 + parent: 104 + - uid: 363 + components: + - type: Transform + pos: -21.5,15.5 + parent: 104 + - uid: 364 + components: + - type: Transform + pos: -21.5,14.5 + parent: 104 + - uid: 365 + components: + - type: Transform + pos: -22.5,14.5 + parent: 104 + - uid: 366 + components: + - type: Transform + pos: -21.5,13.5 + parent: 104 + - uid: 367 + components: + - type: Transform + pos: -21.5,20.5 + parent: 104 + - uid: 368 + components: + - type: Transform + pos: -21.5,21.5 + parent: 104 + - uid: 369 + components: + - type: Transform + pos: -20.5,21.5 + parent: 104 + - uid: 370 + components: + - type: Transform + pos: -19.5,21.5 + parent: 104 + - uid: 371 + components: + - type: Transform + pos: -20.5,22.5 + parent: 104 + - uid: 372 + components: + - type: Transform + pos: -18.5,21.5 + parent: 104 + - uid: 373 + components: + - type: Transform + pos: -20.5,8.5 + parent: 104 + - uid: 374 + components: + - type: Transform + pos: -22.5,22.5 + parent: 104 + - uid: 375 + components: + - type: Transform + pos: -21.5,22.5 + parent: 104 + - uid: 376 + components: + - type: Transform + pos: -23.5,22.5 + parent: 104 + - uid: 377 + components: + - type: Transform + pos: -20.5,15.5 + parent: 104 + - uid: 401 + components: + - type: Transform + pos: -20.5,20.5 + parent: 104 + - uid: 402 + components: + - type: Transform + pos: -20.5,19.5 + parent: 104 + - uid: 403 + components: + - type: Transform + pos: -20.5,18.5 + parent: 104 + - uid: 404 + components: + - type: Transform + pos: -20.5,17.5 + parent: 104 + - uid: 405 + components: + - type: Transform + pos: -20.5,16.5 + parent: 104 + - uid: 406 + components: + - type: Transform + pos: -20.5,14.5 + parent: 104 + - uid: 407 + components: + - type: Transform + pos: -24.5,23.5 + parent: 104 + - uid: 408 + components: + - type: Transform + pos: -24.5,22.5 + parent: 104 + - uid: 409 + components: + - type: Transform + pos: -20.5,10.5 + parent: 104 + - uid: 410 + components: + - type: Transform + pos: -9.5,22.5 + parent: 104 + - uid: 415 + components: + - type: Transform + pos: -18.5,23.5 + parent: 104 + - uid: 417 + components: + - type: Transform + pos: -16.5,6.5 + parent: 104 + - uid: 419 + components: + - type: Transform + pos: -7.5,21.5 + parent: 104 + - uid: 420 + components: + - type: Transform + pos: -8.5,21.5 + parent: 104 + - uid: 421 + components: + - type: Transform + pos: -10.5,23.5 + parent: 104 + - uid: 422 + components: + - type: Transform + pos: -11.5,23.5 + parent: 104 + - uid: 423 + components: + - type: Transform + pos: -19.5,22.5 + parent: 104 + - uid: 425 + components: + - type: Transform + pos: -18.5,22.5 + parent: 104 + - uid: 426 + components: + - type: Transform + pos: -7.5,12.5 + parent: 104 + - uid: 455 + components: + - type: Transform + pos: 1.5,-19.5 + parent: 104 + - uid: 469 + components: + - type: Transform + pos: -8.5,10.5 + parent: 104 + - uid: 471 + components: + - type: Transform + pos: -9.5,10.5 + parent: 104 + - uid: 473 + components: + - type: Transform + pos: -8.5,11.5 + parent: 104 + - uid: 475 + components: + - type: Transform + pos: -9.5,11.5 + parent: 104 + - uid: 477 + components: + - type: Transform + pos: 16.5,0.5 + parent: 104 + - uid: 478 + components: + - type: Transform + pos: -6.5,12.5 + parent: 104 + - uid: 479 + components: + - type: Transform + pos: -2.5,-18.5 + parent: 104 + - uid: 480 + components: + - type: Transform + pos: -2.5,-19.5 + parent: 104 + - uid: 483 + components: + - type: Transform + pos: -2.5,-10.5 + parent: 104 + - uid: 485 + components: + - type: Transform + pos: -2.5,-6.5 + parent: 104 + - uid: 488 + components: + - type: Transform + pos: -2.5,-7.5 + parent: 104 + - uid: 489 + components: + - type: Transform + pos: -2.5,-8.5 + parent: 104 + - uid: 492 + components: + - type: Transform + pos: -3.5,-20.5 + parent: 104 + - uid: 502 + components: + - type: Transform + pos: -3.5,-18.5 + parent: 104 + - uid: 504 + components: + - type: Transform + pos: -2.5,-9.5 + parent: 104 + - uid: 505 + components: + - type: Transform + pos: -3.5,-19.5 + parent: 104 + - uid: 506 + components: + - type: Transform + pos: 17.5,-1.5 + parent: 104 + - uid: 509 + components: + - type: Transform + pos: -4.5,-19.5 + parent: 104 + - uid: 511 + components: + - type: Transform + pos: -4.5,-18.5 + parent: 104 + - uid: 512 + components: + - type: Transform + pos: -9.5,12.5 + parent: 104 + - uid: 513 + components: + - type: Transform + pos: -3.5,-9.5 + parent: 104 + - uid: 521 + components: + - type: Transform + pos: -4.5,-14.5 + parent: 104 + - uid: 522 + components: + - type: Transform + pos: -1.5,19.5 + parent: 104 + - uid: 524 + components: + - type: Transform + pos: -0.5,19.5 + parent: 104 + - uid: 525 + components: + - type: Transform + pos: 9.5,20.5 + parent: 104 + - uid: 529 + components: + - type: Transform + pos: 10.5,19.5 + parent: 104 + - uid: 531 + components: + - type: Transform + pos: 11.5,19.5 + parent: 104 + - uid: 532 + components: + - type: Transform + pos: 12.5,19.5 + parent: 104 + - uid: 533 + components: + - type: Transform + pos: 16.5,19.5 + parent: 104 + - uid: 534 + components: + - type: Transform + pos: 17.5,19.5 + parent: 104 + - uid: 535 + components: + - type: Transform + pos: 18.5,19.5 + parent: 104 + - uid: 536 + components: + - type: Transform + pos: 18.5,11.5 + parent: 104 + - uid: 537 + components: + - type: Transform + pos: 18.5,12.5 + parent: 104 + - uid: 538 + components: + - type: Transform + pos: 18.5,15.5 + parent: 104 + - uid: 539 + components: + - type: Transform + pos: 18.5,18.5 + parent: 104 + - uid: 543 + components: + - type: Transform + pos: 26.5,9.5 + parent: 104 + - uid: 546 + components: + - type: Transform + pos: 28.5,3.5 + parent: 104 + - uid: 547 + components: + - type: Transform + pos: 29.5,3.5 + parent: 104 + - uid: 548 + components: + - type: Transform + pos: 30.5,3.5 + parent: 104 + - uid: 549 + components: + - type: Transform + pos: 22.5,-22.5 + parent: 104 + - uid: 550 + components: + - type: Transform + pos: 31.5,-20.5 + parent: 104 + - uid: 551 + components: + - type: Transform + pos: 31.5,-19.5 + parent: 104 + - uid: 552 + components: + - type: Transform + pos: 31.5,-18.5 + parent: 104 + - uid: 553 + components: + - type: Transform + pos: 31.5,-17.5 + parent: 104 + - uid: 554 + components: + - type: Transform + pos: 31.5,-16.5 + parent: 104 + - uid: 555 + components: + - type: Transform + pos: 31.5,-15.5 + parent: 104 + - uid: 556 + components: + - type: Transform + pos: 31.5,-14.5 + parent: 104 + - uid: 557 + components: + - type: Transform + pos: 31.5,-13.5 + parent: 104 + - uid: 558 + components: + - type: Transform + pos: 31.5,-12.5 + parent: 104 + - uid: 559 + components: + - type: Transform + pos: 31.5,-7.5 + parent: 104 + - uid: 560 + components: + - type: Transform + pos: 31.5,-6.5 + parent: 104 + - uid: 561 + components: + - type: Transform + pos: 31.5,-5.5 + parent: 104 + - uid: 562 + components: + - type: Transform + pos: 31.5,-4.5 + parent: 104 + - uid: 563 + components: + - type: Transform + pos: 31.5,-3.5 + parent: 104 + - uid: 564 + components: + - type: Transform + pos: 31.5,-2.5 + parent: 104 + - uid: 565 + components: + - type: Transform + pos: 31.5,-1.5 + parent: 104 + - uid: 566 + components: + - type: Transform + pos: 31.5,-0.5 + parent: 104 + - uid: 567 + components: + - type: Transform + pos: 31.5,0.5 + parent: 104 + - uid: 568 + components: + - type: Transform + pos: 31.5,1.5 + parent: 104 + - uid: 569 + components: + - type: Transform + pos: 17.5,-22.5 + parent: 104 + - uid: 570 + components: + - type: Transform + pos: 18.5,-22.5 + parent: 104 + - uid: 571 + components: + - type: Transform + pos: 19.5,-22.5 + parent: 104 + - uid: 572 + components: + - type: Transform + pos: 20.5,-22.5 + parent: 104 + - uid: 573 + components: + - type: Transform + pos: 21.5,-22.5 + parent: 104 + - uid: 574 + components: + - type: Transform + pos: -5.5,-22.5 + parent: 104 + - uid: 575 + components: + - type: Transform + pos: 23.5,-22.5 + parent: 104 + - uid: 576 + components: + - type: Transform + pos: 24.5,-22.5 + parent: 104 + - uid: 577 + components: + - type: Transform + pos: 25.5,-22.5 + parent: 104 + - uid: 578 + components: + - type: Transform + pos: 26.5,-22.5 + parent: 104 + - uid: 579 + components: + - type: Transform + pos: 27.5,-22.5 + parent: 104 + - uid: 580 + components: + - type: Transform + pos: 28.5,-22.5 + parent: 104 + - uid: 581 + components: + - type: Transform + pos: -4.5,-20.5 + parent: 104 + - uid: 582 + components: + - type: Transform + pos: -4.5,-15.5 + parent: 104 + - uid: 586 + components: + - type: Transform + pos: 4.5,-22.5 + parent: 104 + - uid: 587 + components: + - type: Transform + pos: 5.5,-22.5 + parent: 104 + - uid: 588 + components: + - type: Transform + pos: 6.5,-22.5 + parent: 104 + - uid: 589 + components: + - type: Transform + pos: 7.5,-22.5 + parent: 104 + - uid: 590 + components: + - type: Transform + pos: 8.5,-22.5 + parent: 104 + - uid: 595 + components: + - type: Transform + pos: -5.5,-21.5 + parent: 104 + - uid: 596 + components: + - type: Transform + pos: -5.5,-20.5 + parent: 104 + - uid: 597 + components: + - type: Transform + pos: -5.5,-19.5 + parent: 104 + - uid: 598 + components: + - type: Transform + pos: -5.5,-18.5 + parent: 104 + - uid: 599 + components: + - type: Transform + pos: -5.5,-17.5 + parent: 104 + - uid: 600 + components: + - type: Transform + pos: -5.5,-16.5 + parent: 104 + - uid: 601 + components: + - type: Transform + pos: -5.5,-15.5 + parent: 104 + - uid: 602 + components: + - type: Transform + pos: -5.5,-14.5 + parent: 104 + - uid: 603 + components: + - type: Transform + pos: -5.5,-13.5 + parent: 104 + - uid: 604 + components: + - type: Transform + pos: -5.5,-12.5 + parent: 104 + - uid: 605 + components: + - type: Transform + pos: -5.5,-11.5 + parent: 104 + - uid: 606 + components: + - type: Transform + pos: -5.5,-10.5 + parent: 104 + - uid: 607 + components: + - type: Transform + pos: -5.5,-9.5 + parent: 104 + - uid: 608 + components: + - type: Transform + pos: -5.5,-8.5 + parent: 104 + - uid: 609 + components: + - type: Transform + pos: -5.5,-7.5 + parent: 104 + - uid: 610 + components: + - type: Transform + pos: -14.5,16.5 + parent: 104 + - uid: 614 + components: + - type: Transform + pos: -16.5,15.5 + parent: 104 + - uid: 615 + components: + - type: Transform + pos: -10.5,9.5 + parent: 104 + - uid: 618 + components: + - type: Transform + pos: -8.5,12.5 + parent: 104 + - uid: 621 + components: + - type: Transform + pos: -9.5,9.5 + parent: 104 + - uid: 622 + components: + - type: Transform + pos: -10.5,10.5 + parent: 104 + - uid: 632 + components: + - type: Transform + pos: -10.5,12.5 + parent: 104 + - uid: 637 + components: + - type: Transform + pos: -10.5,11.5 + parent: 104 + - uid: 640 + components: + - type: Transform + pos: -16.5,5.5 + parent: 104 + - uid: 648 + components: + - type: Transform + pos: -11.5,13.5 + parent: 104 + - uid: 670 + components: + - type: Transform + pos: -18.5,10.5 + parent: 104 + - uid: 672 + components: + - type: Transform + pos: -16.5,16.5 + parent: 104 + - uid: 673 + components: + - type: Transform + pos: -11.5,12.5 + parent: 104 + - uid: 674 + components: + - type: Transform + pos: -19.5,15.5 + parent: 104 + - uid: 676 + components: + - type: Transform + pos: -19.5,16.5 + parent: 104 + - uid: 677 + components: + - type: Transform + pos: -18.5,11.5 + parent: 104 + - uid: 678 + components: + - type: Transform + pos: -14.5,11.5 + parent: 104 + - uid: 679 + components: + - type: Transform + pos: -14.5,17.5 + parent: 104 + - uid: 680 + components: + - type: Transform + pos: -14.5,18.5 + parent: 104 + - uid: 681 + components: + - type: Transform + pos: -15.5,14.5 + parent: 104 + - uid: 682 + components: + - type: Transform + pos: -24.5,25.5 + parent: 104 + - uid: 684 + components: + - type: Transform + pos: -17.5,13.5 + parent: 104 + - uid: 685 + components: + - type: Transform + pos: -15.5,16.5 + parent: 104 + - uid: 686 + components: + - type: Transform + pos: -12.5,12.5 + parent: 104 + - uid: 687 + components: + - type: Transform + pos: -12.5,11.5 + parent: 104 + - uid: 688 + components: + - type: Transform + pos: -11.5,14.5 + parent: 104 + - uid: 689 + components: + - type: Transform + pos: -15.5,15.5 + parent: 104 + - uid: 690 + components: + - type: Transform + pos: -14.5,14.5 + parent: 104 + - uid: 691 + components: + - type: Transform + pos: -19.5,14.5 + parent: 104 + - uid: 692 + components: + - type: Transform + pos: -13.5,16.5 + parent: 104 + - uid: 693 + components: + - type: Transform + pos: -17.5,15.5 + parent: 104 + - uid: 694 + components: + - type: Transform + pos: -11.5,9.5 + parent: 104 + - uid: 695 + components: + - type: Transform + pos: -20.5,23.5 + parent: 104 + - uid: 699 + components: + - type: Transform + pos: -18.5,8.5 + parent: 104 + - uid: 700 + components: + - type: Transform + pos: -18.5,9.5 + parent: 104 + - uid: 701 + components: + - type: Transform + pos: -13.5,12.5 + parent: 104 + - uid: 702 + components: + - type: Transform + pos: -13.5,17.5 + parent: 104 + - uid: 703 + components: + - type: Transform + pos: -13.5,18.5 + parent: 104 + - uid: 704 + components: + - type: Transform + pos: -15.5,18.5 + parent: 104 + - uid: 705 + components: + - type: Transform + pos: -17.5,14.5 + parent: 104 + - uid: 706 + components: + - type: Transform + pos: -11.5,11.5 + parent: 104 + - uid: 708 + components: + - type: Transform + pos: -17.5,11.5 + parent: 104 + - uid: 709 + components: + - type: Transform + pos: -17.5,16.5 + parent: 104 + - uid: 711 + components: + - type: Transform + pos: -17.5,10.5 + parent: 104 + - uid: 712 + components: + - type: Transform + pos: -15.5,17.5 + parent: 104 + - uid: 714 + components: + - type: Transform + pos: -11.5,17.5 + parent: 104 + - uid: 716 + components: + - type: Transform + pos: -13.5,13.5 + parent: 104 + - uid: 717 + components: + - type: Transform + pos: -13.5,14.5 + parent: 104 + - uid: 718 + components: + - type: Transform + pos: -18.5,14.5 + parent: 104 + - uid: 719 + components: + - type: Transform + pos: -18.5,15.5 + parent: 104 + - uid: 720 + components: + - type: Transform + pos: -18.5,16.5 + parent: 104 + - uid: 721 + components: + - type: Transform + pos: -17.5,8.5 + parent: 104 + - uid: 722 + components: + - type: Transform + pos: -16.5,13.5 + parent: 104 + - uid: 723 + components: + - type: Transform + pos: -16.5,14.5 + parent: 104 + - uid: 724 + components: + - type: Transform + pos: -12.5,17.5 + parent: 104 + - uid: 725 + components: + - type: Transform + pos: -12.5,18.5 + parent: 104 + - uid: 726 + components: + - type: Transform + pos: -14.5,12.5 + parent: 104 + - uid: 728 + components: + - type: Transform + pos: -14.5,13.5 + parent: 104 + - uid: 729 + components: + - type: Transform + pos: -18.5,17.5 + parent: 104 + - uid: 733 + components: + - type: Transform + pos: -18.5,18.5 + parent: 104 + - uid: 734 + components: + - type: Transform + pos: -18.5,19.5 + parent: 104 + - uid: 735 + components: + - type: Transform + pos: -12.5,13.5 + parent: 104 + - uid: 740 + components: + - type: Transform + pos: -12.5,15.5 + parent: 104 + - uid: 741 + components: + - type: Transform + pos: -19.5,17.5 + parent: 104 + - uid: 745 + components: + - type: Transform + pos: -19.5,18.5 + parent: 104 + - uid: 746 + components: + - type: Transform + pos: -19.5,19.5 + parent: 104 + - uid: 747 + components: + - type: Transform + pos: -16.5,17.5 + parent: 104 + - uid: 752 + components: + - type: Transform + pos: -16.5,18.5 + parent: 104 + - uid: 753 + components: + - type: Transform + pos: -15.5,11.5 + parent: 104 + - uid: 755 + components: + - type: Transform + pos: -15.5,12.5 + parent: 104 + - uid: 756 + components: + - type: Transform + pos: -12.5,14.5 + parent: 104 + - uid: 757 + components: + - type: Transform + pos: -16.5,12.5 + parent: 104 + - uid: 758 + components: + - type: Transform + pos: -13.5,15.5 + parent: 104 + - uid: 759 + components: + - type: Transform + pos: -19.5,9.5 + parent: 104 + - uid: 762 + components: + - type: Transform + pos: -17.5,9.5 + parent: 104 + - uid: 763 + components: + - type: Transform + pos: -17.5,17.5 + parent: 104 + - uid: 764 + components: + - type: Transform + pos: -17.5,18.5 + parent: 104 + - uid: 765 + components: + - type: Transform + pos: -16.5,11.5 + parent: 104 + - uid: 768 + components: + - type: Transform + pos: -19.5,8.5 + parent: 104 + - uid: 771 + components: + - type: Transform + pos: -6.5,13.5 + parent: 104 + - uid: 774 + components: + - type: Transform + pos: -10.5,18.5 + parent: 104 + - uid: 775 + components: + - type: Transform + pos: -10.5,17.5 + parent: 104 + - uid: 776 + components: + - type: Transform + pos: -10.5,16.5 + parent: 104 + - uid: 777 + components: + - type: Transform + pos: -10.5,15.5 + parent: 104 + - uid: 778 + components: + - type: Transform + pos: -10.5,14.5 + parent: 104 + - uid: 779 + components: + - type: Transform + pos: -10.5,13.5 + parent: 104 + - uid: 780 + components: + - type: Transform + pos: -9.5,19.5 + parent: 104 + - uid: 781 + components: + - type: Transform + pos: -9.5,18.5 + parent: 104 + - uid: 782 + components: + - type: Transform + pos: -9.5,17.5 + parent: 104 + - uid: 783 + components: + - type: Transform + pos: -9.5,16.5 + parent: 104 + - uid: 784 + components: + - type: Transform + pos: -9.5,15.5 + parent: 104 + - uid: 785 + components: + - type: Transform + pos: -9.5,14.5 + parent: 104 + - uid: 786 + components: + - type: Transform + pos: -9.5,13.5 + parent: 104 + - uid: 787 + components: + - type: Transform + pos: -8.5,19.5 + parent: 104 + - uid: 788 + components: + - type: Transform + pos: -8.5,18.5 + parent: 104 + - uid: 789 + components: + - type: Transform + pos: -8.5,17.5 + parent: 104 + - uid: 790 + components: + - type: Transform + pos: -8.5,15.5 + parent: 104 + - uid: 791 + components: + - type: Transform + pos: -8.5,14.5 + parent: 104 + - uid: 792 + components: + - type: Transform + pos: -8.5,13.5 + parent: 104 + - uid: 793 + components: + - type: Transform + pos: -7.5,19.5 + parent: 104 + - uid: 794 + components: + - type: Transform + pos: -7.5,18.5 + parent: 104 + - uid: 795 + components: + - type: Transform + pos: -7.5,15.5 + parent: 104 + - uid: 796 + components: + - type: Transform + pos: -7.5,14.5 + parent: 104 + - uid: 797 + components: + - type: Transform + pos: -7.5,13.5 + parent: 104 + - uid: 798 + components: + - type: Transform + pos: -15.5,13.5 + parent: 104 + - uid: 801 + components: + - type: Transform + pos: -12.5,16.5 + parent: 104 + - uid: 802 + components: + - type: Transform + pos: -14.5,15.5 + parent: 104 + - uid: 803 + components: + - type: Transform + pos: -19.5,23.5 + parent: 104 + - uid: 804 + components: + - type: Transform + pos: -24.5,24.5 + parent: 104 + - uid: 805 + components: + - type: Transform + pos: -19.5,10.5 + parent: 104 + - uid: 806 + components: + - type: Transform + pos: -17.5,12.5 + parent: 104 + - uid: 807 + components: + - type: Transform + pos: -11.5,16.5 + parent: 104 + - uid: 808 + components: + - type: Transform + pos: -11.5,15.5 + parent: 104 + - uid: 809 + components: + - type: Transform + pos: -13.5,11.5 + parent: 104 + - uid: 810 + components: + - type: Transform + pos: -19.5,20.5 + parent: 104 + - uid: 1020 + components: + - type: Transform + pos: -18.5,20.5 + parent: 104 + - uid: 1021 + components: + - type: Transform + pos: -10.5,20.5 + parent: 104 + - uid: 1027 + components: + - type: Transform + pos: -9.5,20.5 + parent: 104 + - uid: 1028 + components: + - type: Transform + pos: -8.5,20.5 + parent: 104 + - uid: 1029 + components: + - type: Transform + pos: -7.5,20.5 + parent: 104 + - uid: 1030 + components: + - type: Transform + pos: -2.5,20.5 + parent: 104 + - uid: 1031 + components: + - type: Transform + pos: -1.5,20.5 + parent: 104 + - uid: 1032 + components: + - type: Transform + pos: -0.5,20.5 + parent: 104 + - uid: 1033 + components: + - type: Transform + pos: 9.5,22.5 + parent: 104 + - uid: 1036 + components: + - type: Transform + pos: 10.5,20.5 + parent: 104 + - uid: 1041 + components: + - type: Transform + pos: 11.5,20.5 + parent: 104 + - uid: 1042 + components: + - type: Transform + pos: 12.5,20.5 + parent: 104 + - uid: 1043 + components: + - type: Transform + pos: 13.5,20.5 + parent: 104 + - uid: 1044 + components: + - type: Transform + pos: 16.5,20.5 + parent: 104 + - uid: 1045 + components: + - type: Transform + pos: 17.5,20.5 + parent: 104 + - uid: 1046 + components: + - type: Transform + pos: 18.5,20.5 + parent: 104 + - uid: 1047 + components: + - type: Transform + pos: 19.5,20.5 + parent: 104 + - uid: 1048 + components: + - type: Transform + pos: 19.5,19.5 + parent: 104 + - uid: 1049 + components: + - type: Transform + pos: 19.5,18.5 + parent: 104 + - uid: 1050 + components: + - type: Transform + pos: 19.5,16.5 + parent: 104 + - uid: 1051 + components: + - type: Transform + pos: 19.5,15.5 + parent: 104 + - uid: 1052 + components: + - type: Transform + pos: 19.5,14.5 + parent: 104 + - uid: 1053 + components: + - type: Transform + pos: 19.5,12.5 + parent: 104 + - uid: 1054 + components: + - type: Transform + pos: 19.5,11.5 + parent: 104 + - uid: 1055 + components: + - type: Transform + pos: 19.5,10.5 + parent: 104 + - uid: 1056 + components: + - type: Transform + pos: -11.5,20.5 + parent: 104 + - uid: 1057 + components: + - type: Transform + pos: 15.5,3.5 + parent: 104 + - uid: 1058 + components: + - type: Transform + pos: 22.5,4.5 + parent: 104 + - uid: 1059 + components: + - type: Transform + pos: 23.5,4.5 + parent: 104 + - uid: 1060 + components: + - type: Transform + pos: 24.5,4.5 + parent: 104 + - uid: 1064 + components: + - type: Transform + pos: 28.5,4.5 + parent: 104 + - uid: 1065 + components: + - type: Transform + pos: 29.5,4.5 + parent: 104 + - uid: 1066 + components: + - type: Transform + pos: 30.5,4.5 + parent: 104 + - uid: 1067 + components: + - type: Transform + pos: 32.5,0.5 + parent: 104 + - uid: 1068 + components: + - type: Transform + pos: 32.5,-0.5 + parent: 104 + - uid: 1069 + components: + - type: Transform + pos: 32.5,-3.5 + parent: 104 + - uid: 1070 + components: + - type: Transform + pos: 32.5,-12.5 + parent: 104 + - uid: 1071 + components: + - type: Transform + pos: 32.5,-13.5 + parent: 104 + - uid: 1072 + components: + - type: Transform + pos: 32.5,-14.5 + parent: 104 + - uid: 1073 + components: + - type: Transform + pos: 32.5,-15.5 + parent: 104 + - uid: 1074 + components: + - type: Transform + pos: 32.5,-16.5 + parent: 104 + - uid: 1075 + components: + - type: Transform + pos: 32.5,-17.5 + parent: 104 + - uid: 1076 + components: + - type: Transform + pos: 32.5,-18.5 + parent: 104 + - uid: 1077 + components: + - type: Transform + pos: 32.5,-19.5 + parent: 104 + - uid: 1078 + components: + - type: Transform + pos: 32.5,-20.5 + parent: 104 + - uid: 1079 + components: + - type: Transform + pos: 32.5,-21.5 + parent: 104 + - uid: 1083 + components: + - type: Transform + pos: -4.5,-16.5 + parent: 104 + - uid: 1084 + components: + - type: Transform + pos: -4.5,-21.5 + parent: 104 + - uid: 1085 + components: + - type: Transform + pos: 28.5,-23.5 + parent: 104 + - uid: 1092 + components: + - type: Transform + pos: 27.5,-23.5 + parent: 104 + - uid: 1093 + components: + - type: Transform + pos: 26.5,-23.5 + parent: 104 + - uid: 1094 + components: + - type: Transform + pos: 25.5,-23.5 + parent: 104 + - uid: 1095 + components: + - type: Transform + pos: 24.5,-23.5 + parent: 104 + - uid: 1096 + components: + - type: Transform + pos: 23.5,-23.5 + parent: 104 + - uid: 1097 + components: + - type: Transform + pos: 22.5,-23.5 + parent: 104 + - uid: 1098 + components: + - type: Transform + pos: 21.5,-23.5 + parent: 104 + - uid: 1099 + components: + - type: Transform + pos: 20.5,-23.5 + parent: 104 + - uid: 1100 + components: + - type: Transform + pos: 19.5,-23.5 + parent: 104 + - uid: 1101 + components: + - type: Transform + pos: 18.5,-23.5 + parent: 104 + - uid: 1105 + components: + - type: Transform + pos: 8.5,-23.5 + parent: 104 + - uid: 1106 + components: + - type: Transform + pos: 7.5,-23.5 + parent: 104 + - uid: 1107 + components: + - type: Transform + pos: 6.5,-23.5 + parent: 104 + - uid: 1108 + components: + - type: Transform + pos: 5.5,-23.5 + parent: 104 + - uid: 1109 + components: + - type: Transform + pos: -23.5,23.5 + parent: 104 + - uid: 1155 + components: + - type: Transform + pos: -23.5,24.5 + parent: 104 + - uid: 1156 + components: + - type: Transform + pos: -23.5,25.5 + parent: 104 + - uid: 1157 + components: + - type: Transform + pos: -22.5,32.5 + parent: 104 + - uid: 1158 + components: + - type: Transform + pos: -23.5,30.5 + parent: 104 + - uid: 1160 + components: + - type: Transform + pos: -23.5,31.5 + parent: 104 + - uid: 1161 + components: + - type: Transform + pos: -22.5,23.5 + parent: 104 + - uid: 1162 + components: + - type: Transform + pos: -22.5,24.5 + parent: 104 + - uid: 1163 + components: + - type: Transform + pos: -22.5,25.5 + parent: 104 + - uid: 1164 + components: + - type: Transform + pos: -21.5,26.5 + parent: 104 + - uid: 1165 + components: + - type: Transform + pos: -22.5,30.5 + parent: 104 + - uid: 1166 + components: + - type: Transform + pos: -21.5,30.5 + parent: 104 + - uid: 1167 + components: + - type: Transform + pos: -24.5,31.5 + parent: 104 + - uid: 1168 + components: + - type: Transform + pos: -23.5,20.5 + parent: 104 + - uid: 1169 + components: + - type: Transform + pos: -24.5,21.5 + parent: 104 + - uid: 1170 + components: + - type: Transform + pos: -22.5,31.5 + parent: 104 + - uid: 1171 + components: + - type: Transform + pos: -25.5,30.5 + parent: 104 + - uid: 1172 + components: + - type: Transform + pos: -25.5,31.5 + parent: 104 + - uid: 1173 + components: + - type: Transform + pos: -25.5,32.5 + parent: 104 + - uid: 1174 + components: + - type: Transform + pos: -23.5,21.5 + parent: 104 + - uid: 1175 + components: + - type: Transform + pos: -23.5,29.5 + parent: 104 + - uid: 1176 + components: + - type: Transform + pos: -22.5,20.5 + parent: 104 + - uid: 1177 + components: + - type: Transform + pos: -23.5,15.5 + parent: 104 + - uid: 1178 + components: + - type: Transform + pos: -23.5,13.5 + parent: 104 + - uid: 1179 + components: + - type: Transform + pos: -22.5,13.5 + parent: 104 + - uid: 1180 + components: + - type: Transform + pos: -11.5,10.5 + parent: 104 + - uid: 1224 + components: + - type: Transform + pos: -23.5,14.5 + parent: 104 + - uid: 1250 + components: + - type: Transform + pos: 1.5,-10.5 + parent: 104 + - uid: 1390 + components: + - type: Transform + pos: -0.5,-10.5 + parent: 104 + - uid: 1391 + components: + - type: Transform + pos: -23.5,28.5 + parent: 104 + - uid: 1392 + components: + - type: Transform + pos: -24.5,30.5 + parent: 104 + - uid: 1393 + components: + - type: Transform + pos: -23.5,32.5 + parent: 104 + - uid: 1394 + components: + - type: Transform + pos: -22.5,26.5 + parent: 104 + - uid: 1395 + components: + - type: Transform + pos: -23.5,26.5 + parent: 104 + - uid: 1396 + components: + - type: Transform + pos: -24.5,32.5 + parent: 104 + - uid: 1397 + components: + - type: Transform + pos: -23.5,27.5 + parent: 104 + - uid: 1398 + components: + - type: Transform + pos: -22.5,28.5 + parent: 104 + - uid: 1399 + components: + - type: Transform + pos: -21.5,29.5 + parent: 104 + - uid: 1400 + components: + - type: Transform + pos: -22.5,29.5 + parent: 104 + - uid: 1401 + components: + - type: Transform + pos: -22.5,27.5 + parent: 104 + - uid: 1402 + components: + - type: Transform + pos: -21.5,27.5 + parent: 104 + - uid: 1403 + components: + - type: Transform + pos: -21.5,28.5 + parent: 104 + - uid: 1404 + components: + - type: Transform + pos: -21.5,25.5 + parent: 104 + - uid: 1405 + components: + - type: Transform + pos: -21.5,24.5 + parent: 104 + - uid: 1406 + components: + - type: Transform + pos: -21.5,23.5 + parent: 104 + - uid: 1407 + components: + - type: Transform + pos: 6.5,-24.5 + parent: 104 + - uid: 1408 + components: + - type: Transform + pos: 7.5,-24.5 + parent: 104 + - uid: 1415 + components: + - type: Transform + pos: 3.5,-21.5 + parent: 104 + - uid: 1416 + components: + - type: Transform + pos: 4.5,-21.5 + parent: 104 + - uid: 1417 + components: + - type: Transform + pos: 20.5,-24.5 + parent: 104 + - uid: 1418 + components: + - type: Transform + pos: 21.5,-24.5 + parent: 104 + - uid: 1419 + components: + - type: Transform + pos: 22.5,-24.5 + parent: 104 + - uid: 1420 + components: + - type: Transform + pos: 23.5,-24.5 + parent: 104 + - uid: 1421 + components: + - type: Transform + pos: 24.5,-24.5 + parent: 104 + - uid: 1422 + components: + - type: Transform + pos: 25.5,-24.5 + parent: 104 + - uid: 1423 + components: + - type: Transform + pos: 26.5,-24.5 + parent: 104 + - uid: 1424 + components: + - type: Transform + pos: 27.5,-24.5 + parent: 104 + - uid: 1425 + components: + - type: Transform + pos: -4.5,-22.5 + parent: 104 + - uid: 1427 + components: + - type: Transform + pos: -4.5,-17.5 + parent: 104 + - uid: 1433 + components: + - type: Transform + pos: 5.5,-24.5 + parent: 104 + - uid: 1434 + components: + - type: Transform + pos: 33.5,-21.5 + parent: 104 + - uid: 1435 + components: + - type: Transform + pos: 33.5,-20.5 + parent: 104 + - uid: 1436 + components: + - type: Transform + pos: 33.5,-19.5 + parent: 104 + - uid: 1437 + components: + - type: Transform + pos: 33.5,-18.5 + parent: 104 + - uid: 1440 + components: + - type: Transform + pos: 33.5,-17.5 + parent: 104 + - uid: 1441 + components: + - type: Transform + pos: 33.5,-16.5 + parent: 104 + - uid: 1442 + components: + - type: Transform + pos: 33.5,-15.5 + parent: 104 + - uid: 1443 + components: + - type: Transform + pos: 33.5,-14.5 + parent: 104 + - uid: 1444 + components: + - type: Transform + pos: 33.5,-13.5 + parent: 104 + - uid: 1446 + components: + - type: Transform + pos: 33.5,-12.5 + parent: 104 + - uid: 1447 + components: + - type: Transform + pos: 30.5,5.5 + parent: 104 + - uid: 1448 + components: + - type: Transform + pos: 29.5,5.5 + parent: 104 + - uid: 1472 + components: + - type: Transform + pos: 28.5,5.5 + parent: 104 + - uid: 1542 + components: + - type: Transform + pos: 24.5,5.5 + parent: 104 + - uid: 1550 + components: + - type: Transform + pos: 23.5,5.5 + parent: 104 + - uid: 1552 + components: + - type: Transform + pos: 22.5,5.5 + parent: 104 + - uid: 1553 + components: + - type: Transform + pos: 21.5,5.5 + parent: 104 + - uid: 1557 + components: + - type: Transform + pos: 19.5,19.5 + parent: 104 + - uid: 1559 + components: + - type: Transform + pos: 19.5,20.5 + parent: 104 + - uid: 1560 + components: + - type: Transform + pos: 19.5,21.5 + parent: 104 + - uid: 1562 + components: + - type: Transform + pos: -11.5,19.5 + parent: 104 + - uid: 1564 + components: + - type: Transform + pos: -11.5,18.5 + parent: 104 + - uid: 1566 + components: + - type: Transform + pos: 20.5,9.5 + parent: 104 + - uid: 1567 + components: + - type: Transform + pos: 20.5,10.5 + parent: 104 + - uid: 1568 + components: + - type: Transform + pos: 20.5,11.5 + parent: 104 + - uid: 1569 + components: + - type: Transform + pos: 20.5,12.5 + parent: 104 + - uid: 1594 + components: + - type: Transform + pos: 20.5,13.5 + parent: 104 + - uid: 1598 + components: + - type: Transform + pos: 20.5,14.5 + parent: 104 + - uid: 1599 + components: + - type: Transform + pos: 20.5,15.5 + parent: 104 + - uid: 1600 + components: + - type: Transform + pos: 20.5,16.5 + parent: 104 + - uid: 1601 + components: + - type: Transform + pos: 20.5,17.5 + parent: 104 + - uid: 1602 + components: + - type: Transform + pos: 20.5,18.5 + parent: 104 + - uid: 1603 + components: + - type: Transform + pos: 20.5,19.5 + parent: 104 + - uid: 1604 + components: + - type: Transform + pos: 20.5,20.5 + parent: 104 + - uid: 1605 + components: + - type: Transform + pos: 20.5,21.5 + parent: 104 + - uid: 1606 + components: + - type: Transform + pos: -10.5,19.5 + parent: 104 + - uid: 1607 + components: + - type: Transform + pos: 19.5,11.5 + parent: 104 + - uid: 1608 + components: + - type: Transform + pos: 19.5,12.5 + parent: 104 + - uid: 1609 + components: + - type: Transform + pos: 19.5,15.5 + parent: 104 + - uid: 1610 + components: + - type: Transform + pos: 18.5,21.5 + parent: 104 + - uid: 1611 + components: + - type: Transform + pos: 17.5,21.5 + parent: 104 + - uid: 1612 + components: + - type: Transform + pos: 16.5,21.5 + parent: 104 + - uid: 1613 + components: + - type: Transform + pos: 15.5,21.5 + parent: 104 + - uid: 1614 + components: + - type: Transform + pos: 14.5,21.5 + parent: 104 + - uid: 1615 + components: + - type: Transform + pos: 13.5,21.5 + parent: 104 + - uid: 1616 + components: + - type: Transform + pos: 12.5,21.5 + parent: 104 + - uid: 1623 + components: + - type: Transform + pos: 11.5,21.5 + parent: 104 + - uid: 1625 + components: + - type: Transform + pos: 10.5,21.5 + parent: 104 + - uid: 1632 + components: + - type: Transform + pos: 9.5,21.5 + parent: 104 + - uid: 1636 + components: + - type: Transform + pos: -0.5,21.5 + parent: 104 + - uid: 1637 + components: + - type: Transform + pos: -1.5,21.5 + parent: 104 + - uid: 1638 + components: + - type: Transform + pos: -2.5,21.5 + parent: 104 + - uid: 1639 + components: + - type: Transform + pos: -0.5,22.5 + parent: 104 + - uid: 1640 + components: + - type: Transform + pos: -1.5,22.5 + parent: 104 + - uid: 1641 + components: + - type: Transform + pos: -2.5,22.5 + parent: 104 + - uid: 1642 + components: + - type: Transform + pos: -3.5,22.5 + parent: 104 + - uid: 1643 + components: + - type: Transform + pos: -7.5,22.5 + parent: 104 + - uid: 1644 + components: + - type: Transform + pos: -8.5,22.5 + parent: 104 + - uid: 1645 + components: + - type: Transform + pos: -8.5,23.5 + parent: 104 + - uid: 1646 + components: + - type: Transform + pos: -9.5,23.5 + parent: 104 + - uid: 1647 + components: + - type: Transform + pos: -10.5,23.5 + parent: 104 + - uid: 1649 + components: + - type: Transform + pos: 28.5,7.5 + parent: 104 + - uid: 1650 + components: + - type: Transform + pos: 28.5,8.5 + parent: 104 + - uid: 1651 + components: + - type: Transform + pos: 22.5,13.5 + parent: 104 + - uid: 1657 + components: + - type: Transform + pos: 23.5,13.5 + parent: 104 + - uid: 1658 + components: + - type: Transform + pos: 17.5,22.5 + parent: 104 + - uid: 1659 + components: + - type: Transform + pos: 24.5,16.5 + parent: 104 + - uid: 1660 + components: + - type: Transform + pos: 24.5,15.5 + parent: 104 + - uid: 1661 + components: + - type: Transform + pos: 24.5,14.5 + parent: 104 + - uid: 1662 + components: + - type: Transform + pos: 23.5,17.5 + parent: 104 + - uid: 1663 + components: + - type: Transform + pos: 28.5,6.5 + parent: 104 + - uid: 1664 + components: + - type: Transform + pos: 23.5,14.5 + parent: 104 + - uid: 1665 + components: + - type: Transform + pos: 22.5,14.5 + parent: 104 + - uid: 1666 + components: + - type: Transform + pos: 23.5,12.5 + parent: 104 + - uid: 1667 + components: + - type: Transform + pos: 21.5,8.5 + parent: 104 + - uid: 1676 + components: + - type: Transform + pos: 24.5,9.5 + parent: 104 + - uid: 1677 + components: + - type: Transform + pos: 24.5,10.5 + parent: 104 + - uid: 1678 + components: + - type: Transform + pos: 24.5,17.5 + parent: 104 + - uid: 1679 + components: + - type: Transform + pos: 23.5,16.5 + parent: 104 + - uid: 1682 + components: + - type: Transform + pos: 23.5,15.5 + parent: 104 + - uid: 1683 + components: + - type: Transform + pos: 24.5,11.5 + parent: 104 + - uid: 1685 + components: + - type: Transform + pos: 23.5,11.5 + parent: 104 + - uid: 1686 + components: + - type: Transform + pos: 21.5,9.5 + parent: 104 + - uid: 1687 + components: + - type: Transform + pos: 27.5,7.5 + parent: 104 + - uid: 1688 + components: + - type: Transform + pos: 27.5,8.5 + parent: 104 + - uid: 1691 + components: + - type: Transform + pos: 26.5,8.5 + parent: 104 + - uid: 1692 + components: + - type: Transform + pos: 22.5,15.5 + parent: 104 + - uid: 1693 + components: + - type: Transform + pos: 24.5,12.5 + parent: 104 + - uid: 1695 + components: + - type: Transform + pos: 23.5,10.5 + parent: 104 + - uid: 1696 + components: + - type: Transform + pos: 22.5,10.5 + parent: 104 + - uid: 1698 + components: + - type: Transform + pos: 12.5,22.5 + parent: 104 + - uid: 1700 + components: + - type: Transform + pos: 25.5,9.5 + parent: 104 + - uid: 1701 + components: + - type: Transform + pos: 25.5,10.5 + parent: 104 + - uid: 1702 + components: + - type: Transform + pos: 13.5,22.5 + parent: 104 + - uid: 1709 + components: + - type: Transform + pos: 24.5,13.5 + parent: 104 + - uid: 1710 + components: + - type: Transform + pos: 22.5,8.5 + parent: 104 + - uid: 1712 + components: + - type: Transform + pos: 22.5,9.5 + parent: 104 + - uid: 1713 + components: + - type: Transform + pos: 11.5,22.5 + parent: 104 + - uid: 1714 + components: + - type: Transform + pos: 25.5,11.5 + parent: 104 + - uid: 1715 + components: + - type: Transform + pos: 22.5,16.5 + parent: 104 + - uid: 1716 + components: + - type: Transform + pos: 22.5,17.5 + parent: 104 + - uid: 1717 + components: + - type: Transform + pos: 19.5,22.5 + parent: 104 + - uid: 1719 + components: + - type: Transform + pos: 21.5,13.5 + parent: 104 + - uid: 1720 + components: + - type: Transform + pos: 23.5,9.5 + parent: 104 + - uid: 1721 + components: + - type: Transform + pos: 21.5,11.5 + parent: 104 + - uid: 1722 + components: + - type: Transform + pos: 22.5,11.5 + parent: 104 + - uid: 1723 + components: + - type: Transform + pos: 10.5,22.5 + parent: 104 + - uid: 1724 + components: + - type: Transform + pos: 22.5,18.5 + parent: 104 + - uid: 1725 + components: + - type: Transform + pos: 16.5,22.5 + parent: 104 + - uid: 1726 + components: + - type: Transform + pos: 15.5,22.5 + parent: 104 + - uid: 1727 + components: + - type: Transform + pos: 14.5,22.5 + parent: 104 + - uid: 1729 + components: + - type: Transform + pos: 23.5,13.5 + parent: 104 + - uid: 1731 + components: + - type: Transform + pos: -11.5,21.5 + parent: 104 + - uid: 1732 + components: + - type: Transform + pos: 22.5,12.5 + parent: 104 + - uid: 1733 + components: + - type: Transform + pos: 27.5,6.5 + parent: 104 + - uid: 1734 + components: + - type: Transform + pos: 22.5,19.5 + parent: 104 + - uid: 1735 + components: + - type: Transform + pos: 18.5,22.5 + parent: 104 + - uid: 1736 + components: + - type: Transform + pos: 21.5,21.5 + parent: 104 + - uid: 1740 + components: + - type: Transform + pos: -11.5,20.5 + parent: 104 + - uid: 1741 + components: + - type: Transform + pos: 21.5,10.5 + parent: 104 + - uid: 1742 + components: + - type: Transform + pos: 21.5,20.5 + parent: 104 + - uid: 1743 + components: + - type: Transform + pos: 20.5,22.5 + parent: 104 + - uid: 1744 + components: + - type: Transform + pos: 21.5,19.5 + parent: 104 + - uid: 1745 + components: + - type: Transform + pos: 21.5,18.5 + parent: 104 + - uid: 1746 + components: + - type: Transform + pos: 21.5,17.5 + parent: 104 + - uid: 1747 + components: + - type: Transform + pos: 21.5,16.5 + parent: 104 + - uid: 1748 + components: + - type: Transform + pos: 21.5,15.5 + parent: 104 + - uid: 1749 + components: + - type: Transform + pos: 21.5,14.5 + parent: 104 + - uid: 1750 + components: + - type: Transform + pos: 21.5,12.5 + parent: 104 + - uid: 1751 + components: + - type: Transform + pos: 9.5,19.5 + parent: 104 + - uid: 1752 + components: + - type: Transform + pos: -2.5,23.5 + parent: 104 + - uid: 1753 + components: + - type: Transform + pos: -3.5,23.5 + parent: 104 + - uid: 1754 + components: + - type: Transform + pos: -4.5,23.5 + parent: 104 + - uid: 1755 + components: + - type: Transform + pos: -5.5,23.5 + parent: 104 + - uid: 1756 + components: + - type: Transform + pos: -6.5,23.5 + parent: 104 + - uid: 1757 + components: + - type: Transform + pos: -7.5,23.5 + parent: 104 + - uid: 1762 + components: + - type: Transform + pos: 1.5,-18.5 + parent: 104 + - uid: 1767 + components: + - type: Transform + pos: -19.5,7.5 + parent: 104 + - uid: 1768 + components: + - type: Transform + pos: -19.5,6.5 + parent: 104 + - uid: 1769 + components: + - type: Transform + pos: -19.5,5.5 + parent: 104 + - uid: 1770 + components: + - type: Transform + pos: -18.5,7.5 + parent: 104 + - uid: 1771 + components: + - type: Transform + pos: -18.5,6.5 + parent: 104 + - uid: 1772 + components: + - type: Transform + pos: -18.5,5.5 + parent: 104 + - uid: 1773 + components: + - type: Transform + pos: -17.5,7.5 + parent: 104 + - uid: 1774 + components: + - type: Transform + pos: -17.5,6.5 + parent: 104 + - uid: 1775 + components: + - type: Transform + pos: -17.5,5.5 + parent: 104 + - uid: 1776 + components: + - type: Transform + pos: -21.5,19.5 + parent: 104 + - uid: 1777 + components: + - type: Transform + pos: -22.5,19.5 + parent: 104 + - uid: 1778 + components: + - type: Transform + pos: -21.5,18.5 + parent: 104 + - uid: 1779 + components: + - type: Transform + pos: -22.5,18.5 + parent: 104 + - uid: 1780 + components: + - type: Transform + pos: -22.5,17.5 + parent: 104 + - uid: 1781 + components: + - type: Transform + pos: -23.5,17.5 + parent: 104 + - uid: 1782 + components: + - type: Transform + pos: -23.5,16.5 + parent: 104 + - uid: 1792 + components: + - type: Transform + pos: 4.5,-19.5 + parent: 104 + - uid: 1793 + components: + - type: Transform + pos: 4.5,-20.5 + parent: 104 + - uid: 1794 + components: + - type: Transform + pos: 2.5,-19.5 + parent: 104 + - uid: 1795 + components: + - type: Transform + pos: 2.5,-20.5 + parent: 104 + - uid: 1796 + components: + - type: Transform + pos: -1.5,-10.5 + parent: 104 + - uid: 1797 + components: + - type: Transform + pos: 3.5,-19.5 + parent: 104 + - uid: 1798 + components: + - type: Transform + pos: 3.5,-20.5 + parent: 104 + - uid: 1801 + components: + - type: Transform + pos: 7.5,-19.5 + parent: 104 + - uid: 1802 + components: + - type: Transform + pos: 7.5,-20.5 + parent: 104 + - uid: 1803 + components: + - type: Transform + pos: 7.5,-21.5 + parent: 104 + - uid: 1804 + components: + - type: Transform + pos: 5.5,-19.5 + parent: 104 + - uid: 1805 + components: + - type: Transform + pos: 5.5,-20.5 + parent: 104 + - uid: 1806 + components: + - type: Transform + pos: 5.5,-21.5 + parent: 104 + - uid: 1807 + components: + - type: Transform + pos: 6.5,-19.5 + parent: 104 + - uid: 1808 + components: + - type: Transform + pos: 6.5,-20.5 + parent: 104 + - uid: 1809 + components: + - type: Transform + pos: 6.5,-21.5 + parent: 104 + - uid: 1835 + components: + - type: Transform + pos: 8.5,-19.5 + parent: 104 + - uid: 1836 + components: + - type: Transform + pos: 8.5,-20.5 + parent: 104 + - uid: 1837 + components: + - type: Transform + pos: 8.5,-21.5 + parent: 104 + - uid: 1865 + components: + - type: Transform + pos: 16.5,-21.5 + parent: 104 + - uid: 1870 + components: + - type: Transform + pos: 17.5,-21.5 + parent: 104 + - uid: 1875 + components: + - type: Transform + pos: 18.5,-21.5 + parent: 104 + - uid: 1880 + components: + - type: Transform + pos: 19.5,-21.5 + parent: 104 + - uid: 1884 + components: + - type: Transform + pos: 20.5,-21.5 + parent: 104 + - uid: 1888 + components: + - type: Transform + pos: 21.5,-21.5 + parent: 104 + - uid: 1892 + components: + - type: Transform + pos: 22.5,-21.5 + parent: 104 + - uid: 1896 + components: + - type: Transform + pos: 23.5,-21.5 + parent: 104 + - uid: 1900 + components: + - type: Transform + pos: 24.5,-21.5 + parent: 104 + - uid: 1904 + components: + - type: Transform + pos: 25.5,-21.5 + parent: 104 + - uid: 1909 + components: + - type: Transform + pos: 28.5,-11.5 + parent: 104 + - uid: 1910 + components: + - type: Transform + pos: 28.5,-10.5 + parent: 104 + - uid: 1911 + components: + - type: Transform + pos: 28.5,-9.5 + parent: 104 + - uid: 1912 + components: + - type: Transform + pos: 28.5,-8.5 + parent: 104 + - uid: 1913 + components: + - type: Transform + pos: 26.5,-21.5 + parent: 104 + - uid: 1923 + components: + - type: Transform + pos: 26.5,-11.5 + parent: 104 + - uid: 1924 + components: + - type: Transform + pos: 26.5,-10.5 + parent: 104 + - uid: 1925 + components: + - type: Transform + pos: 26.5,-9.5 + parent: 104 + - uid: 1926 + components: + - type: Transform + pos: 26.5,-8.5 + parent: 104 + - uid: 1927 + components: + - type: Transform + pos: 26.5,-7.5 + parent: 104 + - uid: 1928 + components: + - type: Transform + pos: 26.5,-6.5 + parent: 104 + - uid: 1930 + components: + - type: Transform + pos: 26.5,-5.5 + parent: 104 + - uid: 1934 + components: + - type: Transform + pos: 26.5,-4.5 + parent: 104 + - uid: 1935 + components: + - type: Transform + pos: 26.5,-3.5 + parent: 104 + - uid: 1936 + components: + - type: Transform + pos: 26.5,-2.5 + parent: 104 + - uid: 1937 + components: + - type: Transform + pos: 26.5,-1.5 + parent: 104 + - uid: 1938 + components: + - type: Transform + pos: 26.5,-0.5 + parent: 104 + - uid: 1939 + components: + - type: Transform + pos: 26.5,0.5 + parent: 104 + - uid: 1940 + components: + - type: Transform + pos: 26.5,1.5 + parent: 104 + - uid: 1942 + components: + - type: Transform + pos: 27.5,-21.5 + parent: 104 + - uid: 1952 + components: + - type: Transform + pos: 27.5,-11.5 + parent: 104 + - uid: 1953 + components: + - type: Transform + pos: 27.5,-10.5 + parent: 104 + - uid: 1954 + components: + - type: Transform + pos: 27.5,-9.5 + parent: 104 + - uid: 1955 + components: + - type: Transform + pos: 27.5,-8.5 + parent: 104 + - uid: 1956 + components: + - type: Transform + pos: 27.5,-7.5 + parent: 104 + - uid: 1957 + components: + - type: Transform + pos: 27.5,-6.5 + parent: 104 + - uid: 1958 + components: + - type: Transform + pos: 27.5,-5.5 + parent: 104 + - uid: 1959 + components: + - type: Transform + pos: 27.5,-4.5 + parent: 104 + - uid: 1960 + components: + - type: Transform + pos: 27.5,-3.5 + parent: 104 + - uid: 1961 + components: + - type: Transform + pos: 27.5,-2.5 + parent: 104 + - uid: 1962 + components: + - type: Transform + pos: 27.5,-1.5 + parent: 104 + - uid: 1963 + components: + - type: Transform + pos: 27.5,-0.5 + parent: 104 + - uid: 1964 + components: + - type: Transform + pos: 27.5,0.5 + parent: 104 + - uid: 1965 + components: + - type: Transform + pos: 27.5,1.5 + parent: 104 + - uid: 1966 + components: + - type: Transform + pos: 27.5,2.5 + parent: 104 + - uid: 1967 + components: + - type: Transform + pos: 28.5,-21.5 + parent: 104 + - uid: 1973 + components: + - type: Transform + pos: 30.5,-1.5 + parent: 104 + - uid: 1974 + components: + - type: Transform + pos: 30.5,-0.5 + parent: 104 + - uid: 1975 + components: + - type: Transform + pos: 30.5,0.5 + parent: 104 + - uid: 1976 + components: + - type: Transform + pos: 30.5,1.5 + parent: 104 + - uid: 1977 + components: + - type: Transform + pos: 30.5,2.5 + parent: 104 + - uid: 1978 + components: + - type: Transform + pos: 28.5,-7.5 + parent: 104 + - uid: 1979 + components: + - type: Transform + pos: 28.5,-6.5 + parent: 104 + - uid: 1980 + components: + - type: Transform + pos: 28.5,-5.5 + parent: 104 + - uid: 1981 + components: + - type: Transform + pos: 28.5,-4.5 + parent: 104 + - uid: 1982 + components: + - type: Transform + pos: 28.5,-3.5 + parent: 104 + - uid: 1983 + components: + - type: Transform + pos: 28.5,-2.5 + parent: 104 + - uid: 1984 + components: + - type: Transform + pos: 28.5,-1.5 + parent: 104 + - uid: 1985 + components: + - type: Transform + pos: 28.5,-0.5 + parent: 104 + - uid: 1986 + components: + - type: Transform + pos: 28.5,0.5 + parent: 104 + - uid: 1987 + components: + - type: Transform + pos: 28.5,1.5 + parent: 104 + - uid: 1988 + components: + - type: Transform + pos: 28.5,2.5 + parent: 104 + - uid: 1989 + components: + - type: Transform + pos: 29.5,-20.5 + parent: 104 + - uid: 1990 + components: + - type: Transform + pos: 29.5,-19.5 + parent: 104 + - uid: 1991 + components: + - type: Transform + pos: 29.5,-18.5 + parent: 104 + - uid: 1992 + components: + - type: Transform + pos: 29.5,-17.5 + parent: 104 + - uid: 1993 + components: + - type: Transform + pos: 29.5,-16.5 + parent: 104 + - uid: 1994 + components: + - type: Transform + pos: 29.5,-15.5 + parent: 104 + - uid: 1995 + components: + - type: Transform + pos: 29.5,-14.5 + parent: 104 + - uid: 1996 + components: + - type: Transform + pos: 29.5,-13.5 + parent: 104 + - uid: 1997 + components: + - type: Transform + pos: 29.5,-12.5 + parent: 104 + - uid: 1998 + components: + - type: Transform + pos: 29.5,-11.5 + parent: 104 + - uid: 1999 + components: + - type: Transform + pos: 29.5,-10.5 + parent: 104 + - uid: 2000 + components: + - type: Transform + pos: 29.5,-9.5 + parent: 104 + - uid: 2001 + components: + - type: Transform + pos: 29.5,-8.5 + parent: 104 + - uid: 2002 + components: + - type: Transform + pos: 29.5,-7.5 + parent: 104 + - uid: 2003 + components: + - type: Transform + pos: 29.5,-6.5 + parent: 104 + - uid: 2004 + components: + - type: Transform + pos: 29.5,-5.5 + parent: 104 + - uid: 2005 + components: + - type: Transform + pos: 29.5,-4.5 + parent: 104 + - uid: 2006 + components: + - type: Transform + pos: 29.5,-3.5 + parent: 104 + - uid: 2007 + components: + - type: Transform + pos: 29.5,-2.5 + parent: 104 + - uid: 2008 + components: + - type: Transform + pos: 29.5,-1.5 + parent: 104 + - uid: 2009 + components: + - type: Transform + pos: 29.5,-0.5 + parent: 104 + - uid: 2010 + components: + - type: Transform + pos: 29.5,0.5 + parent: 104 + - uid: 2011 + components: + - type: Transform + pos: 29.5,1.5 + parent: 104 + - uid: 2012 + components: + - type: Transform + pos: 29.5,2.5 + parent: 104 + - uid: 2013 + components: + - type: Transform + pos: 30.5,-19.5 + parent: 104 + - uid: 2014 + components: + - type: Transform + pos: 30.5,-18.5 + parent: 104 + - uid: 2015 + components: + - type: Transform + pos: 30.5,-17.5 + parent: 104 + - uid: 2020 + components: + - type: Transform + pos: 30.5,-16.5 + parent: 104 + - uid: 2023 + components: + - type: Transform + pos: 30.5,-15.5 + parent: 104 + - uid: 2024 + components: + - type: Transform + pos: 30.5,-14.5 + parent: 104 + - uid: 2025 + components: + - type: Transform + pos: 30.5,-13.5 + parent: 104 + - uid: 2026 + components: + - type: Transform + pos: 30.5,-12.5 + parent: 104 + - uid: 2029 + components: + - type: Transform + pos: 30.5,-9.5 + parent: 104 + - uid: 2030 + components: + - type: Transform + pos: 30.5,-8.5 + parent: 104 + - uid: 2032 + components: + - type: Transform + pos: 30.5,-7.5 + parent: 104 + - uid: 2033 + components: + - type: Transform + pos: 30.5,-6.5 + parent: 104 + - uid: 2034 + components: + - type: Transform + pos: 30.5,-5.5 + parent: 104 + - uid: 2035 + components: + - type: Transform + pos: 30.5,-4.5 + parent: 104 + - uid: 2036 + components: + - type: Transform + pos: 30.5,-3.5 + parent: 104 + - uid: 2037 + components: + - type: Transform + pos: 30.5,-2.5 + parent: 104 + - uid: 2038 + components: + - type: Transform + pos: 20.5,-0.5 + parent: 104 + - uid: 2039 + components: + - type: Transform + pos: 20.5,-1.5 + parent: 104 + - uid: 2040 + components: + - type: Transform + pos: 15.5,2.5 + parent: 104 + - uid: 2041 + components: + - type: Transform + pos: 16.5,2.5 + parent: 104 + - uid: 2042 + components: + - type: Transform + pos: 19.5,0.5 + parent: 104 + - uid: 2043 + components: + - type: Transform + pos: 19.5,-0.5 + parent: 104 + - uid: 2044 + components: + - type: Transform + pos: 19.5,-1.5 + parent: 104 + - uid: 2047 + components: + - type: Transform + pos: 25.5,0.5 + parent: 104 + - uid: 2049 + components: + - type: Transform + pos: 25.5,-0.5 + parent: 104 + - uid: 2051 + components: + - type: Transform + pos: 25.5,-1.5 + parent: 104 + - uid: 2054 + components: + - type: Transform + pos: 24.5,0.5 + parent: 104 + - uid: 2055 + components: + - type: Transform + pos: 24.5,-0.5 + parent: 104 + - uid: 2056 + components: + - type: Transform + pos: 24.5,-1.5 + parent: 104 + - uid: 2058 + components: + - type: Transform + pos: 23.5,1.5 + parent: 104 + - uid: 2059 + components: + - type: Transform + pos: 23.5,0.5 + parent: 104 + - uid: 2060 + components: + - type: Transform + pos: 23.5,-0.5 + parent: 104 + - uid: 2061 + components: + - type: Transform + pos: 23.5,-1.5 + parent: 104 + - uid: 2063 + components: + - type: Transform + pos: 22.5,1.5 + parent: 104 + - uid: 2064 + components: + - type: Transform + pos: 22.5,0.5 + parent: 104 + - uid: 2065 + components: + - type: Transform + pos: 22.5,-0.5 + parent: 104 + - uid: 2066 + components: + - type: Transform + pos: 22.5,-1.5 + parent: 104 + - uid: 2067 + components: + - type: Transform + pos: 14.5,2.5 + parent: 104 + - uid: 2068 + components: + - type: Transform + pos: 15.5,1.5 + parent: 104 + - uid: 2069 + components: + - type: Transform + pos: 21.5,0.5 + parent: 104 + - uid: 2070 + components: + - type: Transform + pos: 21.5,-0.5 + parent: 104 + - uid: 2071 + components: + - type: Transform + pos: 21.5,-1.5 + parent: 104 + - uid: 2072 + components: + - type: Transform + pos: 14.5,3.5 + parent: 104 + - uid: 2073 + components: + - type: Transform + pos: 17.5,-0.5 + parent: 104 + - uid: 2074 + components: + - type: Transform + pos: 17.5,0.5 + parent: 104 + - uid: 2075 + components: + - type: Transform + pos: 17.5,1.5 + parent: 104 + - uid: 2076 + components: + - type: Transform + pos: 18.5,-1.5 + parent: 104 + - uid: 2077 + components: + - type: Transform + pos: 18.5,-0.5 + parent: 104 + - uid: 2078 + components: + - type: Transform + pos: 18.5,0.5 + parent: 104 + - uid: 2079 + components: + - type: Transform + pos: 18.5,1.5 + parent: 104 + - uid: 2080 + components: + - type: Transform + pos: -12.5,28.5 + parent: 104 + - uid: 2084 + components: + - type: Transform + pos: -12.5,27.5 + parent: 104 + - uid: 2085 + components: + - type: Transform + pos: -12.5,26.5 + parent: 104 + - uid: 2086 + components: + - type: Transform + pos: -12.5,25.5 + parent: 104 + - uid: 2087 + components: + - type: Transform + pos: -16.5,25.5 + parent: 104 + - uid: 2088 + components: + - type: Transform + pos: -3.5,-6.5 + parent: 104 + - uid: 2091 + components: + - type: Transform + pos: -15.5,25.5 + parent: 104 + - uid: 2093 + components: + - type: Transform + pos: -14.5,26.5 + parent: 104 + - uid: 2094 + components: + - type: Transform + pos: -14.5,25.5 + parent: 104 + - uid: 2096 + components: + - type: Transform + pos: -13.5,27.5 + parent: 104 + - uid: 2098 + components: + - type: Transform + pos: -13.5,26.5 + parent: 104 + - uid: 2099 + components: + - type: Transform + pos: -13.5,25.5 + parent: 104 + - uid: 2100 + components: + - type: Transform + pos: -20.5,30.5 + parent: 104 + - uid: 2101 + components: + - type: Transform + pos: -20.5,29.5 + parent: 104 + - uid: 2102 + components: + - type: Transform + pos: -20.5,28.5 + parent: 104 + - uid: 2103 + components: + - type: Transform + pos: -20.5,27.5 + parent: 104 + - uid: 2104 + components: + - type: Transform + pos: -20.5,26.5 + parent: 104 + - uid: 2105 + components: + - type: Transform + pos: -20.5,25.5 + parent: 104 + - uid: 2106 + components: + - type: Transform + pos: -20.5,24.5 + parent: 104 + - uid: 2107 + components: + - type: Transform + pos: -19.5,30.5 + parent: 104 + - uid: 2108 + components: + - type: Transform + pos: -19.5,29.5 + parent: 104 + - uid: 2109 + components: + - type: Transform + pos: -19.5,28.5 + parent: 104 + - uid: 2110 + components: + - type: Transform + pos: -19.5,27.5 + parent: 104 + - uid: 2111 + components: + - type: Transform + pos: -19.5,26.5 + parent: 104 + - uid: 2112 + components: + - type: Transform + pos: -19.5,25.5 + parent: 104 + - uid: 2113 + components: + - type: Transform + pos: -19.5,24.5 + parent: 104 + - uid: 2115 + components: + - type: Transform + pos: -18.5,30.5 + parent: 104 + - uid: 2116 + components: + - type: Transform + pos: -18.5,29.5 + parent: 104 + - uid: 2117 + components: + - type: Transform + pos: -18.5,28.5 + parent: 104 + - uid: 2118 + components: + - type: Transform + pos: -18.5,27.5 + parent: 104 + - uid: 2119 + components: + - type: Transform + pos: -18.5,26.5 + parent: 104 + - uid: 2120 + components: + - type: Transform + pos: -18.5,25.5 + parent: 104 + - uid: 2121 + components: + - type: Transform + pos: -18.5,24.5 + parent: 104 + - uid: 2122 + components: + - type: Transform + pos: -17.5,28.5 + parent: 104 + - uid: 2123 + components: + - type: Transform + pos: -17.5,27.5 + parent: 104 + - uid: 2124 + components: + - type: Transform + pos: -17.5,25.5 + parent: 104 + - uid: 2125 + components: + - type: Transform + pos: -11.5,28.5 + parent: 104 + - uid: 2127 + components: + - type: Transform + pos: -11.5,27.5 + parent: 104 + - uid: 2128 + components: + - type: Transform + pos: -11.5,26.5 + parent: 104 + - uid: 2129 + components: + - type: Transform + pos: -11.5,25.5 + parent: 104 + - uid: 2130 + components: + - type: Transform + pos: -11.5,24.5 + parent: 104 + - uid: 2131 + components: + - type: Transform + pos: -21.5,31.5 + parent: 104 + - uid: 2132 + components: + - type: Transform + pos: -20.5,31.5 + parent: 104 + - uid: 2133 + components: + - type: Transform + pos: -19.5,31.5 + parent: 104 + - uid: 2134 + components: + - type: Transform + pos: -10.5,28.5 + parent: 104 + - uid: 2136 + components: + - type: Transform + pos: -10.5,27.5 + parent: 104 + - uid: 2137 + components: + - type: Transform + pos: -10.5,26.5 + parent: 104 + - uid: 2138 + components: + - type: Transform + pos: -10.5,25.5 + parent: 104 + - uid: 2139 + components: + - type: Transform + pos: -10.5,24.5 + parent: 104 + - uid: 2140 + components: + - type: Transform + pos: -9.5,29.5 + parent: 104 + - uid: 2141 + components: + - type: Transform + pos: -9.5,28.5 + parent: 104 + - uid: 2142 + components: + - type: Transform + pos: -9.5,27.5 + parent: 104 + - uid: 2143 + components: + - type: Transform + pos: -9.5,26.5 + parent: 104 + - uid: 2144 + components: + - type: Transform + pos: -9.5,25.5 + parent: 104 + - uid: 2145 + components: + - type: Transform + pos: -9.5,24.5 + parent: 104 + - uid: 2146 + components: + - type: Transform + pos: -8.5,29.5 + parent: 104 + - uid: 2147 + components: + - type: Transform + pos: -8.5,28.5 + parent: 104 + - uid: 2148 + components: + - type: Transform + pos: -8.5,27.5 + parent: 104 + - uid: 2149 + components: + - type: Transform + pos: -8.5,26.5 + parent: 104 + - uid: 2150 + components: + - type: Transform + pos: -8.5,25.5 + parent: 104 + - uid: 2151 + components: + - type: Transform + pos: -8.5,24.5 + parent: 104 + - uid: 2152 + components: + - type: Transform + pos: -7.5,29.5 + parent: 104 + - uid: 2153 + components: + - type: Transform + pos: -7.5,28.5 + parent: 104 + - uid: 2154 + components: + - type: Transform + pos: -7.5,27.5 + parent: 104 + - uid: 2155 + components: + - type: Transform + pos: -7.5,26.5 + parent: 104 + - uid: 2156 + components: + - type: Transform + pos: -7.5,25.5 + parent: 104 + - uid: 2157 + components: + - type: Transform + pos: -7.5,24.5 + parent: 104 + - uid: 2158 + components: + - type: Transform + pos: -6.5,29.5 + parent: 104 + - uid: 2159 + components: + - type: Transform + pos: -6.5,28.5 + parent: 104 + - uid: 2160 + components: + - type: Transform + pos: -6.5,27.5 + parent: 104 + - uid: 2161 + components: + - type: Transform + pos: -6.5,26.5 + parent: 104 + - uid: 2162 + components: + - type: Transform + pos: -6.5,25.5 + parent: 104 + - uid: 2163 + components: + - type: Transform + pos: -6.5,24.5 + parent: 104 + - uid: 2164 + components: + - type: Transform + pos: -5.5,29.5 + parent: 104 + - uid: 2165 + components: + - type: Transform + pos: -5.5,28.5 + parent: 104 + - uid: 2166 + components: + - type: Transform + pos: -5.5,27.5 + parent: 104 + - uid: 2167 + components: + - type: Transform + pos: -5.5,26.5 + parent: 104 + - uid: 2168 + components: + - type: Transform + pos: -5.5,25.5 + parent: 104 + - uid: 2169 + components: + - type: Transform + pos: -5.5,24.5 + parent: 104 + - uid: 2170 + components: + - type: Transform + pos: -4.5,29.5 + parent: 104 + - uid: 2171 + components: + - type: Transform + pos: -4.5,28.5 + parent: 104 + - uid: 2172 + components: + - type: Transform + pos: -4.5,27.5 + parent: 104 + - uid: 2173 + components: + - type: Transform + pos: -4.5,26.5 + parent: 104 + - uid: 2174 + components: + - type: Transform + pos: -4.5,25.5 + parent: 104 + - uid: 2175 + components: + - type: Transform + pos: -4.5,24.5 + parent: 104 + - uid: 2176 + components: + - type: Transform + pos: -3.5,28.5 + parent: 104 + - uid: 2177 + components: + - type: Transform + pos: -3.5,27.5 + parent: 104 + - uid: 2178 + components: + - type: Transform + pos: -3.5,26.5 + parent: 104 + - uid: 2179 + components: + - type: Transform + pos: -3.5,25.5 + parent: 104 + - uid: 2180 + components: + - type: Transform + pos: -3.5,24.5 + parent: 104 + - uid: 2181 + components: + - type: Transform + pos: -2.5,26.5 + parent: 104 + - uid: 2183 + components: + - type: Transform + pos: -2.5,25.5 + parent: 104 + - uid: 2184 + components: + - type: Transform + pos: -2.5,24.5 + parent: 104 + - uid: 2192 + components: + - type: Transform + pos: -5.5,-23.5 + parent: 104 + - uid: 2194 + components: + - type: Transform + pos: 15.5,29.5 + parent: 104 + - uid: 2195 + components: + - type: Transform + pos: 10.5,24.5 + parent: 104 + - uid: 2196 + components: + - type: Transform + pos: 15.5,31.5 + parent: 104 + - uid: 2197 + components: + - type: Transform + pos: 16.5,26.5 + parent: 104 + - uid: 2198 + components: + - type: Transform + pos: 15.5,30.5 + parent: 104 + - uid: 2199 + components: + - type: Transform + pos: 15.5,32.5 + parent: 104 + - uid: 2200 + components: + - type: Transform + pos: 16.5,23.5 + parent: 104 + - uid: 2201 + components: + - type: Transform + pos: 16.5,24.5 + parent: 104 + - uid: 2202 + components: + - type: Transform + pos: 16.5,25.5 + parent: 104 + - uid: 2203 + components: + - type: Transform + pos: 10.5,23.5 + parent: 104 + - uid: 2204 + components: + - type: Transform + pos: 10.5,25.5 + parent: 104 + - uid: 2205 + components: + - type: Transform + pos: 10.5,26.5 + parent: 104 + - uid: 2206 + components: + - type: Transform + pos: 10.5,27.5 + parent: 104 + - uid: 2207 + components: + - type: Transform + pos: 10.5,28.5 + parent: 104 + - uid: 2208 + components: + - type: Transform + pos: 11.5,23.5 + parent: 104 + - uid: 2209 + components: + - type: Transform + pos: 11.5,24.5 + parent: 104 + - uid: 2210 + components: + - type: Transform + pos: 11.5,25.5 + parent: 104 + - uid: 2211 + components: + - type: Transform + pos: 11.5,26.5 + parent: 104 + - uid: 2212 + components: + - type: Transform + pos: 11.5,27.5 + parent: 104 + - uid: 2213 + components: + - type: Transform + pos: 11.5,28.5 + parent: 104 + - uid: 2214 + components: + - type: Transform + pos: 11.5,29.5 + parent: 104 + - uid: 2215 + components: + - type: Transform + pos: 12.5,23.5 + parent: 104 + - uid: 2216 + components: + - type: Transform + pos: 12.5,24.5 + parent: 104 + - uid: 2217 + components: + - type: Transform + pos: 12.5,25.5 + parent: 104 + - uid: 2218 + components: + - type: Transform + pos: 12.5,26.5 + parent: 104 + - uid: 2219 + components: + - type: Transform + pos: 12.5,27.5 + parent: 104 + - uid: 2220 + components: + - type: Transform + pos: 12.5,28.5 + parent: 104 + - uid: 2221 + components: + - type: Transform + pos: 12.5,29.5 + parent: 104 + - uid: 2222 + components: + - type: Transform + pos: 12.5,30.5 + parent: 104 + - uid: 2223 + components: + - type: Transform + pos: 12.5,31.5 + parent: 104 + - uid: 2224 + components: + - type: Transform + pos: 13.5,23.5 + parent: 104 + - uid: 2225 + components: + - type: Transform + pos: 13.5,24.5 + parent: 104 + - uid: 2226 + components: + - type: Transform + pos: 13.5,25.5 + parent: 104 + - uid: 2227 + components: + - type: Transform + pos: 13.5,26.5 + parent: 104 + - uid: 2228 + components: + - type: Transform + pos: 13.5,27.5 + parent: 104 + - uid: 2229 + components: + - type: Transform + pos: 13.5,28.5 + parent: 104 + - uid: 2230 + components: + - type: Transform + pos: 13.5,29.5 + parent: 104 + - uid: 2231 + components: + - type: Transform + pos: 13.5,30.5 + parent: 104 + - uid: 2232 + components: + - type: Transform + pos: 13.5,31.5 + parent: 104 + - uid: 2233 + components: + - type: Transform + pos: 14.5,23.5 + parent: 104 + - uid: 2234 + components: + - type: Transform + pos: 14.5,24.5 + parent: 104 + - uid: 2235 + components: + - type: Transform + pos: 14.5,25.5 + parent: 104 + - uid: 2236 + components: + - type: Transform + pos: 14.5,26.5 + parent: 104 + - uid: 2237 + components: + - type: Transform + pos: 14.5,27.5 + parent: 104 + - uid: 2238 + components: + - type: Transform + pos: 14.5,28.5 + parent: 104 + - uid: 2239 + components: + - type: Transform + pos: 14.5,29.5 + parent: 104 + - uid: 2240 + components: + - type: Transform + pos: 14.5,30.5 + parent: 104 + - uid: 2241 + components: + - type: Transform + pos: 14.5,31.5 + parent: 104 + - uid: 2242 + components: + - type: Transform + pos: 14.5,32.5 + parent: 104 + - uid: 2243 + components: + - type: Transform + pos: 15.5,23.5 + parent: 104 + - uid: 2244 + components: + - type: Transform + pos: 15.5,24.5 + parent: 104 + - uid: 2245 + components: + - type: Transform + pos: 15.5,25.5 + parent: 104 + - uid: 2246 + components: + - type: Transform + pos: 15.5,26.5 + parent: 104 + - uid: 2247 + components: + - type: Transform + pos: 15.5,27.5 + parent: 104 + - uid: 2248 + components: + - type: Transform + pos: 15.5,28.5 + parent: 104 + - uid: 2249 + components: + - type: Transform + pos: 19.5,25.5 + parent: 104 + - uid: 2250 + components: + - type: Transform + pos: 19.5,26.5 + parent: 104 + - uid: 2251 + components: + - type: Transform + pos: 19.5,27.5 + parent: 104 + - uid: 2252 + components: + - type: Transform + pos: 19.5,28.5 + parent: 104 + - uid: 2253 + components: + - type: Transform + pos: 19.5,29.5 + parent: 104 + - uid: 2254 + components: + - type: Transform + pos: 19.5,30.5 + parent: 104 + - uid: 2255 + components: + - type: Transform + pos: 19.5,31.5 + parent: 104 + - uid: 2256 + components: + - type: Transform + pos: 20.5,26.5 + parent: 104 + - uid: 2259 + components: + - type: Transform + pos: 20.5,27.5 + parent: 104 + - uid: 2260 + components: + - type: Transform + pos: 20.5,28.5 + parent: 104 + - uid: 2261 + components: + - type: Transform + pos: 20.5,29.5 + parent: 104 + - uid: 2262 + components: + - type: Transform + pos: 16.5,27.5 + parent: 104 + - uid: 2263 + components: + - type: Transform + pos: 16.5,28.5 + parent: 104 + - uid: 2264 + components: + - type: Transform + pos: 16.5,29.5 + parent: 104 + - uid: 2265 + components: + - type: Transform + pos: 16.5,30.5 + parent: 104 + - uid: 2266 + components: + - type: Transform + pos: 16.5,31.5 + parent: 104 + - uid: 2267 + components: + - type: Transform + pos: 16.5,32.5 + parent: 104 + - uid: 2268 + components: + - type: Transform + pos: 17.5,23.5 + parent: 104 + - uid: 2269 + components: + - type: Transform + pos: 17.5,24.5 + parent: 104 + - uid: 2270 + components: + - type: Transform + pos: 17.5,25.5 + parent: 104 + - uid: 2271 + components: + - type: Transform + pos: 17.5,26.5 + parent: 104 + - uid: 2272 + components: + - type: Transform + pos: 17.5,27.5 + parent: 104 + - uid: 2273 + components: + - type: Transform + pos: 17.5,28.5 + parent: 104 + - uid: 2274 + components: + - type: Transform + pos: 17.5,29.5 + parent: 104 + - uid: 2275 + components: + - type: Transform + pos: 17.5,30.5 + parent: 104 + - uid: 2276 + components: + - type: Transform + pos: 17.5,31.5 + parent: 104 + - uid: 2277 + components: + - type: Transform + pos: 17.5,32.5 + parent: 104 + - uid: 2278 + components: + - type: Transform + pos: 18.5,23.5 + parent: 104 + - uid: 2279 + components: + - type: Transform + pos: 18.5,24.5 + parent: 104 + - uid: 2280 + components: + - type: Transform + pos: 18.5,25.5 + parent: 104 + - uid: 2281 + components: + - type: Transform + pos: 18.5,26.5 + parent: 104 + - uid: 2282 + components: + - type: Transform + pos: 18.5,27.5 + parent: 104 + - uid: 2283 + components: + - type: Transform + pos: 18.5,28.5 + parent: 104 + - uid: 2284 + components: + - type: Transform + pos: 18.5,29.5 + parent: 104 + - uid: 2285 + components: + - type: Transform + pos: 18.5,30.5 + parent: 104 + - uid: 2286 + components: + - type: Transform + pos: 18.5,31.5 + parent: 104 + - uid: 2287 + components: + - type: Transform + pos: 18.5,32.5 + parent: 104 + - uid: 2288 + components: + - type: Transform + pos: 19.5,23.5 + parent: 104 + - uid: 2289 + components: + - type: Transform + pos: 19.5,24.5 + parent: 104 +- proto: BalloonSyn + entities: + - uid: 1327 + components: + - type: Transform + pos: 0.03361702,-6.368435 + parent: 104 +- proto: BaseComputer + entities: + - uid: 18 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-8.5 + parent: 104 + - uid: 29 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 1.5,-8.5 + parent: 104 + - uid: 111 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-1.5 + parent: 104 + - uid: 2292 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-2.5 + parent: 104 + - uid: 2295 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-3.5 + parent: 104 +- proto: Beaker + entities: + - uid: 2031 + components: + - type: Transform + pos: 3.6114278,-10.732791 + parent: 104 + - uid: 2095 + components: + - type: Transform + pos: 4.017678,-12.520861 + parent: 104 +- proto: Bed + entities: + - uid: 957 + components: + - type: Transform + pos: 15.5,-3.5 + parent: 104 + - uid: 1548 + components: + - type: Transform + pos: 15.5,-4.5 + parent: 104 +- proto: BedsheetMedical + entities: + - uid: 1872 + components: + - type: Transform + pos: 1.5,-12.5 + parent: 104 + - uid: 1877 + components: + - type: Transform + pos: -1.5,-16.5 + parent: 104 + - uid: 1898 + components: + - type: Transform + pos: 1.5,-16.5 + parent: 104 + - uid: 1899 + components: + - type: Transform + pos: -1.5,-12.5 + parent: 104 +- proto: BedsheetSyndie + entities: + - uid: 154 + components: + - type: Transform + pos: 15.5,-3.5 + parent: 104 + - uid: 289 + components: + - type: Transform + pos: 15.5,-4.5 + parent: 104 +- proto: BigBox + entities: + - uid: 2458 + components: + - type: Transform + pos: 13.500806,-4.4874377 + parent: 104 +- proto: Bookshelf + entities: + - uid: 140 + components: + - type: Transform + pos: -0.5,-0.5 + parent: 104 +- proto: BoozeDispenser + entities: + - uid: 230 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 2.5,-8.5 + parent: 104 +- proto: BoxBeaker + entities: + - uid: 2114 + components: + - type: Transform + pos: 5.539936,-12.489611 + parent: 104 +- proto: BoxFolderBlack + entities: + - uid: 1022 + components: + - type: Transform + pos: -13.494709,9.54891 + parent: 104 +- proto: BoxMagazinePistolCaselessRiflePractice + entities: + - uid: 1635 + components: + - type: Transform + pos: 5.4970627,10.597828 + parent: 104 +- proto: BoxMagazinePistolSubMachineGunPractice + entities: + - uid: 1634 + components: + - type: Transform + pos: 5.388983,13.953581 + parent: 104 +- proto: BoxPillCanister + entities: + - uid: 1844 + components: + - type: Transform + pos: 5.430561,-12.364611 + parent: 104 +- proto: BoxShotgunPractice + entities: + - uid: 1633 + components: + - type: Transform + pos: 5.4972115,14.562988 + parent: 104 + - type: BallisticAmmoProvider + unspawnedCount: 12 +- proto: Bucket + entities: + - uid: 153 + components: + - type: Transform + pos: 10.809023,-4.25956 + parent: 104 +- proto: CableApcExtension + entities: + - uid: 13 + components: + - type: Transform + pos: -10.5,-5.5 + parent: 104 + - uid: 24 + components: + - type: Transform + pos: -31.5,0.5 + parent: 104 + - uid: 100 + components: + - type: Transform + pos: 13.5,0.5 + parent: 104 + - uid: 143 + components: + - type: Transform + pos: 12.5,0.5 + parent: 104 + - uid: 213 + components: + - type: Transform + pos: -16.5,-19.5 + parent: 104 + - uid: 214 + components: + - type: Transform + pos: -15.5,-19.5 + parent: 104 + - uid: 220 + components: + - type: Transform + pos: 11.5,0.5 + parent: 104 + - uid: 235 + components: + - type: Transform + pos: 1.5,-13.5 + parent: 104 + - uid: 245 + components: + - type: Transform + pos: 12.5,-18.5 + parent: 104 + - uid: 254 + components: + - type: Transform + pos: -30.5,0.5 + parent: 104 + - uid: 279 + components: + - type: Transform + pos: 11.5,-0.5 + parent: 104 + - uid: 287 + components: + - type: Transform + pos: -15.5,0.5 + parent: 104 + - uid: 297 + components: + - type: Transform + pos: 3.5,-14.5 + parent: 104 + - uid: 390 + components: + - type: Transform + pos: -10.5,-7.5 + parent: 104 + - uid: 397 + components: + - type: Transform + pos: -10.5,0.5 + parent: 104 + - uid: 411 + components: + - type: Transform + pos: -10.5,-8.5 + parent: 104 + - uid: 412 + components: + - type: Transform + pos: -26.5,-19.5 + parent: 104 + - uid: 428 + components: + - type: Transform + pos: -23.5,-19.5 + parent: 104 + - uid: 429 + components: + - type: Transform + pos: -10.5,-0.5 + parent: 104 + - uid: 430 + components: + - type: Transform + pos: -10.5,1.5 + parent: 104 + - uid: 432 + components: + - type: Transform + pos: -27.5,0.5 + parent: 104 + - uid: 433 + components: + - type: Transform + pos: -17.5,-19.5 + parent: 104 + - uid: 434 + components: + - type: Transform + pos: -21.5,-19.5 + parent: 104 + - uid: 435 + components: + - type: Transform + pos: -10.5,-4.5 + parent: 104 + - uid: 436 + components: + - type: Transform + pos: -10.5,-6.5 + parent: 104 + - uid: 437 + components: + - type: Transform + pos: -10.5,-9.5 + parent: 104 + - uid: 438 + components: + - type: Transform + pos: -20.5,-19.5 + parent: 104 + - uid: 439 + components: + - type: Transform + pos: -10.5,-3.5 + parent: 104 + - uid: 442 + components: + - type: Transform + pos: -19.5,-19.5 + parent: 104 + - uid: 444 + components: + - type: Transform + pos: -10.5,1.5 + parent: 104 + - uid: 445 + components: + - type: Transform + pos: -26.5,0.5 + parent: 104 + - uid: 449 + components: + - type: Transform + pos: -18.5,-19.5 + parent: 104 + - uid: 452 + components: + - type: Transform + pos: -30.5,-19.5 + parent: 104 + - uid: 453 + components: + - type: Transform + pos: -29.5,-19.5 + parent: 104 + - uid: 454 + components: + - type: Transform + pos: -23.5,0.5 + parent: 104 + - uid: 465 + components: + - type: Transform + pos: -28.5,-19.5 + parent: 104 + - uid: 466 + components: + - type: Transform + pos: -27.5,-19.5 + parent: 104 + - uid: 467 + components: + - type: Transform + pos: -24.5,-19.5 + parent: 104 + - uid: 476 + components: + - type: Transform + pos: -22.5,-19.5 + parent: 104 + - uid: 619 + components: + - type: Transform + pos: -10.5,-12.5 + parent: 104 + - uid: 624 + components: + - type: Transform + pos: -10.5,-10.5 + parent: 104 + - uid: 625 + components: + - type: Transform + pos: -10.5,-11.5 + parent: 104 + - uid: 627 + components: + - type: Transform + pos: -10.5,-15.5 + parent: 104 + - uid: 628 + components: + - type: Transform + pos: -10.5,-16.5 + parent: 104 + - uid: 630 + components: + - type: Transform + pos: -10.5,-17.5 + parent: 104 + - uid: 657 + components: + - type: Transform + pos: -25.5,-19.5 + parent: 104 + - uid: 659 + components: + - type: Transform + pos: -32.5,-19.5 + parent: 104 + - uid: 660 + components: + - type: Transform + pos: -31.5,-19.5 + parent: 104 + - uid: 666 + components: + - type: Transform + pos: -22.5,0.5 + parent: 104 + - uid: 814 + components: + - type: Transform + pos: -6.5,-3.5 + parent: 104 + - uid: 832 + components: + - type: Transform + pos: -7.5,-3.5 + parent: 104 + - uid: 840 + components: + - type: Transform + pos: -25.5,0.5 + parent: 104 + - uid: 845 + components: + - type: Transform + pos: -10.5,-1.5 + parent: 104 + - uid: 866 + components: + - type: Transform + pos: -10.5,-2.5 + parent: 104 + - uid: 867 + components: + - type: Transform + pos: -5.5,-3.5 + parent: 104 + - uid: 868 + components: + - type: Transform + pos: -10.5,-3.5 + parent: 104 + - uid: 870 + components: + - type: Transform + pos: -9.5,-3.5 + parent: 104 + - uid: 877 + components: + - type: Transform + pos: -8.5,-3.5 + parent: 104 + - uid: 884 + components: + - type: Transform + pos: -4.5,-3.5 + parent: 104 + - uid: 920 + components: + - type: Transform + pos: -10.5,-13.5 + parent: 104 + - uid: 926 + components: + - type: Transform + pos: -12.5,0.5 + parent: 104 + - uid: 927 + components: + - type: Transform + pos: -12.5,-19.5 + parent: 104 + - uid: 929 + components: + - type: Transform + pos: -24.5,0.5 + parent: 104 + - uid: 931 + components: + - type: Transform + pos: 12.5,-19.5 + parent: 104 + - uid: 933 + components: + - type: Transform + pos: -10.5,-19.5 + parent: 104 + - uid: 934 + components: + - type: Transform + pos: -10.5,-14.5 + parent: 104 + - uid: 960 + components: + - type: Transform + pos: 12.5,-17.5 + parent: 104 + - uid: 961 + components: + - type: Transform + pos: 11.5,-16.5 + parent: 104 + - uid: 962 + components: + - type: Transform + pos: 6.5,-14.5 + parent: 104 + - uid: 963 + components: + - type: Transform + pos: -14.5,0.5 + parent: 104 + - uid: 967 + components: + - type: Transform + pos: 9.5,-16.5 + parent: 104 + - uid: 968 + components: + - type: Transform + pos: 7.5,-16.5 + parent: 104 + - uid: 969 + components: + - type: Transform + pos: -10.5,-18.5 + parent: 104 + - uid: 970 + components: + - type: Transform + pos: -33.5,-19.5 + parent: 104 + - uid: 971 + components: + - type: Transform + pos: -33.5,0.5 + parent: 104 + - uid: 973 + components: + - type: Transform + pos: 5.5,-17.5 + parent: 104 + - uid: 979 + components: + - type: Transform + pos: 1.5,-16.5 + parent: 104 + - uid: 981 + components: + - type: Transform + pos: 5.5,-14.5 + parent: 104 + - uid: 982 + components: + - type: Transform + pos: 12.5,-16.5 + parent: 104 + - uid: 983 + components: + - type: Transform + pos: 5.5,-16.5 + parent: 104 + - uid: 1081 + components: + - type: Transform + pos: -17.5,0.5 + parent: 104 + - uid: 1082 + components: + - type: Transform + pos: -11.5,0.5 + parent: 104 + - uid: 1115 + components: + - type: Transform + pos: 4.5,-14.5 + parent: 104 + - uid: 1116 + components: + - type: Transform + pos: 1.5,-12.5 + parent: 104 + - uid: 1117 + components: + - type: Transform + pos: 6.5,-16.5 + parent: 104 + - uid: 1122 + components: + - type: Transform + pos: 1.5,-14.5 + parent: 104 + - uid: 1131 + components: + - type: Transform + pos: -13.5,0.5 + parent: 104 + - uid: 1152 + components: + - type: Transform + pos: 12.5,-20.5 + parent: 104 + - uid: 1154 + components: + - type: Transform + pos: 3.5,-13.5 + parent: 104 + - uid: 1182 + components: + - type: Transform + pos: 5.5,-15.5 + parent: 104 + - uid: 1183 + components: + - type: Transform + pos: 7.5,-14.5 + parent: 104 + - uid: 1187 + components: + - type: Transform + pos: -20.5,0.5 + parent: 104 + - uid: 1189 + components: + - type: Transform + pos: -32.5,0.5 + parent: 104 + - uid: 1191 + components: + - type: Transform + pos: 3.5,-12.5 + parent: 104 + - uid: 1194 + components: + - type: Transform + pos: 2.5,-14.5 + parent: 104 + - uid: 1195 + components: + - type: Transform + pos: 8.5,-16.5 + parent: 104 + - uid: 1200 + components: + - type: Transform + pos: 1.5,-15.5 + parent: 104 + - uid: 1204 + components: + - type: Transform + pos: 10.5,-16.5 + parent: 104 + - uid: 1232 + components: + - type: Transform + pos: -10.5,6.5 + parent: 104 + - uid: 1233 + components: + - type: Transform + pos: -10.5,5.5 + parent: 104 + - uid: 1234 + components: + - type: Transform + pos: -10.5,4.5 + parent: 104 + - uid: 1235 + components: + - type: Transform + pos: -10.5,3.5 + parent: 104 + - uid: 1236 + components: + - type: Transform + pos: -10.5,2.5 + parent: 104 + - uid: 1237 + components: + - type: Transform + pos: -11.5,6.5 + parent: 104 + - uid: 1238 + components: + - type: Transform + pos: -12.5,6.5 + parent: 104 + - uid: 1239 + components: + - type: Transform + pos: -13.5,6.5 + parent: 104 + - uid: 1240 + components: + - type: Transform + pos: -14.5,6.5 + parent: 104 + - uid: 1249 + components: + - type: Transform + pos: -11.5,-19.5 + parent: 104 + - uid: 1275 + components: + - type: Transform + pos: -6.5,-2.5 + parent: 104 + - uid: 1277 + components: + - type: Transform + pos: -21.5,0.5 + parent: 104 + - uid: 1278 + components: + - type: Transform + pos: -16.5,0.5 + parent: 104 + - uid: 1281 + components: + - type: Transform + pos: -6.5,-1.5 + parent: 104 + - uid: 1282 + components: + - type: Transform + pos: -6.5,-0.5 + parent: 104 + - uid: 1283 + components: + - type: Transform + pos: -6.5,0.5 + parent: 104 + - uid: 1292 + components: + - type: Transform + pos: 3.5,1.5 + parent: 104 + - uid: 1337 + components: + - type: Transform + pos: -6.5,-4.5 + parent: 104 + - uid: 1338 + components: + - type: Transform + pos: -6.5,-5.5 + parent: 104 + - uid: 1339 + components: + - type: Transform + pos: -6.5,-6.5 + parent: 104 + - uid: 1340 + components: + - type: Transform + pos: -6.5,-7.5 + parent: 104 + - uid: 1341 + components: + - type: Transform + pos: -6.5,-8.5 + parent: 104 + - uid: 1342 + components: + - type: Transform + pos: -6.5,-9.5 + parent: 104 + - uid: 1343 + components: + - type: Transform + pos: -6.5,-10.5 + parent: 104 + - uid: 1344 + components: + - type: Transform + pos: -6.5,-11.5 + parent: 104 + - uid: 1345 + components: + - type: Transform + pos: -6.5,-12.5 + parent: 104 + - uid: 1346 + components: + - type: Transform + pos: -6.5,-13.5 + parent: 104 + - uid: 1347 + components: + - type: Transform + pos: -6.5,-14.5 + parent: 104 + - uid: 1348 + components: + - type: Transform + pos: -6.5,-15.5 + parent: 104 + - uid: 1349 + components: + - type: Transform + pos: -6.5,-16.5 + parent: 104 + - uid: 1429 + components: + - type: Transform + pos: -18.5,0.5 + parent: 104 + - uid: 1430 + components: + - type: Transform + pos: -19.5,0.5 + parent: 104 + - uid: 1439 + components: + - type: Transform + pos: 20.5,-12.5 + parent: 104 + - uid: 1449 + components: + - type: Transform + pos: 16.5,-13.5 + parent: 104 + - uid: 1450 + components: + - type: Transform + pos: 15.5,-13.5 + parent: 104 + - uid: 1451 + components: + - type: Transform + pos: 15.5,-12.5 + parent: 104 + - uid: 1452 + components: + - type: Transform + pos: 15.5,-11.5 + parent: 104 + - uid: 1453 + components: + - type: Transform + pos: 15.5,-10.5 + parent: 104 + - uid: 1454 + components: + - type: Transform + pos: 14.5,-10.5 + parent: 104 + - uid: 1455 + components: + - type: Transform + pos: 13.5,-10.5 + parent: 104 + - uid: 1456 + components: + - type: Transform + pos: 12.5,-10.5 + parent: 104 + - uid: 1457 + components: + - type: Transform + pos: 11.5,-10.5 + parent: 104 + - uid: 1458 + components: + - type: Transform + pos: 15.5,-14.5 + parent: 104 + - uid: 1459 + components: + - type: Transform + pos: 14.5,-14.5 + parent: 104 + - uid: 1460 + components: + - type: Transform + pos: 13.5,-14.5 + parent: 104 + - uid: 1461 + components: + - type: Transform + pos: 12.5,-14.5 + parent: 104 + - uid: 1473 + components: + - type: Transform + pos: -13.5,-19.5 + parent: 104 + - uid: 1474 + components: + - type: Transform + pos: 7.5,-5.5 + parent: 104 + - uid: 1475 + components: + - type: Transform + pos: 6.5,-1.5 + parent: 104 + - uid: 1476 + components: + - type: Transform + pos: 3.5,0.5 + parent: 104 + - uid: 1488 + components: + - type: Transform + pos: 6.5,-5.5 + parent: 104 + - uid: 1489 + components: + - type: Transform + pos: 8.5,-5.5 + parent: 104 + - uid: 1490 + components: + - type: Transform + pos: 6.5,-4.5 + parent: 104 + - uid: 1491 + components: + - type: Transform + pos: 6.5,-3.5 + parent: 104 + - uid: 1492 + components: + - type: Transform + pos: 6.5,-2.5 + parent: 104 + - uid: 1493 + components: + - type: Transform + pos: 6.5,-0.5 + parent: 104 + - uid: 1494 + components: + - type: Transform + pos: 5.5,-0.5 + parent: 104 + - uid: 1495 + components: + - type: Transform + pos: 4.5,-0.5 + parent: 104 + - uid: 1496 + components: + - type: Transform + pos: 4.5,0.5 + parent: 104 + - uid: 1497 + components: + - type: Transform + pos: 2.5,0.5 + parent: 104 + - uid: 1498 + components: + - type: Transform + pos: 2.5,-0.5 + parent: 104 + - uid: 1499 + components: + - type: Transform + pos: 1.5,-0.5 + parent: 104 + - uid: 1500 + components: + - type: Transform + pos: 0.5,-0.5 + parent: 104 + - uid: 1501 + components: + - type: Transform + pos: 0.5,-1.5 + parent: 104 + - uid: 1502 + components: + - type: Transform + pos: -0.5,-1.5 + parent: 104 + - uid: 1503 + components: + - type: Transform + pos: -0.5,-1.5 + parent: 104 + - uid: 1504 + components: + - type: Transform + pos: -0.5,-2.5 + parent: 104 + - uid: 1505 + components: + - type: Transform + pos: -0.5,-3.5 + parent: 104 + - uid: 1506 + components: + - type: Transform + pos: -0.5,-4.5 + parent: 104 + - uid: 1507 + components: + - type: Transform + pos: -0.5,-5.5 + parent: 104 + - uid: 1508 + components: + - type: Transform + pos: -0.5,-6.5 + parent: 104 + - uid: 1509 + components: + - type: Transform + pos: -0.5,-7.5 + parent: 104 + - uid: 1510 + components: + - type: Transform + pos: 0.5,-7.5 + parent: 104 + - uid: 1511 + components: + - type: Transform + pos: 1.5,-7.5 + parent: 104 + - uid: 1512 + components: + - type: Transform + pos: 2.5,-7.5 + parent: 104 + - uid: 1513 + components: + - type: Transform + pos: 4.5,-7.5 + parent: 104 + - uid: 1514 + components: + - type: Transform + pos: 3.5,-7.5 + parent: 104 + - uid: 1515 + components: + - type: Transform + pos: 5.5,-7.5 + parent: 104 + - uid: 1516 + components: + - type: Transform + pos: 5.5,-8.5 + parent: 104 + - uid: 1517 + components: + - type: Transform + pos: 6.5,-8.5 + parent: 104 + - uid: 1518 + components: + - type: Transform + pos: 7.5,-8.5 + parent: 104 + - uid: 1519 + components: + - type: Transform + pos: 8.5,-8.5 + parent: 104 + - uid: 1520 + components: + - type: Transform + pos: 9.5,-8.5 + parent: 104 + - uid: 1521 + components: + - type: Transform + pos: 10.5,-8.5 + parent: 104 + - uid: 1522 + components: + - type: Transform + pos: 10.5,-7.5 + parent: 104 + - uid: 1523 + components: + - type: Transform + pos: 11.5,-7.5 + parent: 104 + - uid: 1524 + components: + - type: Transform + pos: 12.5,-7.5 + parent: 104 + - uid: 1525 + components: + - type: Transform + pos: 13.5,-7.5 + parent: 104 + - uid: 1526 + components: + - type: Transform + pos: 14.5,-7.5 + parent: 104 + - uid: 1527 + components: + - type: Transform + pos: 14.5,-7.5 + parent: 104 + - uid: 1528 + components: + - type: Transform + pos: 15.5,-7.5 + parent: 104 + - uid: 1529 + components: + - type: Transform + pos: 14.5,-6.5 + parent: 104 + - uid: 1530 + components: + - type: Transform + pos: 14.5,-5.5 + parent: 104 + - uid: 1531 + components: + - type: Transform + pos: 14.5,-4.5 + parent: 104 + - uid: 1533 + components: + - type: Transform + pos: -29.5,0.5 + parent: 104 + - uid: 1538 + components: + - type: Transform + pos: 7.5,-0.5 + parent: 104 + - uid: 1539 + components: + - type: Transform + pos: 8.5,-0.5 + parent: 104 + - uid: 1540 + components: + - type: Transform + pos: 9.5,-0.5 + parent: 104 + - uid: 1541 + components: + - type: Transform + pos: 8.5,-0.5 + parent: 104 + - uid: 1543 + components: + - type: Transform + pos: 10.5,-0.5 + parent: 104 + - uid: 1575 + components: + - type: Transform + pos: 21.5,-5.5 + parent: 104 + - uid: 1576 + components: + - type: Transform + pos: 20.5,-5.5 + parent: 104 + - uid: 1577 + components: + - type: Transform + pos: 19.5,-5.5 + parent: 104 + - uid: 1578 + components: + - type: Transform + pos: 19.5,-6.5 + parent: 104 + - uid: 1579 + components: + - type: Transform + pos: 19.5,-7.5 + parent: 104 + - uid: 1580 + components: + - type: Transform + pos: 19.5,-9.5 + parent: 104 + - uid: 1581 + components: + - type: Transform + pos: 19.5,-9.5 + parent: 104 + - uid: 1582 + components: + - type: Transform + pos: 19.5,-8.5 + parent: 104 + - uid: 1583 + components: + - type: Transform + pos: 20.5,-9.5 + parent: 104 + - uid: 1584 + components: + - type: Transform + pos: 21.5,-9.5 + parent: 104 + - uid: 1585 + components: + - type: Transform + pos: 21.5,-4.5 + parent: 104 + - uid: 1586 + components: + - type: Transform + pos: 18.5,-7.5 + parent: 104 + - uid: 1587 + components: + - type: Transform + pos: 17.5,-7.5 + parent: 104 + - uid: 1588 + components: + - type: Transform + pos: 21.5,-8.5 + parent: 104 + - uid: 1589 + components: + - type: Transform + pos: 21.5,-7.5 + parent: 104 + - uid: 1590 + components: + - type: Transform + pos: 21.5,-6.5 + parent: 104 + - uid: 1591 + components: + - type: Transform + pos: 10.5,-1.5 + parent: 104 + - uid: 1592 + components: + - type: Transform + pos: 10.5,-3.5 + parent: 104 + - uid: 1593 + components: + - type: Transform + pos: 10.5,-2.5 + parent: 104 + - uid: 1595 + components: + - type: Transform + pos: -1.5,-3.5 + parent: 104 + - uid: 1596 + components: + - type: Transform + pos: -2.5,-3.5 + parent: 104 + - uid: 1597 + components: + - type: Transform + pos: -3.5,-3.5 + parent: 104 + - uid: 1941 + components: + - type: Transform + pos: -28.5,0.5 + parent: 104 + - uid: 1948 + components: + - type: Transform + pos: -14.5,-19.5 + parent: 104 + - uid: 2301 + components: + - type: Transform + pos: 12.5,-21.5 + parent: 104 + - uid: 2302 + components: + - type: Transform + pos: 11.5,-21.5 + parent: 104 + - uid: 2303 + components: + - type: Transform + pos: 13.5,-21.5 + parent: 104 + - uid: 2304 + components: + - type: Transform + pos: 0.5,-14.5 + parent: 104 + - uid: 2305 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 104 + - uid: 2306 + components: + - type: Transform + pos: -1.5,-14.5 + parent: 104 + - uid: 2307 + components: + - type: Transform + pos: -2.5,-14.5 + parent: 104 + - uid: 2374 + components: + - type: Transform + pos: 20.5,-12.5 + parent: 104 + - uid: 2375 + components: + - type: Transform + pos: 20.5,-13.5 + parent: 104 + - uid: 2376 + components: + - type: Transform + pos: 20.5,-14.5 + parent: 104 + - uid: 2377 + components: + - type: Transform + pos: 20.5,-15.5 + parent: 104 + - uid: 2378 + components: + - type: Transform + pos: 20.5,-16.5 + parent: 104 + - uid: 2379 + components: + - type: Transform + pos: 20.5,-17.5 + parent: 104 + - uid: 2381 + components: + - type: Transform + pos: 20.5,-18.5 + parent: 104 + - uid: 2382 + components: + - type: Transform + pos: 20.5,-19.5 + parent: 104 + - uid: 2383 + components: + - type: Transform + pos: 19.5,-17.5 + parent: 104 + - uid: 2384 + components: + - type: Transform + pos: 18.5,-17.5 + parent: 104 + - uid: 2385 + components: + - type: Transform + pos: 21.5,-17.5 + parent: 104 + - uid: 2386 + components: + - type: Transform + pos: 22.5,-17.5 + parent: 104 + - uid: 2387 + components: + - type: Transform + pos: 23.5,-17.5 + parent: 104 + - uid: 2388 + components: + - type: Transform + pos: 24.5,-17.5 + parent: 104 + - uid: 2389 + components: + - type: Transform + pos: 25.5,-17.5 + parent: 104 + - uid: 2390 + components: + - type: Transform + pos: 26.5,-17.5 + parent: 104 + - uid: 2391 + components: + - type: Transform + pos: 25.5,-18.5 + parent: 104 + - uid: 2392 + components: + - type: Transform + pos: 25.5,-19.5 + parent: 104 + - uid: 2393 + components: + - type: Transform + pos: 25.5,-16.5 + parent: 104 + - uid: 2394 + components: + - type: Transform + pos: 25.5,-15.5 + parent: 104 + - uid: 2395 + components: + - type: Transform + pos: 25.5,-14.5 + parent: 104 + - uid: 2396 + components: + - type: Transform + pos: 25.5,-13.5 + parent: 104 + - uid: 2439 + components: + - type: Transform + pos: 13.5,-9.5 + parent: 104 + - uid: 2440 + components: + - type: Transform + pos: 5.5,-6.5 + parent: 104 + - uid: 2441 + components: + - type: Transform + pos: 5.5,-5.5 + parent: 104 + - uid: 3378 + components: + - type: Transform + pos: 7.5,-9.5 + parent: 104 + - uid: 3379 + components: + - type: Transform + pos: 7.5,-10.5 + parent: 104 + - uid: 3380 + components: + - type: Transform + pos: 6.5,-10.5 + parent: 104 + - uid: 3381 + components: + - type: Transform + pos: 5.5,-10.5 + parent: 104 + - uid: 3382 + components: + - type: Transform + pos: 4.5,-10.5 + parent: 104 + - uid: 3383 + components: + - type: Transform + pos: 3.5,-10.5 + parent: 104 + - uid: 3384 + components: + - type: Transform + pos: 3.5,-11.5 + parent: 104 +- proto: CableApcStack + entities: + - uid: 211 + components: + - type: Transform + pos: 11.428643,-10.548745 + parent: 104 + - uid: 1324 + components: + - type: Transform + pos: 4.439247,-17.36072 + parent: 104 +- proto: CableHV + entities: + - uid: 2334 + components: + - type: Transform + pos: 17.5,-19.5 + parent: 104 + - uid: 2335 + components: + - type: Transform + pos: 17.5,-18.5 + parent: 104 + - uid: 2336 + components: + - type: Transform + pos: 17.5,-17.5 + parent: 104 + - uid: 2337 + components: + - type: Transform + pos: 18.5,-19.5 + parent: 104 + - uid: 2338 + components: + - type: Transform + pos: 19.5,-19.5 + parent: 104 + - uid: 2339 + components: + - type: Transform + pos: 20.5,-19.5 + parent: 104 + - uid: 2340 + components: + - type: Transform + pos: 21.5,-19.5 + parent: 104 + - uid: 2341 + components: + - type: Transform + pos: 22.5,-19.5 + parent: 104 + - uid: 2342 + components: + - type: Transform + pos: 23.5,-19.5 + parent: 104 + - uid: 2343 + components: + - type: Transform + pos: 24.5,-19.5 + parent: 104 + - uid: 2344 + components: + - type: Transform + pos: 25.5,-19.5 + parent: 104 + - uid: 2345 + components: + - type: Transform + pos: 26.5,-19.5 + parent: 104 + - uid: 2346 + components: + - type: Transform + pos: 27.5,-19.5 + parent: 104 + - uid: 2347 + components: + - type: Transform + pos: 27.5,-18.5 + parent: 104 + - uid: 2348 + components: + - type: Transform + pos: 27.5,-17.5 + parent: 104 + - uid: 2352 + components: + - type: Transform + pos: 21.5,-18.5 + parent: 104 + - uid: 2353 + components: + - type: Transform + pos: 22.5,-18.5 + parent: 104 + - uid: 2354 + components: + - type: Transform + pos: 23.5,-18.5 + parent: 104 + - uid: 2355 + components: + - type: Transform + pos: 22.5,-17.5 + parent: 104 + - uid: 2356 + components: + - type: Transform + pos: 23.5,-17.5 + parent: 104 + - uid: 2357 + components: + - type: Transform + pos: 24.5,-17.5 + parent: 104 + - uid: 2358 + components: + - type: Transform + pos: 24.5,-16.5 + parent: 104 + - uid: 2359 + components: + - type: Transform + pos: 24.5,-15.5 + parent: 104 + - uid: 2360 + components: + - type: Transform + pos: 24.5,-14.5 + parent: 104 + - uid: 2361 + components: + - type: Transform + pos: 21.5,-17.5 + parent: 104 + - uid: 2362 + components: + - type: Transform + pos: 25.5,-14.5 + parent: 104 + - uid: 2363 + components: + - type: Transform + pos: 26.5,-14.5 + parent: 104 + - uid: 2364 + components: + - type: Transform + pos: 27.5,-14.5 + parent: 104 + - uid: 2365 + components: + - type: Transform + pos: 27.5,-13.5 + parent: 104 +- proto: CableHVStack + entities: + - uid: 209 + components: + - type: Transform + pos: 11.334893,-10.282843 + parent: 104 +- proto: CableMV + entities: + - uid: 240 + components: + - type: Transform + pos: 17.5,-13.5 + parent: 104 + - uid: 1438 + components: + - type: Transform + pos: 20.5,-12.5 + parent: 104 + - uid: 1462 + components: + - type: Transform + pos: 16.5,-13.5 + parent: 104 + - uid: 1463 + components: + - type: Transform + pos: 15.5,-13.5 + parent: 104 + - uid: 1464 + components: + - type: Transform + pos: 15.5,-12.5 + parent: 104 + - uid: 1465 + components: + - type: Transform + pos: 15.5,-11.5 + parent: 104 + - uid: 1466 + components: + - type: Transform + pos: 15.5,-10.5 + parent: 104 + - uid: 1467 + components: + - type: Transform + pos: 14.5,-10.5 + parent: 104 + - uid: 1468 + components: + - type: Transform + pos: 13.5,-10.5 + parent: 104 + - uid: 1469 + components: + - type: Transform + pos: 13.5,-9.5 + parent: 104 + - uid: 1478 + components: + - type: Transform + pos: 12.5,-10.5 + parent: 104 + - uid: 1479 + components: + - type: Transform + pos: 11.5,-10.5 + parent: 104 + - uid: 1480 + components: + - type: Transform + pos: 10.5,-10.5 + parent: 104 + - uid: 1481 + components: + - type: Transform + pos: 9.5,-10.5 + parent: 104 + - uid: 1482 + components: + - type: Transform + pos: 9.5,-9.5 + parent: 104 + - uid: 1483 + components: + - type: Transform + pos: 9.5,-8.5 + parent: 104 + - uid: 1484 + components: + - type: Transform + pos: 9.5,-7.5 + parent: 104 + - uid: 1485 + components: + - type: Transform + pos: 9.5,-6.5 + parent: 104 + - uid: 1486 + components: + - type: Transform + pos: 8.5,-6.5 + parent: 104 + - uid: 1487 + components: + - type: Transform + pos: 8.5,-5.5 + parent: 104 + - uid: 2322 + components: + - type: Transform + pos: 18.5,-13.5 + parent: 104 + - uid: 2323 + components: + - type: Transform + pos: 19.5,-13.5 + parent: 104 + - uid: 2324 + components: + - type: Transform + pos: 20.5,-13.5 + parent: 104 + - uid: 2325 + components: + - type: Transform + pos: 20.5,-12.5 + parent: 104 + - uid: 2367 + components: + - type: Transform + pos: 27.5,-13.5 + parent: 104 + - uid: 2368 + components: + - type: Transform + pos: 26.5,-13.5 + parent: 104 + - uid: 2369 + components: + - type: Transform + pos: 25.5,-13.5 + parent: 104 + - uid: 2370 + components: + - type: Transform + pos: 24.5,-13.5 + parent: 104 + - uid: 2371 + components: + - type: Transform + pos: 23.5,-13.5 + parent: 104 + - uid: 2372 + components: + - type: Transform + pos: 22.5,-13.5 + parent: 104 + - uid: 2373 + components: + - type: Transform + pos: 21.5,-13.5 + parent: 104 +- proto: CableMVStack + entities: + - uid: 210 + components: + - type: Transform + pos: 11.381768,-10.407973 + parent: 104 +- proto: CableTerminal + entities: + - uid: 2349 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 21.5,-19.5 + parent: 104 + - uid: 2350 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 22.5,-19.5 + parent: 104 + - uid: 2351 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 23.5,-19.5 + parent: 104 +- proto: Carpet + entities: + - uid: 2459 + components: + - type: Transform + pos: 14.5,-4.5 + parent: 104 + - uid: 2460 + components: + - type: Transform + pos: 14.5,-3.5 + parent: 104 +- proto: Catwalk + entities: + - uid: 217 + components: + - type: Transform + pos: -12.5,-19.5 + parent: 104 + - uid: 218 + components: + - type: Transform + pos: -26.5,-20.5 + parent: 104 + - uid: 256 + components: + - type: Transform + pos: -29.5,2.5 + parent: 104 + - uid: 264 + components: + - type: Transform + pos: -24.5,-19.5 + parent: 104 + - uid: 278 + components: + - type: Transform + pos: -21.5,-23.5 + parent: 104 + - uid: 381 + components: + - type: Transform + pos: -20.5,-21.5 + parent: 104 + - uid: 382 + components: + - type: Transform + pos: -20.5,-23.5 + parent: 104 + - uid: 383 + components: + - type: Transform + pos: -20.5,-22.5 + parent: 104 + - uid: 384 + components: + - type: Transform + pos: -26.5,-19.5 + parent: 104 + - uid: 385 + components: + - type: Transform + pos: -30.5,4.5 + parent: 104 + - uid: 386 + components: + - type: Transform + pos: -30.5,-19.5 + parent: 104 + - uid: 387 + components: + - type: Transform + pos: -14.5,-20.5 + parent: 104 + - uid: 388 + components: + - type: Transform + pos: -29.5,3.5 + parent: 104 + - uid: 389 + components: + - type: Transform + pos: -14.5,-22.5 + parent: 104 + - uid: 391 + components: + - type: Transform + pos: -33.5,3.5 + parent: 104 + - uid: 392 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -34.5,-21.5 + parent: 104 + - uid: 393 + components: + - type: Transform + pos: -19.5,-22.5 + parent: 104 + - uid: 394 + components: + - type: Transform + pos: -32.5,4.5 + parent: 104 + - uid: 395 + components: + - type: Transform + pos: -24.5,-21.5 + parent: 104 + - uid: 396 + components: + - type: Transform + pos: -10.5,-16.5 + parent: 104 + - uid: 398 + components: + - type: Transform + pos: -9.5,-15.5 + parent: 104 + - uid: 399 + components: + - type: Transform + pos: -10.5,-15.5 + parent: 104 + - uid: 400 + components: + - type: Transform + pos: -9.5,-14.5 + parent: 104 + - uid: 413 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -24.5,3.5 + parent: 104 + - uid: 414 + components: + - type: Transform + pos: -35.5,-19.5 + parent: 104 + - uid: 431 + components: + - type: Transform + pos: -9.5,-13.5 + parent: 104 + - uid: 440 + components: + - type: Transform + pos: -16.5,-21.5 + parent: 104 + - uid: 450 + components: + - type: Transform + pos: -33.5,-20.5 + parent: 104 + - uid: 451 + components: + - type: Transform + pos: -34.5,-19.5 + parent: 104 + - uid: 464 + components: + - type: Transform + pos: -34.5,-20.5 + parent: 104 + - uid: 468 + components: + - type: Transform + pos: -9.5,-21.5 + parent: 104 + - uid: 470 + components: + - type: Transform + pos: -9.5,-20.5 + parent: 104 + - uid: 490 + components: + - type: Transform + pos: -13.5,0.5 + parent: 104 + - uid: 491 + components: + - type: Transform + pos: -12.5,1.5 + parent: 104 + - uid: 494 + components: + - type: Transform + pos: -11.5,1.5 + parent: 104 + - uid: 496 + components: + - type: Transform + pos: -14.5,1.5 + parent: 104 + - uid: 499 + components: + - type: Transform + pos: -9.5,-19.5 + parent: 104 + - uid: 500 + components: + - type: Transform + pos: -9.5,-18.5 + parent: 104 + - uid: 501 + components: + - type: Transform + pos: -30.5,0.5 + parent: 104 + - uid: 503 + components: + - type: Transform + pos: -29.5,1.5 + parent: 104 + - uid: 508 + components: + - type: Transform + pos: -12.5,0.5 + parent: 104 + - uid: 510 + components: + - type: Transform + pos: -31.5,0.5 + parent: 104 + - uid: 514 + components: + - type: Transform + pos: -27.5,0.5 + parent: 104 + - uid: 515 + components: + - type: Transform + pos: -30.5,1.5 + parent: 104 + - uid: 516 + components: + - type: Transform + pos: -9.5,-17.5 + parent: 104 + - uid: 517 + components: + - type: Transform + pos: -9.5,-16.5 + parent: 104 + - uid: 518 + components: + - type: Transform + pos: -10.5,-14.5 + parent: 104 + - uid: 519 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -31.5,-21.5 + parent: 104 + - uid: 520 + components: + - type: Transform + pos: -28.5,0.5 + parent: 104 + - uid: 583 + components: + - type: Transform + pos: -26.5,1.5 + parent: 104 + - uid: 584 + components: + - type: Transform + pos: -10.5,-13.5 + parent: 104 + - uid: 616 + components: + - type: Transform + pos: -31.5,3.5 + parent: 104 + - uid: 617 + components: + - type: Transform + pos: -14.5,-23.5 + parent: 104 + - uid: 626 + components: + - type: Transform + pos: -16.5,-22.5 + parent: 104 + - uid: 631 + components: + - type: Transform + pos: -9.5,-23.5 + parent: 104 + - uid: 633 + components: + - type: Transform + pos: -9.5,-22.5 + parent: 104 + - uid: 634 + components: + - type: Transform + pos: -16.5,-23.5 + parent: 104 + - uid: 635 + components: + - type: Transform + pos: -25.5,0.5 + parent: 104 + - uid: 636 + components: + - type: Transform + pos: -27.5,1.5 + parent: 104 + - uid: 638 + components: + - type: Transform + pos: -26.5,0.5 + parent: 104 + - uid: 639 + components: + - type: Transform + pos: -24.5,1.5 + parent: 104 + - uid: 641 + components: + - type: Transform + pos: -11.5,-22.5 + parent: 104 + - uid: 642 + components: + - type: Transform + pos: -11.5,-23.5 + parent: 104 + - uid: 643 + components: + - type: Transform + pos: -10.5,-23.5 + parent: 104 + - uid: 644 + components: + - type: Transform + pos: -24.5,0.5 + parent: 104 + - uid: 645 + components: + - type: Transform + pos: -25.5,1.5 + parent: 104 + - uid: 646 + components: + - type: Transform + pos: -15.5,0.5 + parent: 104 + - uid: 647 + components: + - type: Transform + pos: -13.5,-23.5 + parent: 104 + - uid: 652 + components: + - type: Transform + pos: -14.5,0.5 + parent: 104 + - uid: 653 + components: + - type: Transform + pos: -13.5,1.5 + parent: 104 + - uid: 654 + components: + - type: Transform + pos: -29.5,0.5 + parent: 104 + - uid: 655 + components: + - type: Transform + pos: -28.5,1.5 + parent: 104 + - uid: 656 + components: + - type: Transform + pos: -35.5,-20.5 + parent: 104 + - uid: 658 + components: + - type: Transform + pos: -11.5,0.5 + parent: 104 + - uid: 661 + components: + - type: Transform + pos: -20.5,-19.5 + parent: 104 + - uid: 662 + components: + - type: Transform + pos: -18.5,0.5 + parent: 104 + - uid: 663 + components: + - type: Transform + pos: -12.5,-23.5 + parent: 104 + - uid: 664 + components: + - type: Transform + pos: -17.5,1.5 + parent: 104 + - uid: 665 + components: + - type: Transform + pos: -21.5,0.5 + parent: 104 + - uid: 667 + components: + - type: Transform + pos: -20.5,-20.5 + parent: 104 + - uid: 668 + components: + - type: Transform + pos: -32.5,1.5 + parent: 104 + - uid: 669 + components: + - type: Transform + pos: -31.5,1.5 + parent: 104 + - uid: 707 + components: + - type: Transform + pos: -32.5,0.5 + parent: 104 + - uid: 713 + components: + - type: Transform + pos: -20.5,1.5 + parent: 104 + - uid: 715 + components: + - type: Transform + pos: -10.5,-22.5 + parent: 104 + - uid: 736 + components: + - type: Transform + pos: -10.5,-21.5 + parent: 104 + - uid: 737 + components: + - type: Transform + pos: -10.5,-20.5 + parent: 104 + - uid: 738 + components: + - type: Transform + pos: -20.5,0.5 + parent: 104 + - uid: 742 + components: + - type: Transform + pos: -10.5,-19.5 + parent: 104 + - uid: 748 + components: + - type: Transform + pos: -10.5,-18.5 + parent: 104 + - uid: 749 + components: + - type: Transform + pos: -10.5,-17.5 + parent: 104 + - uid: 760 + components: + - type: Transform + pos: -14.5,-21.5 + parent: 104 + - uid: 761 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -26.5,-21.5 + parent: 104 + - uid: 769 + components: + - type: Transform + pos: -33.5,4.5 + parent: 104 + - uid: 772 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -33.5,-21.5 + parent: 104 + - uid: 799 + components: + - type: Transform + pos: -26.5,3.5 + parent: 104 + - uid: 800 + components: + - type: Transform + pos: -34.5,4.5 + parent: 104 + - uid: 812 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -32.5,-21.5 + parent: 104 + - uid: 813 + components: + - type: Transform + pos: -29.5,4.5 + parent: 104 + - uid: 815 + components: + - type: Transform + pos: -17.5,-23.5 + parent: 104 + - uid: 816 + components: + - type: Transform + pos: -34.5,1.5 + parent: 104 + - uid: 817 + components: + - type: Transform + pos: -24.5,-22.5 + parent: 104 + - uid: 818 + components: + - type: Transform + pos: -24.5,-23.5 + parent: 104 + - uid: 820 + components: + - type: Transform + pos: -21.5,-21.5 + parent: 104 + - uid: 821 + components: + - type: Transform + pos: -21.5,-22.5 + parent: 104 + - uid: 822 + components: + - type: Transform + pos: -27.5,3.5 + parent: 104 + - uid: 823 + components: + - type: Transform + pos: -26.5,4.5 + parent: 104 + - uid: 824 + components: + - type: Transform + pos: -25.5,4.5 + parent: 104 + - uid: 825 + components: + - type: Transform + pos: -25.5,2.5 + parent: 104 + - uid: 826 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -33.5,-23.5 + parent: 104 + - uid: 827 + components: + - type: Transform + pos: -27.5,4.5 + parent: 104 + - uid: 828 + components: + - type: Transform + pos: -27.5,2.5 + parent: 104 + - uid: 829 + components: + - type: Transform + pos: -26.5,2.5 + parent: 104 + - uid: 830 + components: + - type: Transform + pos: -25.5,3.5 + parent: 104 + - uid: 831 + components: + - type: Transform + pos: -19.5,-20.5 + parent: 104 + - uid: 833 + components: + - type: Transform + pos: -18.5,-21.5 + parent: 104 + - uid: 834 + components: + - type: Transform + pos: -17.5,0.5 + parent: 104 + - uid: 835 + components: + - type: Transform + pos: -15.5,1.5 + parent: 104 + - uid: 836 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -29.5,-21.5 + parent: 104 + - uid: 837 + components: + - type: Transform + pos: -16.5,1.5 + parent: 104 + - uid: 839 + components: + - type: Transform + pos: -32.5,3.5 + parent: 104 + - uid: 841 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -33.5,-22.5 + parent: 104 + - uid: 842 + components: + - type: Transform + pos: -17.5,-22.5 + parent: 104 + - uid: 843 + components: + - type: Transform + pos: -18.5,-23.5 + parent: 104 + - uid: 844 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -32.5,-22.5 + parent: 104 + - uid: 846 + components: + - type: Transform + pos: -34.5,0.5 + parent: 104 + - uid: 847 + components: + - type: Transform + pos: -19.5,-21.5 + parent: 104 + - uid: 848 + components: + - type: Transform + pos: -16.5,0.5 + parent: 104 + - uid: 849 + components: + - type: Transform + pos: -18.5,-22.5 + parent: 104 + - uid: 850 + components: + - type: Transform + pos: -17.5,-21.5 + parent: 104 + - uid: 851 + components: + - type: Transform + pos: -17.5,-19.5 + parent: 104 + - uid: 852 + components: + - type: Transform + pos: -18.5,-19.5 + parent: 104 + - uid: 853 + components: + - type: Transform + pos: -19.5,1.5 + parent: 104 + - uid: 854 + components: + - type: Transform + pos: -17.5,-20.5 + parent: 104 + - uid: 855 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -30.5,-21.5 + parent: 104 + - uid: 856 + components: + - type: Transform + pos: -31.5,2.5 + parent: 104 + - uid: 857 + components: + - type: Transform + pos: -19.5,0.5 + parent: 104 + - uid: 858 + components: + - type: Transform + pos: -30.5,3.5 + parent: 104 + - uid: 859 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -28.5,-21.5 + parent: 104 + - uid: 860 + components: + - type: Transform + pos: -33.5,1.5 + parent: 104 + - uid: 861 + components: + - type: Transform + pos: -33.5,0.5 + parent: 104 + - uid: 862 + components: + - type: Transform + pos: -31.5,-20.5 + parent: 104 + - uid: 863 + components: + - type: Transform + pos: -32.5,-19.5 + parent: 104 + - uid: 864 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -27.5,-21.5 + parent: 104 + - uid: 865 + components: + - type: Transform + pos: -28.5,2.5 + parent: 104 + - uid: 869 + components: + - type: Transform + pos: -32.5,2.5 + parent: 104 + - uid: 871 + components: + - type: Transform + pos: -31.5,4.5 + parent: 104 + - uid: 872 + components: + - type: Transform + pos: -30.5,2.5 + parent: 104 + - uid: 873 + components: + - type: Transform + pos: -18.5,1.5 + parent: 104 + - uid: 874 + components: + - type: Transform + pos: -15.5,4.5 + parent: 104 + - uid: 875 + components: + - type: Transform + pos: -15.5,3.5 + parent: 104 + - uid: 876 + components: + - type: Transform + pos: -15.5,2.5 + parent: 104 + - uid: 878 + components: + - type: Transform + pos: -23.5,-21.5 + parent: 104 + - uid: 879 + components: + - type: Transform + pos: -23.5,-22.5 + parent: 104 + - uid: 880 + components: + - type: Transform + pos: -23.5,-23.5 + parent: 104 + - uid: 881 + components: + - type: Transform + pos: -18.5,-20.5 + parent: 104 + - uid: 882 + components: + - type: Transform + pos: -22.5,-21.5 + parent: 104 + - uid: 883 + components: + - type: Transform + pos: -19.5,-19.5 + parent: 104 + - uid: 885 + components: + - type: Transform + pos: -32.5,-20.5 + parent: 104 + - uid: 886 + components: + - type: Transform + pos: -33.5,-19.5 + parent: 104 + - uid: 887 + components: + - type: Transform + pos: -28.5,4.5 + parent: 104 + - uid: 888 + components: + - type: Transform + pos: -28.5,3.5 + parent: 104 + - uid: 889 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -34.5,-23.5 + parent: 104 + - uid: 890 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -32.5,-23.5 + parent: 104 + - uid: 891 + components: + - type: Transform + pos: -33.5,2.5 + parent: 104 + - uid: 892 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -31.5,-22.5 + parent: 104 + - uid: 893 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -31.5,-23.5 + parent: 104 + - uid: 894 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -30.5,-22.5 + parent: 104 + - uid: 895 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -30.5,-23.5 + parent: 104 + - uid: 896 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -29.5,-22.5 + parent: 104 + - uid: 897 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -29.5,-23.5 + parent: 104 + - uid: 898 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -28.5,-22.5 + parent: 104 + - uid: 899 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -28.5,-23.5 + parent: 104 + - uid: 900 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -27.5,-22.5 + parent: 104 + - uid: 901 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -26.5,-22.5 + parent: 104 + - uid: 902 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -26.5,-23.5 + parent: 104 + - uid: 903 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -25.5,-22.5 + parent: 104 + - uid: 904 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -25.5,-23.5 + parent: 104 + - uid: 905 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -35.5,-22.5 + parent: 104 + - uid: 906 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -35.5,-23.5 + parent: 104 + - uid: 907 + components: + - type: Transform + pos: -19.5,-23.5 + parent: 104 + - uid: 908 + components: + - type: Transform + pos: -21.5,-19.5 + parent: 104 + - uid: 909 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -25.5,-21.5 + parent: 104 + - uid: 910 + components: + - type: Transform + pos: -31.5,-19.5 + parent: 104 + - uid: 911 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -35.5,-21.5 + parent: 104 + - uid: 912 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -34.5,-22.5 + parent: 104 + - uid: 913 + components: + - type: Transform + pos: -23.5,4.5 + parent: 104 + - uid: 914 + components: + - type: Transform + pos: -21.5,-20.5 + parent: 104 + - uid: 915 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -23.5,3.5 + parent: 104 + - uid: 916 + components: + - type: Transform + pos: -12.5,4.5 + parent: 104 + - uid: 917 + components: + - type: Transform + pos: -12.5,3.5 + parent: 104 + - uid: 918 + components: + - type: Transform + pos: -12.5,2.5 + parent: 104 + - uid: 919 + components: + - type: Transform + pos: -22.5,-22.5 + parent: 104 + - uid: 921 + components: + - type: Transform + pos: -15.5,-22.5 + parent: 104 + - uid: 922 + components: + - type: Transform + pos: -30.5,-20.5 + parent: 104 + - uid: 923 + components: + - type: Transform + pos: -13.5,4.5 + parent: 104 + - uid: 924 + components: + - type: Transform + pos: -13.5,3.5 + parent: 104 + - uid: 925 + components: + - type: Transform + pos: -13.5,2.5 + parent: 104 + - uid: 928 + components: + - type: Transform + pos: -15.5,-23.5 + parent: 104 + - uid: 930 + components: + - type: Transform + pos: -14.5,4.5 + parent: 104 + - uid: 932 + components: + - type: Transform + pos: -14.5,2.5 + parent: 104 + - uid: 935 + components: + - type: Transform + pos: -22.5,-20.5 + parent: 104 + - uid: 936 + components: + - type: Transform + pos: -16.5,4.5 + parent: 104 + - uid: 937 + components: + - type: Transform + pos: -16.5,3.5 + parent: 104 + - uid: 938 + components: + - type: Transform + pos: -16.5,2.5 + parent: 104 + - uid: 939 + components: + - type: Transform + pos: -27.5,-20.5 + parent: 104 + - uid: 940 + components: + - type: Transform + pos: -15.5,-21.5 + parent: 104 + - uid: 941 + components: + - type: Transform + pos: -13.5,-19.5 + parent: 104 + - uid: 943 + components: + - type: Transform + pos: -17.5,4.5 + parent: 104 + - uid: 944 + components: + - type: Transform + pos: -17.5,3.5 + parent: 104 + - uid: 945 + components: + - type: Transform + pos: -17.5,2.5 + parent: 104 + - uid: 946 + components: + - type: Transform + pos: -12.5,-22.5 + parent: 104 + - uid: 947 + components: + - type: Transform + pos: -22.5,-23.5 + parent: 104 + - uid: 948 + components: + - type: Transform + pos: -13.5,-21.5 + parent: 104 + - uid: 949 + components: + - type: Transform + pos: -22.5,1.5 + parent: 104 + - uid: 950 + components: + - type: Transform + pos: -18.5,4.5 + parent: 104 + - uid: 951 + components: + - type: Transform + pos: -18.5,3.5 + parent: 104 + - uid: 952 + components: + - type: Transform + pos: -18.5,2.5 + parent: 104 + - uid: 953 + components: + - type: Transform + pos: -12.5,-21.5 + parent: 104 + - uid: 954 + components: + - type: Transform + pos: -13.5,-22.5 + parent: 104 + - uid: 958 + components: + - type: Transform + pos: -19.5,3.5 + parent: 104 + - uid: 959 + components: + - type: Transform + pos: -19.5,2.5 + parent: 104 + - uid: 964 + components: + - type: Transform + pos: -11.5,4.5 + parent: 104 + - uid: 965 + components: + - type: Transform + pos: -11.5,3.5 + parent: 104 + - uid: 966 + components: + - type: Transform + pos: -11.5,2.5 + parent: 104 + - uid: 972 + components: + - type: Transform + pos: -11.5,-21.5 + parent: 104 + - uid: 984 + components: + - type: Transform + pos: -9.5,-9.5 + parent: 104 + - uid: 985 + components: + - type: Transform + pos: -9.5,-10.5 + parent: 104 + - uid: 986 + components: + - type: Transform + pos: -9.5,-11.5 + parent: 104 + - uid: 987 + components: + - type: Transform + pos: -9.5,-12.5 + parent: 104 + - uid: 988 + components: + - type: Transform + pos: -10.5,4.5 + parent: 104 + - uid: 989 + components: + - type: Transform + pos: -10.5,3.5 + parent: 104 + - uid: 990 + components: + - type: Transform + pos: -10.5,2.5 + parent: 104 + - uid: 991 + components: + - type: Transform + pos: -10.5,1.5 + parent: 104 + - uid: 992 + components: + - type: Transform + pos: -10.5,0.5 + parent: 104 + - uid: 993 + components: + - type: Transform + pos: -10.5,-0.5 + parent: 104 + - uid: 994 + components: + - type: Transform + pos: -10.5,-1.5 + parent: 104 + - uid: 995 + components: + - type: Transform + pos: -10.5,-2.5 + parent: 104 + - uid: 996 + components: + - type: Transform + pos: -10.5,-3.5 + parent: 104 + - uid: 997 + components: + - type: Transform + pos: -10.5,-4.5 + parent: 104 + - uid: 998 + components: + - type: Transform + pos: -10.5,-5.5 + parent: 104 + - uid: 999 + components: + - type: Transform + pos: -10.5,-6.5 + parent: 104 + - uid: 1000 + components: + - type: Transform + pos: -10.5,-7.5 + parent: 104 + - uid: 1001 + components: + - type: Transform + pos: -10.5,-8.5 + parent: 104 + - uid: 1002 + components: + - type: Transform + pos: -10.5,-9.5 + parent: 104 + - uid: 1003 + components: + - type: Transform + pos: -10.5,-10.5 + parent: 104 + - uid: 1004 + components: + - type: Transform + pos: -10.5,-11.5 + parent: 104 + - uid: 1005 + components: + - type: Transform + pos: -10.5,-12.5 + parent: 104 + - uid: 1006 + components: + - type: Transform + pos: -9.5,4.5 + parent: 104 + - uid: 1007 + components: + - type: Transform + pos: -9.5,3.5 + parent: 104 + - uid: 1008 + components: + - type: Transform + pos: -9.5,2.5 + parent: 104 + - uid: 1009 + components: + - type: Transform + pos: -9.5,1.5 + parent: 104 + - uid: 1010 + components: + - type: Transform + pos: -9.5,0.5 + parent: 104 + - uid: 1011 + components: + - type: Transform + pos: -9.5,-0.5 + parent: 104 + - uid: 1012 + components: + - type: Transform + pos: -9.5,-1.5 + parent: 104 + - uid: 1013 + components: + - type: Transform + pos: -9.5,-2.5 + parent: 104 + - uid: 1014 + components: + - type: Transform + pos: -9.5,-3.5 + parent: 104 + - uid: 1015 + components: + - type: Transform + pos: -9.5,-4.5 + parent: 104 + - uid: 1016 + components: + - type: Transform + pos: -9.5,-5.5 + parent: 104 + - uid: 1017 + components: + - type: Transform + pos: -9.5,-6.5 + parent: 104 + - uid: 1018 + components: + - type: Transform + pos: -9.5,-7.5 + parent: 104 + - uid: 1019 + components: + - type: Transform + pos: -9.5,-8.5 + parent: 104 + - uid: 1086 + components: + - type: Transform + pos: -14.5,3.5 + parent: 104 + - uid: 1087 + components: + - type: Transform + pos: -19.5,4.5 + parent: 104 + - uid: 1202 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -24.5,4.5 + parent: 104 + - uid: 1203 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -27.5,-23.5 + parent: 104 + - uid: 1222 + components: + - type: Transform + pos: -34.5,2.5 + parent: 104 + - uid: 1223 + components: + - type: Transform + pos: -34.5,3.5 + parent: 104 + - uid: 1280 + components: + - type: Transform + pos: -23.5,-20.5 + parent: 104 + - uid: 1284 + components: + - type: Transform + pos: -25.5,-19.5 + parent: 104 + - uid: 1289 + components: + - type: Transform + pos: -28.5,-20.5 + parent: 104 + - uid: 1290 + components: + - type: Transform + pos: -22.5,0.5 + parent: 104 + - uid: 1362 + components: + - type: Transform + pos: -4.5,-2.5 + parent: 104 + - uid: 1363 + components: + - type: Transform + pos: -2.5,-2.5 + parent: 104 + - uid: 1364 + components: + - type: Transform + pos: -3.5,-2.5 + parent: 104 + - uid: 1365 + components: + - type: Transform + pos: -4.5,-4.5 + parent: 104 + - uid: 1366 + components: + - type: Transform + pos: -3.5,-4.5 + parent: 104 + - uid: 1377 + components: + - type: Transform + pos: -2.5,-4.5 + parent: 104 + - uid: 1428 + components: + - type: Transform + pos: -16.5,-19.5 + parent: 104 + - uid: 1535 + components: + - type: Transform + pos: -13.5,-20.5 + parent: 104 + - uid: 1554 + components: + - type: Transform + pos: -29.5,-19.5 + parent: 104 + - uid: 1674 + components: + - type: Transform + pos: -28.5,-19.5 + parent: 104 + - uid: 1785 + components: + - type: Transform + pos: -11.5,-19.5 + parent: 104 + - uid: 1786 + components: + - type: Transform + pos: -22.5,-19.5 + parent: 104 + - uid: 1787 + components: + - type: Transform + pos: -11.5,-20.5 + parent: 104 + - uid: 1788 + components: + - type: Transform + pos: -27.5,-19.5 + parent: 104 + - uid: 1789 + components: + - type: Transform + pos: -24.5,-20.5 + parent: 104 + - uid: 1791 + components: + - type: Transform + pos: -25.5,-20.5 + parent: 104 + - uid: 1799 + components: + - type: Transform + pos: -29.5,-20.5 + parent: 104 + - uid: 1825 + components: + - type: Transform + pos: -12.5,-20.5 + parent: 104 + - uid: 1886 + components: + - type: Transform + pos: -15.5,-20.5 + parent: 104 + - uid: 1887 + components: + - type: Transform + pos: -14.5,-19.5 + parent: 104 + - uid: 1947 + components: + - type: Transform + pos: -15.5,-19.5 + parent: 104 + - uid: 1949 + components: + - type: Transform + pos: -23.5,-19.5 + parent: 104 + - uid: 2046 + components: + - type: Transform + pos: -16.5,-20.5 + parent: 104 + - uid: 2048 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -23.5,2.5 + parent: 104 + - uid: 2050 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -24.5,2.5 + parent: 104 + - uid: 2052 + components: + - type: Transform + pos: -23.5,1.5 + parent: 104 + - uid: 2188 + components: + - type: Transform + pos: -21.5,1.5 + parent: 104 + - uid: 2405 + components: + - type: Transform + pos: 23.5,-19.5 + parent: 104 + - uid: 2406 + components: + - type: Transform + pos: 22.5,-19.5 + parent: 104 + - uid: 2407 + components: + - type: Transform + pos: 21.5,-19.5 + parent: 104 + - uid: 2408 + components: + - type: Transform + pos: 21.5,-17.5 + parent: 104 + - uid: 2409 + components: + - type: Transform + pos: 22.5,-17.5 + parent: 104 + - uid: 2410 + components: + - type: Transform + pos: 23.5,-17.5 + parent: 104 + - uid: 2505 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -22.5,3.5 + parent: 104 + - uid: 2507 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -20.5,4.5 + parent: 104 + - uid: 2508 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -21.5,2.5 + parent: 104 + - uid: 2514 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -20.5,3.5 + parent: 104 + - uid: 2515 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -21.5,4.5 + parent: 104 + - uid: 2521 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -20.5,2.5 + parent: 104 + - uid: 2526 + components: + - type: Transform + pos: -23.5,0.5 + parent: 104 + - uid: 2553 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -22.5,4.5 + parent: 104 + - uid: 2554 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -22.5,2.5 + parent: 104 + - uid: 2557 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -21.5,3.5 + parent: 104 +- proto: Chair + entities: + - uid: 487 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-20.5 + parent: 104 + - uid: 1367 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -3.5,-4.5 + parent: 104 + - uid: 1376 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -4.5,-4.5 + parent: 104 + - uid: 1878 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-21.5 + parent: 104 + - uid: 1879 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-22.5 + parent: 104 + - uid: 2312 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-13.5 + parent: 104 + - uid: 2313 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 7.5,-14.5 + parent: 104 +- proto: ChairFolding + entities: + - uid: 544 + components: + - type: Transform + pos: 20.5,8.5 + parent: 104 + - uid: 1062 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 18.5,4.5 + parent: 104 + - uid: 1063 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 18.5,2.5 + parent: 104 +- proto: ChairOfficeDark + entities: + - uid: 1298 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 13.5,-17.5 + parent: 104 + - uid: 1304 + components: + - type: Transform + pos: 11.5,-17.5 + parent: 104 + - uid: 1764 + components: + - type: Transform + pos: -13.5,6.5 + parent: 104 + - uid: 2427 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 19.5,-15.5 + parent: 104 + - uid: 2428 + components: + - type: Transform + pos: 25.5,-15.5 + parent: 104 +- proto: ChairOfficeLight + entities: + - uid: 751 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -0.5,-16.5 + parent: 104 + - uid: 1124 + components: + - type: Transform + pos: -0.5,-12.5 + parent: 104 + - uid: 1296 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 4.5,-11.5 + parent: 104 + - uid: 1297 + components: + - type: Transform + pos: 4.5,-16.5 + parent: 104 +- proto: ChairPilotSeat + entities: + - uid: 326 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 20.5,-6.5 + parent: 104 + - uid: 327 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 19.5,-7.5 + parent: 104 + - uid: 328 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 21.5,-7.5 + parent: 104 + - uid: 329 + components: + - type: Transform + pos: 20.5,-8.5 + parent: 104 + - uid: 330 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 18.5,-6.5 + parent: 104 + - uid: 331 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 19.5,-5.5 + parent: 104 + - uid: 332 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 21.5,-5.5 + parent: 104 + - uid: 333 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 22.5,-6.5 + parent: 104 + - uid: 334 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 22.5,-8.5 + parent: 104 + - uid: 335 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 21.5,-9.5 + parent: 104 + - uid: 336 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 19.5,-9.5 + parent: 104 + - uid: 337 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 18.5,-8.5 + parent: 104 +- proto: ChairWood + entities: + - uid: 2429 + components: + - type: Transform + pos: -0.5,-1.5 + parent: 104 +- proto: chem_master + entities: + - uid: 2444 + components: + - type: Transform + pos: 5.5,-10.5 + parent: 104 +- proto: ChemicalPayload + entities: + - uid: 2380 + components: + - type: Transform + pos: 3.3003616,-17.381208 + parent: 104 + - uid: 3110 + components: + - type: Transform + pos: 3.6284866,-17.287361 + parent: 104 +- proto: ChemistryHotplate + entities: + - uid: 1383 + components: + - type: Transform + pos: 4.5,-12.5 + parent: 104 +- proto: ChessBoard + entities: + - uid: 354 + components: + - type: Transform + pos: 13.499366,-8.433407 + parent: 104 +- proto: ClosetEmergencyFilledRandom + entities: + - uid: 1359 + components: + - type: Transform + pos: -2.5,-2.5 + parent: 104 +- proto: ClosetL3Filled + entities: + - uid: 1153 + components: + - type: Transform + pos: 7.5,-17.5 + parent: 104 +- proto: ClosetRadiationSuitFilled + entities: + - uid: 1114 + components: + - type: Transform + pos: 6.5,-17.5 + parent: 104 +- proto: ClosetToolFilled + entities: + - uid: 2417 + components: + - type: Transform + pos: 20.5,-14.5 + parent: 104 + - uid: 2418 + components: + - type: Transform + pos: 24.5,-14.5 + parent: 104 +- proto: ClothingBackpackDuffelSyndicateFilledMedical + entities: + - uid: 2437 + components: + - type: Transform + pos: 0.5,-12.5 + parent: 104 +- proto: ClothingBackpackWaterTank + entities: + - uid: 151 + components: + - type: Transform + pos: 9.36168,-4.462339 + parent: 104 + - type: SolutionAmmoProvider + maxShots: 200 +- proto: ClothingBeltUtilityFilled + entities: + - uid: 212 + components: + - type: Transform + pos: 13.897393,-10.470539 + parent: 104 +- proto: ClothingEyesGlassesChemical + entities: + - uid: 1738 + components: + - type: Transform + pos: 3.5009227,-11.519134 + parent: 104 +- proto: ClothingNeckScarfStripedRed + entities: + - uid: 1150 + components: + - type: Transform + pos: -2.530845,-16.461567 + parent: 104 +- proto: ClothingNeckStethoscope + entities: + - uid: 1336 + components: + - type: Transform + pos: 7.5027137,-12.301802 + parent: 104 +- proto: ClothingShoesBootsSalvage + entities: + - uid: 542 + components: + - type: Transform + pos: 19.574894,7.069533 + parent: 104 + - uid: 545 + components: + - type: Transform + pos: 19.246769,6.507033 + parent: 104 +- proto: CombatKnife + entities: + - uid: 1694 + components: + - type: Transform + pos: 5.483225,11.368477 + parent: 104 +- proto: ComfyChair + entities: + - uid: 21 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 12.5,-8.5 + parent: 104 + - uid: 352 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 14.5,-8.5 + parent: 104 +- proto: computerBodyScanner + entities: + - uid: 2436 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-14.5 + parent: 104 +- proto: ComputerPowerMonitoring + entities: + - uid: 1730 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 27.5,-14.5 + parent: 104 +- proto: ComputerSurveillanceWirelessCameraMonitor + entities: + - uid: 1127 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -14.5,6.5 + parent: 104 +- proto: CyberPen + entities: + - uid: 696 + components: + - type: Transform + pos: -14.478909,9.585995 + parent: 104 +- proto: DresserFilled + entities: + - uid: 349 + components: + - type: Transform + pos: 13.5,-3.5 + parent: 104 +- proto: DrinkAbsintheBottleFull + entities: + - uid: 15 + components: + - type: Transform + pos: 3.9415665,-8.34479 + parent: 104 +- proto: DrinkBeerBottleFull + entities: + - uid: 157 + components: + - type: Transform + pos: 2.5961013,-2.2186527 + parent: 104 + - uid: 170 + components: + - type: Transform + pos: 3.6117263,-3.1874027 + parent: 104 +- proto: DrinkGinBottleFull + entities: + - uid: 134 + components: + - type: Transform + pos: 3.2228165,-8.4229965 + parent: 104 +- proto: DrinkWhiskeyBottleFull + entities: + - uid: 25 + components: + - type: Transform + pos: -0.5303154,-6.2851996 + parent: 104 +- proto: EmpGrenade + entities: + - uid: 2456 + components: + - type: Transform + pos: 14.40774,-17.385033 + parent: 104 + - uid: 2457 + components: + - type: Transform + pos: 14.62649,-17.385033 + parent: 104 +- proto: ExtinguisherCabinetFilled + entities: + - uid: 139 + components: + - type: Transform + pos: 28.5,-14.5 + parent: 104 + - uid: 141 + components: + - type: Transform + pos: 14.5,-0.5 + parent: 104 + - uid: 1812 + components: + - type: Transform + pos: -1.5,-2.5 + parent: 104 + - uid: 1816 + components: + - type: Transform + pos: 8.5,-10.5 + parent: 104 + - uid: 1822 + components: + - type: Transform + pos: 15.5,-17.5 + parent: 104 +- proto: FaxMachineSyndie + entities: + - uid: 81 + components: + - type: Transform + pos: 7.5,-4.5 + parent: 104 +- proto: filingCabinetDrawer + entities: + - uid: 1140 + components: + - type: Transform + pos: -12.5,7.5 + parent: 104 +- proto: filingCabinetDrawerRandom + entities: + - uid: 108 + components: + - type: Transform + pos: 7.5,-5.5 + parent: 104 +- proto: FirelockGlass + entities: + - uid: 1765 + components: + - type: Transform + pos: -13.5,5.5 + parent: 104 +- proto: Fireplace + entities: + - uid: 1273 + components: + - type: Transform + pos: 1.5,0.5 + parent: 104 +- proto: FloorDrain + entities: + - uid: 1361 + components: + - type: Transform + pos: -4.5,-2.5 + parent: 104 + - type: Fixtures + fixtures: {} + - uid: 2438 + components: + - type: Transform + pos: 10.5,-3.5 + parent: 104 + - type: Fixtures + fixtures: {} + - uid: 2443 + components: + - type: Transform + pos: 4.5,-11.5 + parent: 104 + - type: Fixtures + fixtures: {} +- proto: FloraRockSolid01 + entities: + - uid: 1707 + components: + - type: Transform + pos: 5.5565968,16.377468 + parent: 104 +- proto: FloraRockSolid02 + entities: + - uid: 1708 + components: + - type: Transform + pos: -3.1481876,-0.15203857 + parent: 104 + - uid: 1711 + components: + - type: Transform + pos: -1.3676796,16.13784 + parent: 104 +- proto: FloraRockSolid03 + entities: + - uid: 1706 + components: + - type: Transform + pos: 13.550529,12.489372 + parent: 104 +- proto: FloraTreeConifer01 + entities: + - uid: 261 + components: + - type: Transform + pos: 7.7892694,4.987089 + parent: 104 + - uid: 275 + components: + - type: Transform + pos: -3.80645,4.798395 + parent: 104 + - uid: 281 + components: + - type: Transform + pos: -1.0279694,6.890992 + parent: 104 + - uid: 1619 + components: + - type: Transform + pos: 16.10984,7.3249645 + parent: 104 + - uid: 1622 + components: + - type: Transform + pos: 7.42463,16.597832 + parent: 104 + - uid: 1626 + components: + - type: Transform + pos: 11.605486,16.944057 + parent: 104 +- proto: FloraTreeConifer02 + entities: + - uid: 219 + components: + - type: Transform + pos: 5.5,4.5 + parent: 104 + - uid: 260 + components: + - type: Transform + pos: 7.7267694,7.536619 + parent: 104 + - uid: 267 + components: + - type: Transform + pos: 12.789269,3.6888618 + parent: 104 + - uid: 268 + components: + - type: Transform + pos: -0.17814255,7.5043383 + parent: 104 + - uid: 273 + components: + - type: Transform + pos: -2.591959,2.9214401 + parent: 104 + - uid: 280 + components: + - type: Transform + pos: 1.5111885,4.4898434 + parent: 104 + - uid: 1618 + components: + - type: Transform + pos: 11.32859,10.093473 + parent: 104 + - uid: 1620 + components: + - type: Transform + pos: 15.85984,13.16621 + parent: 104 + - uid: 1621 + components: + - type: Transform + pos: 14.582346,17.286049 + parent: 104 +- proto: FloraTreeConifer03 + entities: + - uid: 271 + components: + - type: Transform + pos: -1.1000175,4.26659 + parent: 104 + - uid: 1035 + components: + - type: Transform + pos: 0.74747276,19.424063 + parent: 104 + - uid: 1673 + components: + - type: Transform + pos: -3.5,15.5 + parent: 104 + - uid: 1830 + components: + - type: Transform + pos: 8.497473,19.862019 + parent: 104 +- proto: FloraTreeSnow01 + entities: + - uid: 262 + components: + - type: Transform + pos: 9.630766,6.7434845 + parent: 104 +- proto: FloraTreeSnow02 + entities: + - uid: 270 + components: + - type: Transform + pos: -2.3114033,5.0917635 + parent: 104 + - uid: 1630 + components: + - type: Transform + pos: 4.558771,16.696045 + parent: 104 +- proto: FloraTreeSnow03 + entities: + - uid: 274 + components: + - type: Transform + pos: -3.3325882,6.8936405 + parent: 104 + - uid: 1629 + components: + - type: Transform + pos: 9.924105,15.382175 + parent: 104 +- proto: FloraTreeSnow04 + entities: + - uid: 265 + components: + - type: Transform + pos: 0.24603653,5.7335367 + parent: 104 + - uid: 276 + components: + - type: Transform + pos: 5.8604717,5.2799397 + parent: 104 + - uid: 1624 + components: + - type: Transform + pos: -3.1344147,13.722986 + parent: 104 +- proto: FloraTreeSnow05 + entities: + - uid: 269 + components: + - type: Transform + pos: 0.30623245,3.9068413 + parent: 104 + - uid: 1627 + components: + - type: Transform + pos: 13.331816,13.977352 + parent: 104 + - uid: 1628 + components: + - type: Transform + pos: 15.066191,11.052429 + parent: 104 + - uid: 1631 + components: + - type: Transform + pos: 2.1525211,16.977589 + parent: 104 + - uid: 1672 + components: + - type: Transform + pos: -3.5,16.5 + parent: 104 +- proto: FoodBoxDonkpocket + entities: + - uid: 11 + components: + - type: Transform + pos: 12.527094,-6.308407 + parent: 104 +- proto: FoodBoxDonkpocketTeriyaki + entities: + - uid: 2475 + components: + - type: Transform + pos: 12.636917,-6.187726 + parent: 104 +- proto: FoodBoxDonut + entities: + - uid: 2445 + components: + - type: Transform + pos: 2.5389562,-6.3072195 + parent: 104 +- proto: FoodDonutJellySlugcat + entities: + - uid: 3327 + components: + - type: Transform + pos: -5.7255287,20.539352 + parent: 104 +- proto: GeneratorBasic15kW + entities: + - uid: 2326 + components: + - type: Transform + pos: 27.5,-19.5 + parent: 104 + - uid: 2328 + components: + - type: Transform + pos: 17.5,-17.5 + parent: 104 + - uid: 2329 + components: + - type: Transform + pos: 17.5,-19.5 + parent: 104 + - uid: 2330 + components: + - type: Transform + pos: 27.5,-17.5 + parent: 104 +- proto: GeneratorRTG + entities: + - uid: 2415 + components: + - type: Transform + pos: 17.5,-18.5 + parent: 104 + - uid: 2416 + components: + - type: Transform + pos: 27.5,-18.5 + parent: 104 +- proto: GravityGenerator + entities: + - uid: 2327 + components: + - type: Transform + pos: 22.5,-15.5 + parent: 104 + - type: GravityGenerator + charge: 100 + - type: PointLight + radius: 175.75 +- proto: GrenadeFlashBang + entities: + - uid: 2309 + components: + - type: Transform + pos: 11.399388,-16.363453 + parent: 104 + - uid: 2310 + components: + - type: Transform + pos: 11.571263,-16.363453 + parent: 104 +- proto: Grille + entities: + - uid: 95 + components: + - type: Transform + pos: 0.5,1.5 + parent: 104 + - uid: 115 + components: + - type: Transform + pos: -1.5,-0.5 + parent: 104 + - uid: 118 + components: + - type: Transform + pos: 2.5,1.5 + parent: 104 + - uid: 147 + components: + - type: Transform + pos: 11.5,1.5 + parent: 104 + - uid: 231 + components: + - type: Transform + pos: 2.5,2.5 + parent: 104 + - uid: 232 + components: + - type: Transform + pos: 2.5,3.5 + parent: 104 + - uid: 233 + components: + - type: Transform + pos: 4.5,2.5 + parent: 104 + - uid: 234 + components: + - type: Transform + pos: 4.5,3.5 + parent: 104 + - uid: 303 + components: + - type: Transform + pos: 16.5,-6.5 + parent: 104 + - uid: 304 + components: + - type: Transform + pos: 16.5,-8.5 + parent: 104 + - uid: 526 + components: + - type: Transform + pos: 0.5,21.5 + parent: 104 + - uid: 527 + components: + - type: Transform + pos: 6.5,21.5 + parent: 104 + - uid: 528 + components: + - type: Transform + pos: 1.5,21.5 + parent: 104 + - uid: 1034 + components: + - type: Transform + pos: 5.5,21.5 + parent: 104 + - uid: 1112 + components: + - type: Transform + pos: -1.5,-17.5 + parent: 104 + - uid: 1120 + components: + - type: Transform + pos: 2.5,-15.5 + parent: 104 + - uid: 1121 + components: + - type: Transform + pos: 2.5,-13.5 + parent: 104 + - uid: 1146 + components: + - type: Transform + pos: -12.5,5.5 + parent: 104 + - uid: 1148 + components: + - type: Transform + pos: -14.5,5.5 + parent: 104 + - uid: 1241 + components: + - type: Transform + pos: 5.5,-9.5 + parent: 104 + - uid: 1243 + components: + - type: Transform + pos: 7.5,-9.5 + parent: 104 + - uid: 1389 + components: + - type: Transform + pos: 4.5,1.5 + parent: 104 + - uid: 1410 + components: + - type: Transform + pos: 8.5,-17.5 + parent: 104 + - uid: 1823 + components: + - type: Transform + pos: 8.5,20.5 + parent: 104 + - uid: 1824 + components: + - type: Transform + pos: 4.5,21.5 + parent: 104 + - uid: 1831 + components: + - type: Transform + pos: 8.5,21.5 + parent: 104 + - uid: 1832 + components: + - type: Transform + pos: 3.5,21.5 + parent: 104 + - uid: 1840 + components: + - type: Transform + pos: 11.5,-23.5 + parent: 104 + - uid: 1841 + components: + - type: Transform + pos: 10.5,-23.5 + parent: 104 + - uid: 1847 + components: + - type: Transform + pos: 12.5,-23.5 + parent: 104 + - uid: 1854 + components: + - type: Transform + pos: 13.5,-23.5 + parent: 104 + - uid: 1860 + components: + - type: Transform + pos: 14.5,-23.5 + parent: 104 + - uid: 1931 + components: + - type: Transform + pos: 0.5,20.5 + parent: 104 + - uid: 1932 + components: + - type: Transform + pos: 7.5,21.5 + parent: 104 + - uid: 1933 + components: + - type: Transform + pos: 2.5,21.5 + parent: 104 + - uid: 2189 + components: + - type: Transform + pos: -0.5,-17.5 + parent: 104 + - uid: 2296 + components: + - type: Transform + pos: 0.5,-17.5 + parent: 104 + - uid: 2318 + components: + - type: Transform + pos: 11.5,-8.5 + parent: 104 + - uid: 2319 + components: + - type: Transform + pos: 11.5,-6.5 + parent: 104 +- proto: KitchenKnife + entities: + - uid: 1061 + components: + - type: Transform + pos: 18.918644,6.663283 + parent: 104 +- proto: SyndicateMicrowave + entities: + - uid: 10 + components: + - type: Transform + pos: 13.5,-6.5 + parent: 104 +- proto: KitchenReagentGrinder + entities: + - uid: 1257 + components: + - type: Transform + pos: 3.5,-10.5 + parent: 104 +- proto: LargeBeaker + entities: + - uid: 2021 + components: + - type: Transform + pos: 5.039936,-12.489611 + parent: 104 +- proto: LightPostSmall + entities: + - uid: 221 + components: + - type: Transform + pos: 15.5,16.5 + parent: 104 + - uid: 222 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -2.5,3.5 + parent: 104 + - uid: 291 + components: + - type: Transform + pos: 17.5,3.5 + parent: 104 + - uid: 540 + components: + - type: Transform + pos: 27.5,4.5 + parent: 104 + - uid: 541 + components: + - type: Transform + pos: 18.5,8.5 + parent: 104 + - uid: 629 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,8.5 + parent: 104 + - uid: 1226 + components: + - type: Transform + pos: -4.5,-0.5 + parent: 104 + - uid: 1276 + components: + - type: Transform + pos: -34.5,4.5 + parent: 104 + - uid: 1279 + components: + - type: Transform + pos: -35.5,-23.5 + parent: 104 + - uid: 1413 + components: + - type: Transform + pos: 0.5,-22.5 + parent: 104 + - uid: 1668 + components: + - type: Transform + pos: 5.5,17.5 + parent: 104 + - uid: 1669 + components: + - type: Transform + pos: 10.5,11.5 + parent: 104 + - uid: 1671 + components: + - type: Transform + pos: -3.5,17.5 + parent: 104 + - uid: 1675 + components: + - type: Transform + pos: 10.5,2.5 + parent: 104 +- proto: LockerSyndicatePersonalFilled + entities: + - uid: 178 + components: + - type: Transform + pos: 9.5,-13.5 + parent: 104 + - uid: 196 + components: + - type: Transform + pos: 11.5,-14.5 + parent: 104 + - uid: 197 + components: + - type: Transform + pos: 11.5,-13.5 + parent: 104 + - uid: 198 + components: + - type: Transform + pos: 9.5,-14.5 + parent: 104 + - uid: 1271 + components: + - type: Transform + pos: 13.5,-13.5 + parent: 104 + - uid: 1375 + components: + - type: Transform + pos: 13.5,-14.5 + parent: 104 + - uid: 2476 + components: + - type: Transform + pos: 15.5,-6.5 + parent: 104 +- proto: MachineCentrifuge + entities: + - uid: 1728 + components: + - type: Transform + pos: 3.5,-11.5 + parent: 104 +- proto: MachineElectrolysisUnit + entities: + - uid: 1718 + components: + - type: Transform + pos: 3.5,-12.5 + parent: 104 +- proto: MagazinePistolSubMachineGunPractice + entities: + - uid: 1648 + components: + - type: Transform + pos: 5.7430096,13.989216 + parent: 104 + - type: BallisticAmmoProvider + unspawnedCount: 35 +- proto: MedicalBed + entities: + - uid: 1868 + components: + - type: Transform + pos: 1.5,-16.5 + parent: 104 + - uid: 1874 + components: + - type: Transform + pos: -1.5,-12.5 + parent: 104 + - uid: 1890 + components: + - type: Transform + pos: 1.5,-12.5 + parent: 104 + - uid: 1891 + components: + - type: Transform + pos: -1.5,-16.5 + parent: 104 +- proto: MedkitAdvancedFilled + entities: + - uid: 447 + components: + - type: Transform + pos: 7.5183387,-10.279964 + parent: 104 +- proto: MedkitBruteFilled + entities: + - uid: 138 + components: + - type: Transform + pos: 7.5183387,-10.576839 + parent: 104 +- proto: MedkitBurnFilled + entities: + - uid: 1894 + components: + - type: Transform + pos: 7.5339637,-10.889339 + parent: 104 +- proto: MedkitFilled + entities: + - uid: 770 + components: + - type: Transform + pos: 0.48478004,-16.399067 + parent: 104 +- proto: MedkitOxygenFilled + entities: + - uid: 1895 + components: + - type: Transform + pos: 7.5339637,-11.186214 + parent: 104 +- proto: MedkitRadiationFilled + entities: + - uid: 448 + components: + - type: Transform + pos: 7.5495887,-11.733089 + parent: 104 +- proto: MedkitToxinFilled + entities: + - uid: 1388 + components: + - type: Transform + pos: 7.5339637,-11.436214 + parent: 104 +- proto: Mirror + entities: + - uid: 142 + components: + - type: Transform + pos: 9.5,1.5 + parent: 104 + - uid: 146 + components: + - type: Transform + pos: 10.5,1.5 + parent: 104 +- proto: ModularGrenade + entities: + - uid: 1326 + components: + - type: Transform + pos: 4.001747,-17.289776 + parent: 104 + - uid: 1328 + components: + - type: Transform + pos: 4.204872,-17.508755 + parent: 104 +- proto: MopBucket + entities: + - uid: 149 + components: + - type: Transform + pos: 11.5,-3.5 + parent: 104 +- proto: MopItem + entities: + - uid: 150 + components: + - type: Transform + pos: 11.5,-3.5 + parent: 104 +- proto: Multitool + entities: + - uid: 1555 + components: + - type: Transform + pos: 11.895346,-10.413807 + parent: 104 +- proto: NukeDiskFake + entities: + - uid: 282 + components: + - type: Transform + pos: 12.149857,15.427643 + parent: 104 +- proto: OperatingTable + entities: + - uid: 2435 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 104 +- proto: Paper + entities: + - uid: 612 + components: + - type: Transform + pos: -14.166584,9.634687 + parent: 104 +- proto: PartRodMetal + entities: + - uid: 2425 + components: + - type: Transform + pos: 24.5,-16.5 + parent: 104 + - uid: 2426 + components: + - type: Transform + pos: 24.5,-16.5 + parent: 104 +- proto: PhoneInstrumentSyndicate + entities: + - uid: 1684 + components: + - type: Transform + pos: 1.4424791,-6.340748 + parent: 104 +- proto: PianoInstrument + entities: + - uid: 2430 + components: + - type: Transform + pos: -0.5,-2.5 + parent: 104 +- proto: PlasmaReinforcedWindowDirectional + entities: + - uid: 1817 + components: + - type: Transform + pos: 13.5,-18.5 + parent: 104 + - uid: 1818 + components: + - type: Transform + pos: 12.5,-18.5 + parent: 104 + - uid: 1819 + components: + - type: Transform + pos: 11.5,-18.5 + parent: 104 + - uid: 1858 + components: + - type: Transform + pos: 14.5,-18.5 + parent: 104 +- proto: PlushieBee + entities: + - uid: 2317 + components: + - type: Transform + pos: 1.5,-12.5 + parent: 104 +- proto: PlushieLizard + entities: + - uid: 1266 + components: + - type: Transform + pos: -15.435635,22.055351 + parent: 104 + - uid: 2315 + components: + - type: Transform + pos: -1.5,-16.5 + parent: 104 + - uid: 3367 + components: + - type: Transform + pos: -4.605839,20.481245 + parent: 104 +- proto: PlushieNuke + entities: + - uid: 163 + components: + - type: Transform + pos: 1.8113766,-6.337348 + parent: 104 + - uid: 283 + components: + - type: Transform + pos: 12.912951,15.575253 + parent: 104 +- proto: PlushieRouny + entities: + - uid: 2316 + components: + - type: Transform + pos: -1.5,-12.5 + parent: 104 +- proto: PlushieVox + entities: + - uid: 2314 + components: + - type: Transform + pos: 1.5,-16.5 + parent: 104 +- proto: PortableScrubber + entities: + - uid: 811 + components: + - type: Transform + pos: 10.5,-10.5 + parent: 104 +- proto: PosterBroken + entities: + - uid: 227 + components: + - type: Transform + pos: -0.5,10.5 + parent: 104 +- proto: PosterContrabandC20r + entities: + - uid: 1556 + components: + - type: Transform + pos: 10.5,-9.5 + parent: 104 +- proto: PosterContrabandClown + entities: + - uid: 1681 + components: + - type: Transform + pos: 12.5,1.5 + parent: 104 +- proto: PosterContrabandCybersun600 + entities: + - uid: 2466 + components: + - type: Transform + pos: 8.5,-9.5 + parent: 104 +- proto: PosterContrabandDonk + entities: + - uid: 12 + components: + - type: Transform + pos: 12.5,-5.5 + parent: 104 + - uid: 2465 + components: + - type: Transform + pos: -10.5,8.5 + parent: 104 +- proto: PosterContrabandDonutCorp + entities: + - uid: 2450 + components: + - type: Transform + pos: -3.5,-1.5 + parent: 104 +- proto: PosterContrabandEnergySwords + entities: + - uid: 1384 + components: + - type: Transform + pos: 14.5,-9.5 + parent: 104 +- proto: PosterContrabandEnlistGorlex + entities: + - uid: 2461 + components: + - type: Transform + pos: -0.5,0.5 + parent: 104 +- proto: PosterContrabandInterdyne + entities: + - uid: 2464 + components: + - type: Transform + pos: 8.5,-12.5 + parent: 104 +- proto: PosterContrabandLustyExomorph + entities: + - uid: 754 + components: + - type: Transform + pos: -3.5,-13.5 + parent: 104 + - uid: 2447 + components: + - type: Transform + pos: 15.5,-15.5 + parent: 104 +- proto: PosterContrabandMoth + entities: + - uid: 2462 + components: + - type: Transform + pos: 8.5,-2.5 + parent: 104 +- proto: PosterContrabandNuclearDeviceInformational + entities: + - uid: 2298 + components: + - type: Transform + pos: -1.5,-5.5 + parent: 104 +- proto: PosterContrabandPower + entities: + - uid: 1563 + components: + - type: Transform + pos: 19.5,-12.5 + parent: 104 +- proto: PosterContrabandPunchShit + entities: + - uid: 2451 + components: + - type: Transform + pos: 25.5,-12.5 + parent: 104 +- proto: PosterContrabandRebelsUnite + entities: + - uid: 2446 + components: + - type: Transform + pos: 8.5,-3.5 + parent: 104 +- proto: PosterContrabandRedRum + entities: + - uid: 1387 + components: + - type: Transform + pos: 2.5,-9.5 + parent: 104 +- proto: PosterContrabandRevolver + entities: + - uid: 2452 + components: + - type: Transform + pos: -1.5,-7.5 + parent: 104 +- proto: PosterContrabandRobustSoftdrinks + entities: + - uid: 1245 + components: + - type: Transform + pos: -9.5,8.5 + parent: 104 +- proto: PosterContrabandSyndicatePistol + entities: + - uid: 1386 + components: + - type: Transform + pos: 12.5,-9.5 + parent: 104 +- proto: PosterContrabandSyndicateRecruitment + entities: + - uid: 1242 + components: + - type: Transform + pos: -15.5,7.5 + parent: 104 + - uid: 1561 + components: + - type: Transform + pos: 16.5,-11.5 + parent: 104 + - uid: 2453 + components: + - type: Transform + pos: -3.5,-5.5 + parent: 104 + - uid: 2454 + components: + - type: Transform + pos: 12.5,-15.5 + parent: 104 +- proto: PosterContrabandTheBigGasTruth + entities: + - uid: 2455 + components: + - type: Transform + pos: 10.5,-15.5 + parent: 104 +- proto: PosterContrabandVoteWeh + entities: + - uid: 1113 + components: + - type: Transform + pos: -3.5,-15.5 + parent: 104 + - uid: 1227 + components: + - type: Transform + pos: -17.5,24.5 + parent: 104 + - uid: 1228 + components: + - type: Transform + pos: -16.5,24.5 + parent: 104 + - uid: 1229 + components: + - type: Transform + pos: -15.5,24.5 + parent: 104 + - uid: 1230 + components: + - type: Transform + pos: -14.5,24.5 + parent: 104 + - uid: 1231 + components: + - type: Transform + pos: -13.5,24.5 + parent: 104 + - uid: 1248 + components: + - type: Transform + pos: -12.5,24.5 + parent: 104 + - uid: 1251 + components: + - type: Transform + pos: -12.5,23.5 + parent: 104 + - uid: 1252 + components: + - type: Transform + pos: -12.5,22.5 + parent: 104 + - uid: 1253 + components: + - type: Transform + pos: -12.5,21.5 + parent: 104 + - uid: 1254 + components: + - type: Transform + pos: -12.5,20.5 + parent: 104 + - uid: 1255 + components: + - type: Transform + pos: -12.5,19.5 + parent: 104 + - uid: 1256 + components: + - type: Transform + pos: -13.5,19.5 + parent: 104 + - uid: 1258 + components: + - type: Transform + pos: -14.5,19.5 + parent: 104 + - uid: 1259 + components: + - type: Transform + pos: -15.5,19.5 + parent: 104 + - uid: 1260 + components: + - type: Transform + pos: -16.5,19.5 + parent: 104 + - uid: 1261 + components: + - type: Transform + pos: -17.5,19.5 + parent: 104 + - uid: 1262 + components: + - type: Transform + pos: -17.5,20.5 + parent: 104 + - uid: 1263 + components: + - type: Transform + pos: -17.5,21.5 + parent: 104 + - uid: 1264 + components: + - type: Transform + pos: -17.5,22.5 + parent: 104 + - uid: 1265 + components: + - type: Transform + pos: -17.5,23.5 + parent: 104 + - uid: 1565 + components: + - type: Transform + pos: 12.5,-3.5 + parent: 104 +- proto: PosterContrabandWaffleCorp + entities: + - uid: 2463 + components: + - type: Transform + pos: 14.5,-2.5 + parent: 104 +- proto: PosterLegitAnatomyPoster + entities: + - uid: 2320 + components: + - type: Transform + pos: 2.5,-11.5 + parent: 104 +- proto: PosterLegitCleanliness + entities: + - uid: 955 + components: + - type: Transform + pos: 0.5,-11.5 + parent: 104 +- proto: PosterLegitIan + entities: + - uid: 1551 + components: + - type: Transform + pos: -0.5,12.5 + parent: 104 +- proto: PosterLegitIonRifle + entities: + - uid: 1558 + components: + - type: Transform + pos: 16.5,-14.5 + parent: 104 +- proto: PosterLegitNanotrasenLogo + entities: + - uid: 237 + components: + - type: Transform + pos: -0.5,14.5 + parent: 104 +- proto: PosterMapSplit + entities: + - uid: 350 + components: + - type: Transform + pos: 0.5,-9.5 + parent: 104 +- proto: PosterMapWaystation + entities: + - uid: 2449 + components: + - type: Transform + pos: 10.5,-5.5 + parent: 104 +- proto: PottedPlant24 + entities: + - uid: 1143 + components: + - type: Transform + pos: -14.5,7.5 + parent: 104 +- proto: PottedPlantBioluminscent + entities: + - uid: 7 + components: + - type: Transform + pos: 15.481667,-8.824032 + parent: 104 +- proto: PottedPlantRandom + entities: + - uid: 1656 + components: + - type: Transform + pos: 9.5,-1.5 + parent: 104 +- proto: PottedPlantRandomPlastic + entities: + - uid: 1654 + components: + - type: Transform + pos: 8.5,-6.5 + parent: 104 + - uid: 1655 + components: + - type: Transform + pos: 5.5,-8.5 + parent: 104 +- proto: Poweredlight + entities: + - uid: 1333 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 13.5,-0.5 + parent: 104 +- proto: PoweredLightPostSmall + entities: + - uid: 2525 + components: + - type: Transform + pos: -10.5,0.5 + parent: 104 + - uid: 2527 + components: + - type: Transform + pos: -31.5,0.5 + parent: 104 + - uid: 2528 + components: + - type: Transform + pos: -27.5,0.5 + parent: 104 + - uid: 2529 + components: + - type: Transform + pos: -23.5,0.5 + parent: 104 + - uid: 2530 + components: + - type: Transform + pos: -19.5,0.5 + parent: 104 + - uid: 2531 + components: + - type: Transform + pos: -15.5,0.5 + parent: 104 + - uid: 2532 + components: + - type: Transform + pos: -10.5,-19.5 + parent: 104 + - uid: 2533 + components: + - type: Transform + pos: -15.5,-19.5 + parent: 104 + - uid: 2534 + components: + - type: Transform + pos: -19.5,-19.5 + parent: 104 + - uid: 2535 + components: + - type: Transform + pos: -23.5,-19.5 + parent: 104 + - uid: 2536 + components: + - type: Transform + pos: -27.5,-19.5 + parent: 104 + - uid: 2537 + components: + - type: Transform + pos: -31.5,-19.5 + parent: 104 +- proto: PoweredSmallLight + entities: + - uid: 23 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 15.5,-1.5 + parent: 104 +- proto: Rack + entities: + - uid: 472 + components: + - type: Transform + pos: 11.5,-16.5 + parent: 104 + - uid: 1360 + components: + - type: Transform + pos: -2.5,-4.5 + parent: 104 +- proto: Railing + entities: + - uid: 253 + components: + - type: Transform + pos: -31.5,0.5 + parent: 104 + - uid: 446 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-6.5 + parent: 104 + - uid: 456 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-2.5 + parent: 104 + - uid: 457 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-3.5 + parent: 104 + - uid: 458 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-4.5 + parent: 104 + - uid: 459 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -8.5,5.5 + parent: 104 + - uid: 461 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-5.5 + parent: 104 + - uid: 481 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -8.5,6.5 + parent: 104 + - uid: 523 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -8.5,7.5 + parent: 104 + - uid: 585 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-22.5 + parent: 104 + - uid: 942 + components: + - type: Transform + pos: -20.5,0.5 + parent: 104 + - uid: 956 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -34.5,1.5 + parent: 104 + - uid: 1080 + components: + - type: Transform + pos: -34.5,-23.5 + parent: 104 + - uid: 1111 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-0.5 + parent: 104 + - uid: 1159 + components: + - type: Transform + pos: -22.5,-23.5 + parent: 104 + - uid: 1301 + components: + - type: Transform + pos: -28.5,-23.5 + parent: 104 + - uid: 1302 + components: + - type: Transform + pos: -21.5,-23.5 + parent: 104 + - uid: 1303 + components: + - type: Transform + pos: -26.5,-23.5 + parent: 104 + - uid: 1305 + components: + - type: Transform + pos: -24.5,-23.5 + parent: 104 + - uid: 1306 + components: + - type: Transform + pos: -20.5,-23.5 + parent: 104 + - uid: 1307 + components: + - type: Transform + pos: -19.5,-23.5 + parent: 104 + - uid: 1308 + components: + - type: Transform + pos: -18.5,-23.5 + parent: 104 + - uid: 1309 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -26.5,4.5 + parent: 104 + - uid: 1310 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -24.5,4.5 + parent: 104 + - uid: 1311 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -23.5,4.5 + parent: 104 + - uid: 1312 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -22.5,4.5 + parent: 104 + - uid: 1313 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -25.5,4.5 + parent: 104 + - uid: 1314 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -30.5,4.5 + parent: 104 + - uid: 1315 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -32.5,4.5 + parent: 104 + - uid: 1316 + components: + - type: Transform + pos: -32.5,-23.5 + parent: 104 + - uid: 1317 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -29.5,4.5 + parent: 104 + - uid: 1319 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -31.5,4.5 + parent: 104 + - uid: 1323 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -34.5,3.5 + parent: 104 + - uid: 1352 + components: + - type: Transform + pos: -23.5,-23.5 + parent: 104 + - uid: 1353 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -33.5,4.5 + parent: 104 + - uid: 1354 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -28.5,4.5 + parent: 104 + - uid: 1355 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -27.5,4.5 + parent: 104 + - uid: 1357 + components: + - type: Transform + pos: -27.5,-23.5 + parent: 104 + - uid: 1373 + components: + - type: Transform + pos: -31.5,-23.5 + parent: 104 + - uid: 1374 + components: + - type: Transform + pos: -29.5,-23.5 + parent: 104 + - uid: 1378 + components: + - type: Transform + pos: -25.5,-23.5 + parent: 104 + - uid: 1379 + components: + - type: Transform + pos: -30.5,-23.5 + parent: 104 + - uid: 1414 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-1.5 + parent: 104 + - uid: 1432 + components: + - type: Transform + pos: -33.5,-23.5 + parent: 104 + - uid: 1800 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-21.5 + parent: 104 + - uid: 1838 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-18.5 + parent: 104 + - uid: 1839 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-17.5 + parent: 104 + - uid: 1845 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-16.5 + parent: 104 + - uid: 1846 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-14.5 + parent: 104 + - uid: 1851 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-13.5 + parent: 104 + - uid: 1852 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-15.5 + parent: 104 + - uid: 1857 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-7.5 + parent: 104 + - uid: 1882 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -19.5,4.5 + parent: 104 + - uid: 1902 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -20.5,4.5 + parent: 104 + - uid: 1903 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -21.5,4.5 + parent: 104 + - uid: 1916 + components: + - type: Transform + pos: -15.5,-23.5 + parent: 104 + - uid: 1917 + components: + - type: Transform + pos: -14.5,-23.5 + parent: 104 + - uid: 1918 + components: + - type: Transform + pos: -13.5,-23.5 + parent: 104 + - uid: 1919 + components: + - type: Transform + pos: -12.5,-23.5 + parent: 104 + - uid: 1920 + components: + - type: Transform + pos: -11.5,-23.5 + parent: 104 + - uid: 1921 + components: + - type: Transform + pos: -17.5,-23.5 + parent: 104 + - uid: 1922 + components: + - type: Transform + pos: -16.5,-23.5 + parent: 104 + - uid: 1945 + components: + - type: Transform + pos: -10.5,-23.5 + parent: 104 + - uid: 1946 + components: + - type: Transform + pos: -9.5,-23.5 + parent: 104 + - uid: 1951 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -34.5,2.5 + parent: 104 + - uid: 2045 + components: + - type: Transform + pos: -32.5,0.5 + parent: 104 + - uid: 2083 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-9.5 + parent: 104 + - uid: 2089 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-12.5 + parent: 104 + - uid: 2097 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-8.5 + parent: 104 + - uid: 2126 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-10.5 + parent: 104 + - uid: 2135 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -10.5,-11.5 + parent: 104 + - uid: 2482 + components: + - type: Transform + pos: -30.5,0.5 + parent: 104 + - uid: 2483 + components: + - type: Transform + pos: -28.5,0.5 + parent: 104 + - uid: 2484 + components: + - type: Transform + pos: -27.5,0.5 + parent: 104 + - uid: 2485 + components: + - type: Transform + pos: -26.5,0.5 + parent: 104 + - uid: 2486 + components: + - type: Transform + pos: -24.5,0.5 + parent: 104 + - uid: 2487 + components: + - type: Transform + pos: -23.5,0.5 + parent: 104 + - uid: 2488 + components: + - type: Transform + pos: -22.5,0.5 + parent: 104 + - uid: 2489 + components: + - type: Transform + pos: -19.5,0.5 + parent: 104 + - uid: 2490 + components: + - type: Transform + pos: -18.5,0.5 + parent: 104 + - uid: 2491 + components: + - type: Transform + pos: -16.5,0.5 + parent: 104 + - uid: 2492 + components: + - type: Transform + pos: -15.5,0.5 + parent: 104 + - uid: 2493 + components: + - type: Transform + pos: -14.5,0.5 + parent: 104 + - uid: 2494 + components: + - type: Transform + pos: -12.5,0.5 + parent: 104 + - uid: 2495 + components: + - type: Transform + pos: -11.5,0.5 + parent: 104 + - uid: 2497 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -11.5,-19.5 + parent: 104 + - uid: 2498 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -12.5,-19.5 + parent: 104 + - uid: 2499 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -14.5,-19.5 + parent: 104 + - uid: 2500 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -15.5,-19.5 + parent: 104 + - uid: 2501 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -16.5,-19.5 + parent: 104 + - uid: 2502 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -18.5,-19.5 + parent: 104 + - uid: 2503 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -19.5,-19.5 + parent: 104 + - uid: 2504 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -20.5,-19.5 + parent: 104 + - uid: 2506 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -22.5,-19.5 + parent: 104 + - uid: 2509 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -23.5,-19.5 + parent: 104 + - uid: 2510 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -24.5,-19.5 + parent: 104 + - uid: 2511 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -26.5,-19.5 + parent: 104 + - uid: 2512 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -27.5,-19.5 + parent: 104 + - uid: 2513 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -28.5,-19.5 + parent: 104 + - uid: 2516 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -30.5,-19.5 + parent: 104 + - uid: 2517 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -31.5,-19.5 + parent: 104 + - uid: 2518 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -32.5,-19.5 + parent: 104 + - uid: 2519 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -34.5,-19.5 + parent: 104 + - uid: 2520 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-20.5 + parent: 104 +- proto: RailingCorner + entities: + - uid: 1790 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -35.5,-23.5 + parent: 104 + - uid: 1950 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -34.5,4.5 + parent: 104 + - uid: 2496 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -34.5,0.5 + parent: 104 + - uid: 2522 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -35.5,-19.5 + parent: 104 +- proto: RailingCornerSmall + entities: + - uid: 1285 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -8.5,-23.5 + parent: 104 + - uid: 1761 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -8.5,8.5 + parent: 104 + - uid: 1869 + components: + - type: Transform + pos: -8.5,-24.5 + parent: 104 + - uid: 1873 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -9.5,-24.5 + parent: 104 + - uid: 1883 + components: + - type: Transform + pos: -18.5,4.5 + parent: 104 + - uid: 2523 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -10.5,0.5 + parent: 104 + - uid: 2524 + components: + - type: Transform + pos: -10.5,-19.5 + parent: 104 +- proto: ReinforcedPlasmaWindow + entities: + - uid: 1842 + components: + - type: Transform + pos: 12.5,-23.5 + parent: 104 + - uid: 1848 + components: + - type: Transform + pos: 14.5,-23.5 + parent: 104 + - uid: 1849 + components: + - type: Transform + pos: 11.5,-23.5 + parent: 104 + - uid: 1853 + components: + - type: Transform + pos: 13.5,-23.5 + parent: 104 + - uid: 1855 + components: + - type: Transform + pos: 10.5,-23.5 + parent: 104 +- proto: ReinforcedWindow + entities: + - uid: 52 + components: + - type: Transform + pos: 16.5,-6.5 + parent: 104 + - uid: 73 + components: + - type: Transform + pos: 11.5,1.5 + parent: 104 + - uid: 89 + components: + - type: Transform + pos: 7.5,-9.5 + parent: 104 + - uid: 99 + components: + - type: Transform + pos: 0.5,1.5 + parent: 104 + - uid: 103 + components: + - type: Transform + pos: 4.5,1.5 + parent: 104 + - uid: 127 + components: + - type: Transform + pos: 11.5,-8.5 + parent: 104 + - uid: 128 + components: + - type: Transform + pos: 11.5,-6.5 + parent: 104 + - uid: 129 + components: + - type: Transform + pos: 16.5,-8.5 + parent: 104 + - uid: 155 + components: + - type: Transform + pos: 2.5,3.5 + parent: 104 + - uid: 223 + components: + - type: Transform + pos: 4.5,2.5 + parent: 104 + - uid: 224 + components: + - type: Transform + pos: 4.5,3.5 + parent: 104 + - uid: 353 + components: + - type: Transform + pos: 2.5,2.5 + parent: 104 + - uid: 498 + components: + - type: Transform + pos: -1.5,-17.5 + parent: 104 + - uid: 530 + components: + - type: Transform + pos: 8.5,20.5 + parent: 104 + - uid: 649 + components: + - type: Transform + pos: -12.5,5.5 + parent: 104 + - uid: 1037 + components: + - type: Transform + pos: 1.5,21.5 + parent: 104 + - uid: 1038 + components: + - type: Transform + pos: 6.5,21.5 + parent: 104 + - uid: 1039 + components: + - type: Transform + pos: 8.5,21.5 + parent: 104 + - uid: 1040 + components: + - type: Transform + pos: 7.5,21.5 + parent: 104 + - uid: 1102 + components: + - type: Transform + pos: 8.5,-17.5 + parent: 104 + - uid: 1103 + components: + - type: Transform + pos: 2.5,21.5 + parent: 104 + - uid: 1118 + components: + - type: Transform + pos: 0.5,-17.5 + parent: 104 + - uid: 1142 + components: + - type: Transform + pos: -14.5,5.5 + parent: 104 + - uid: 1186 + components: + - type: Transform + pos: 2.5,-15.5 + parent: 104 + - uid: 1196 + components: + - type: Transform + pos: -0.5,-17.5 + parent: 104 + - uid: 1198 + components: + - type: Transform + pos: 2.5,-13.5 + parent: 104 + - uid: 1382 + components: + - type: Transform + pos: -1.5,-0.5 + parent: 104 + - uid: 1385 + components: + - type: Transform + pos: 2.5,1.5 + parent: 104 + - uid: 1820 + components: + - type: Transform + pos: 5.5,21.5 + parent: 104 + - uid: 1821 + components: + - type: Transform + pos: 0.5,21.5 + parent: 104 + - uid: 1828 + components: + - type: Transform + pos: 4.5,21.5 + parent: 104 + - uid: 1829 + components: + - type: Transform + pos: 0.5,20.5 + parent: 104 + - uid: 1929 + components: + - type: Transform + pos: 3.5,21.5 + parent: 104 + - uid: 2081 + components: + - type: Transform + pos: 5.5,-9.5 + parent: 104 +- proto: RemoteSignaller + entities: + - uid: 1322 + components: + - type: Transform + pos: 5.611122,-17.316748 + parent: 104 + - uid: 1332 + components: + - type: Transform + pos: 5.267372,-17.316748 + parent: 104 +- proto: Screwdriver + entities: + - uid: 1325 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 4.954872,-17.44188 + parent: 104 +- proto: SheetGlass + entities: + - uid: 2421 + components: + - type: Transform + pos: 20.5,-16.5 + parent: 104 + - uid: 2422 + components: + - type: Transform + pos: 20.5,-16.5 + parent: 104 +- proto: SheetPlasteel + entities: + - uid: 2423 + components: + - type: Transform + pos: 24.5,-15.5 + parent: 104 + - uid: 2424 + components: + - type: Transform + pos: 24.5,-15.5 + parent: 104 +- proto: SheetPlastic + entities: + - uid: 460 + components: + - type: Transform + pos: 12.60107,-16.44067 + parent: 104 +- proto: SheetSteel + entities: + - uid: 286 + components: + - type: Transform + pos: 13.50732,-16.456295 + parent: 104 + - uid: 1350 + components: + - type: Transform + pos: 3.460312,-15.657375 + parent: 104 + - uid: 2419 + components: + - type: Transform + pos: 20.5,-15.5 + parent: 104 + - uid: 2420 + components: + - type: Transform + pos: 20.5,-15.5 + parent: 104 +- proto: SignalTrigger + entities: + - uid: 1330 + components: + - type: Transform + pos: 3.6284866,-16.955233 + parent: 104 + - uid: 1331 + components: + - type: Transform + pos: 3.3003616,-17.06472 + parent: 104 +- proto: SignDirectionalBar + entities: + - uid: 2297 + components: + - type: Transform + pos: -1.5,-4.5 + parent: 104 +- proto: SignEscapePods + entities: + - uid: 1737 + components: + - type: MetaData + desc: steel rain babey + name: drop pod sign + - type: Transform + rot: 1.5707963267948966 rad + pos: 16.5,-8.5 + parent: 104 +- proto: SignMedical + entities: + - uid: 351 + components: + - type: Transform + pos: 4.5,-9.5 + parent: 104 +- proto: SignSurgery + entities: + - uid: 266 + components: + - type: Transform + pos: 2.5,-12.5 + parent: 104 +- proto: SignToxins2 + entities: + - uid: 1318 + components: + - type: Transform + pos: 8.5,-15.5 + parent: 104 +- proto: SinkStemlessWater + entities: + - uid: 22 + components: + - type: Transform + pos: 15.5,-1.5 + parent: 104 +- proto: SinkWide + entities: + - uid: 144 + components: + - type: Transform + pos: 9.5,0.5 + parent: 104 + - uid: 145 + components: + - type: Transform + pos: 10.5,0.5 + parent: 104 + - uid: 1246 + components: + - type: Transform + pos: 9.5,-3.5 + parent: 104 + - uid: 1358 + components: + - type: Transform + pos: -4.5,-2.5 + parent: 104 + - uid: 2311 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-7.5 + parent: 104 +- proto: SmallLight + entities: + - uid: 484 + components: + - type: Transform + pos: -12.5,7.5 + parent: 104 + - uid: 1024 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-6.5 + parent: 104 + - uid: 1141 + components: + - type: Transform + pos: -13.5,9.5 + parent: 104 + - uid: 1766 + components: + - type: Transform + pos: 11.5,-16.5 + parent: 104 +- proto: SMESBasic + entities: + - uid: 2331 + components: + - type: Transform + pos: 21.5,-18.5 + parent: 104 + - uid: 2332 + components: + - type: Transform + pos: 22.5,-18.5 + parent: 104 + - uid: 2333 + components: + - type: Transform + pos: 23.5,-18.5 + parent: 104 +- proto: soda_dispenser + entities: + - uid: 225 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-8.5 + parent: 104 +- proto: SpaceVillainArcadeFilled + entities: + - uid: 1652 + components: + - type: Transform + pos: 10.5,-6.5 + parent: 104 + - uid: 1653 + components: + - type: Transform + pos: 9.5,-6.5 + parent: 104 +- proto: SpawnMobLizard + entities: + - uid: 2473 + components: + - type: Transform + pos: 5.5,-1.5 + parent: 104 +- proto: SpawnPointNukies + entities: + - uid: 167 + components: + - type: Transform + pos: 3.5,-5.5 + parent: 104 + - uid: 172 + components: + - type: Transform + pos: 3.5,-7.5 + parent: 104 + - uid: 173 + components: + - type: Transform + pos: 0.5,-5.5 + parent: 104 + - uid: 2477 + components: + - type: Transform + pos: 4.5,-3.5 + parent: 104 + - uid: 2478 + components: + - type: Transform + pos: 3.5,-1.5 + parent: 104 + - uid: 2479 + components: + - type: Transform + pos: 2.5,-1.5 + parent: 104 + - uid: 2480 + components: + - type: Transform + pos: 1.5,-2.5 + parent: 104 +- proto: Spear + entities: + - uid: 3366 + components: + - type: Transform + pos: -5.3505287,20.508072 + parent: 104 +- proto: SprayBottle + entities: + - uid: 1699 + components: + - type: Transform + pos: 14.060668,-16.439913 + parent: 104 +- proto: StasisBed + entities: + - uid: 1130 + components: + - type: Transform + pos: -2.5,-14.5 + parent: 104 +- proto: Stool + entities: + - uid: 156 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-3.5 + parent: 104 + - uid: 158 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-2.5 + parent: 104 + - uid: 164 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-3.5 + parent: 104 + - uid: 166 + components: + - type: Transform + pos: 2.5,-1.5 + parent: 104 + - uid: 169 + components: + - type: Transform + pos: 3.5,-1.5 + parent: 104 + - uid: 194 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 10.5,-13.5 + parent: 104 + - uid: 195 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 12.5,-14.5 + parent: 104 + - uid: 200 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 10.5,-14.5 + parent: 104 + - uid: 507 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 11.5,-1.5 + parent: 104 + - uid: 1334 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 12.5,-1.5 + parent: 104 + - uid: 1369 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 14.5,-13.5 + parent: 104 + - uid: 1371 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 12.5,-13.5 + parent: 104 + - uid: 1372 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 14.5,-14.5 + parent: 104 +- proto: StoolBar + entities: + - uid: 1 + components: + - type: Transform + pos: -0.5,-5.5 + parent: 104 + - uid: 2 + components: + - type: Transform + pos: 3.5,-5.5 + parent: 104 + - uid: 3 + components: + - type: Transform + pos: 0.5,-5.5 + parent: 104 + - uid: 4 + components: + - type: Transform + pos: 1.5,-5.5 + parent: 104 + - uid: 5 + components: + - type: Transform + pos: 2.5,-5.5 + parent: 104 + - uid: 6 + components: + - type: Transform + pos: 4.5,-5.5 + parent: 104 +- proto: SubstationBasic + entities: + - uid: 2366 + components: + - type: Transform + pos: 27.5,-13.5 + parent: 104 +- proto: SurveillanceCameraGeneral + entities: + - uid: 85 + components: + - type: Transform + pos: 1.5,-8.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraGeneral + nameSet: True + id: Bar + - uid: 1144 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -6.5,-16.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraGeneral + nameSet: True + id: Dock + - uid: 1185 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -10.5,7.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraGeneral + nameSet: True + id: Surveillance Shack + - uid: 1268 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 15.5,-12.5 + parent: 104 + - uid: 1269 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-12.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraGeneral + nameSet: True + id: Medbay + - uid: 1270 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 7.5,-12.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraGeneral + nameSet: True + id: Chem and Bombs + - uid: 1272 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 13.5,-0.5 + parent: 104 + - uid: 1274 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -3.5,-2.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraGeneral + nameSet: True + id: Entrance + - uid: 1320 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 11.5,-4.5 + parent: 104 + - uid: 1321 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 14.5,-3.5 + parent: 104 +- proto: SurveillanceCameraRouterGeneral + entities: + - uid: 620 + components: + - type: Transform + pos: -15.5,9.5 + parent: 104 +- proto: SurveillanceCameraWirelessRouterEntertainment + entities: + - uid: 978 + components: + - type: Transform + pos: -15.5,8.5 + parent: 104 +- proto: SurveillanceWirelessCameraAnchoredEntertainment + entities: + - uid: 1023 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -13.5,21.5 + parent: 104 + - type: SurveillanceCamera + setupAvailableNetworks: + - SurveillanceCameraEntertainment + nameSet: True + id: Weeh +- proto: SyndicatePersonalAI + entities: + - uid: 160 + components: + - type: Transform + pos: 1.4711013,-3.6405277 + parent: 104 +- proto: SyndieFlag + entities: + - uid: 819 + components: + - type: Transform + pos: 11.5,-9.5 + parent: 104 +- proto: SyndieHandyFlag + entities: + - uid: 1810 + components: + - type: Transform + pos: 3.5076954,-6.413422 + parent: 104 +- proto: SyringeInaprovaline + entities: + - uid: 1151 + components: + - type: Transform + pos: -2.530845,-12.414692 + parent: 104 +- proto: Table + entities: + - uid: 16 + components: + - type: Transform + pos: 12.5,-6.5 + parent: 104 + - uid: 107 + components: + - type: Transform + pos: 7.5,-4.5 + parent: 104 + - uid: 1090 + components: + - type: Transform + pos: 7.5,-11.5 + parent: 104 + - uid: 1193 + components: + - type: Transform + pos: 7.5,-12.5 + parent: 104 + - uid: 1300 + components: + - type: Transform + pos: 7.5,-10.5 + parent: 104 + - uid: 1813 + components: + - type: Transform + pos: 12.5,-16.5 + parent: 104 + - uid: 1814 + components: + - type: Transform + pos: 13.5,-16.5 + parent: 104 + - uid: 1815 + components: + - type: Transform + pos: 14.5,-16.5 + parent: 104 + - uid: 1859 + components: + - type: Transform + pos: 14.5,-17.5 + parent: 104 + - uid: 2321 + components: + - type: Transform + pos: 13.5,-6.5 + parent: 104 +- proto: TableCarpet + entities: + - uid: 9 + components: + - type: Transform + pos: 13.5,-8.5 + parent: 104 +- proto: TableGlass + entities: + - uid: 980 + components: + - type: Transform + pos: -2.5,-16.5 + parent: 104 + - uid: 1125 + components: + - type: Transform + pos: -2.5,-12.5 + parent: 104 + - uid: 2090 + components: + - type: Transform + pos: 0.5,-16.5 + parent: 104 + - uid: 2092 + components: + - type: Transform + pos: 0.5,-12.5 + parent: 104 +- proto: TablePlasmaGlass + entities: + - uid: 2411 + components: + - type: Transform + pos: 20.5,-16.5 + parent: 104 + - uid: 2412 + components: + - type: Transform + pos: 20.5,-15.5 + parent: 104 + - uid: 2413 + components: + - type: Transform + pos: 24.5,-16.5 + parent: 104 + - uid: 2414 + components: + - type: Transform + pos: 24.5,-15.5 + parent: 104 + - uid: 2431 + components: + - type: Transform + pos: 17.5,-16.5 + parent: 104 + - uid: 2432 + components: + - type: Transform + pos: 17.5,-15.5 + parent: 104 + - uid: 2433 + components: + - type: Transform + pos: 27.5,-16.5 + parent: 104 + - uid: 2434 + components: + - type: Transform + pos: 27.5,-15.5 + parent: 104 +- proto: TableReinforced + entities: + - uid: 201 + components: + - type: Transform + pos: 13.5,-10.5 + parent: 104 + - uid: 202 + components: + - type: Transform + pos: 12.5,-10.5 + parent: 104 + - uid: 203 + components: + - type: Transform + pos: 14.5,-10.5 + parent: 104 + - uid: 208 + components: + - type: Transform + pos: 11.5,-10.5 + parent: 104 + - uid: 226 + components: + - type: Transform + pos: 5.5,10.5 + parent: 104 + - uid: 229 + components: + - type: Transform + pos: 5.5,11.5 + parent: 104 + - uid: 239 + components: + - type: Transform + pos: 5.5,13.5 + parent: 104 + - uid: 241 + components: + - type: Transform + pos: 5.5,14.5 + parent: 104 + - uid: 285 + components: + - type: Transform + pos: 5.5,12.5 + parent: 104 + - uid: 697 + components: + - type: Transform + pos: -13.5,5.5 + parent: 104 + - uid: 1129 + components: + - type: Transform + pos: -14.5,9.5 + parent: 104 + - uid: 1149 + components: + - type: Transform + pos: -13.5,9.5 + parent: 104 +- proto: TableReinforcedGlass + entities: + - uid: 838 + components: + - type: Transform + pos: 5.5,-17.5 + parent: 104 + - uid: 975 + components: + - type: Transform + pos: 4.5,-17.5 + parent: 104 + - uid: 976 + components: + - type: Transform + pos: 3.5,-17.5 + parent: 104 + - uid: 1192 + components: + - type: Transform + pos: 3.5,-16.5 + parent: 104 + - uid: 1351 + components: + - type: Transform + pos: 3.5,-15.5 + parent: 104 + - uid: 1739 + components: + - type: Transform + pos: 5.5,-12.5 + parent: 104 + - uid: 2016 + components: + - type: Transform + pos: 4.5,-12.5 + parent: 104 + - uid: 2022 + components: + - type: Transform + pos: 3.5,-10.5 + parent: 104 + - uid: 2027 + components: + - type: Transform + pos: 3.5,-11.5 + parent: 104 + - uid: 2028 + components: + - type: Transform + pos: 3.5,-12.5 + parent: 104 +- proto: TableWood + entities: + - uid: 8 + components: + - type: Transform + pos: 3.5,-3.5 + parent: 104 + - uid: 28 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 2.5,-8.5 + parent: 104 + - uid: 30 + components: + - type: Transform + pos: 3.5,-8.5 + parent: 104 + - uid: 31 + components: + - type: Transform + pos: 4.5,-8.5 + parent: 104 + - uid: 32 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 4.5,-6.5 + parent: 104 + - uid: 33 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 3.5,-6.5 + parent: 104 + - uid: 34 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.5,-6.5 + parent: 104 + - uid: 35 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-6.5 + parent: 104 + - uid: 36 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-6.5 + parent: 104 + - uid: 37 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-6.5 + parent: 104 + - uid: 159 + components: + - type: Transform + pos: 2.5,-3.5 + parent: 104 + - uid: 162 + components: + - type: Transform + pos: 2.5,-2.5 + parent: 104 + - uid: 355 + components: + - type: Transform + pos: 3.5,-2.5 + parent: 104 +- proto: TargetClown + entities: + - uid: 1291 + components: + - type: Transform + pos: 0.5,12.5 + parent: 104 +- proto: TargetHuman + entities: + - uid: 1293 + components: + - type: Transform + pos: 0.5,14.5 + parent: 104 + - uid: 2308 + components: + - type: Transform + pos: 12.5,-21.5 + parent: 104 +- proto: TargetStrange + entities: + - uid: 1294 + components: + - type: Transform + pos: 0.5,10.5 + parent: 104 +- proto: TearGasGrenade + entities: + - uid: 306 + components: + - type: Transform + pos: 14.638793,-16.908663 + parent: 104 + - uid: 1703 + components: + - type: Transform + pos: 14.341918,-16.877413 + parent: 104 +- proto: Telecrystal1 + entities: + - uid: 2474 + components: + - type: Transform + pos: 2.9398513,-3.4842777 + parent: 104 +- proto: TimerTrigger + entities: + - uid: 1225 + components: + - type: Transform + pos: 3.7048721,-16.445147 + parent: 104 + - uid: 1329 + components: + - type: Transform + pos: 3.3003616,-16.470352 + parent: 104 +- proto: ToiletEmpty + entities: + - uid: 293 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 15.5,-1.5 + parent: 104 +- proto: ToolboxElectricalFilled + entities: + - uid: 206 + components: + - type: Transform + pos: 13.241143,-10.4236145 + parent: 104 +- proto: ToolboxEmergencyFilled + entities: + - uid: 1368 + components: + - type: Transform + pos: -2.4700382,-4.362167 + parent: 104 +- proto: ToolboxSyndicateFilled + entities: + - uid: 205 + components: + - type: Transform + pos: 12.366143,-10.392332 + parent: 104 +- proto: ToyFigurineCaptain + entities: + - uid: 2470 + components: + - type: Transform + pos: 2.5657434,-3.260709 + parent: 104 +- proto: ToyFigurineNukie + entities: + - uid: 14 + components: + - type: Transform + pos: 3.4242263,-2.6717777 + parent: 104 + - uid: 161 + components: + - type: Transform + pos: 3.5492263,-2.4374027 + parent: 104 + - uid: 2468 + components: + - type: Transform + pos: 3.6429763,-2.6717777 + parent: 104 +- proto: ToyFigurineNukieCommander + entities: + - uid: 2471 + components: + - type: Transform + pos: 3.1898513,-2.9530277 + parent: 104 +- proto: ToyFigurineNukieElite + entities: + - uid: 2467 + components: + - type: Transform + pos: 3.1117263,-2.6874027 + parent: 104 +- proto: ToyMauler + entities: + - uid: 2469 + components: + - type: Transform + pos: 3.0179763,-2.3124027 + parent: 104 +- proto: ToyNuke + entities: + - uid: 26 + components: + - type: Transform + pos: 4.4214306,-6.3946886 + parent: 104 + - uid: 284 + components: + - type: Transform + pos: 12.453509,14.985125 + parent: 104 +- proto: ToySkeleton + entities: + - uid: 17 + components: + - type: Transform + pos: 3.5882664,-8.344303 + parent: 104 +- proto: VariantCubeBox + entities: + - uid: 1826 + components: + - type: Transform + pos: 14.485744,-16.427467 + parent: 104 +- proto: VendingMachineBooze + entities: + - uid: 1380 + components: + - type: Transform + pos: -0.5,-8.5 + parent: 104 +- proto: VendingMachineChang + entities: + - uid: 1208 + components: + - type: Transform + pos: -10.5,7.5 + parent: 104 +- proto: VendingMachineChemicalsSyndicate + entities: + - uid: 1335 + components: + - type: Transform + pos: 4.5,-10.5 + parent: 104 +- proto: VendingMachineCigs + entities: + - uid: 117 + components: + - type: MetaData + name: cigarette machine + - type: Transform + pos: 7.5,0.5 + parent: 104 +- proto: VendingMachineClothing + entities: + - uid: 1617 + components: + - type: Transform + pos: 13.5,0.5 + parent: 104 +- proto: VendingMachineCola + entities: + - uid: 116 + components: + - type: Transform + pos: 5.5,0.5 + parent: 104 + - uid: 1244 + components: + - type: Transform + pos: -9.5,7.5 + parent: 104 +- proto: VendingMachineSnack + entities: + - uid: 2442 + components: + - type: Transform + pos: 6.5,0.5 + parent: 104 +- proto: VendingMachineSyndieDrobe + entities: + - uid: 1299 + components: + - type: Transform + pos: 11.5,0.5 + parent: 104 +- proto: VendingMachineTheater + entities: + - uid: 1680 + components: + - type: Transform + pos: 12.5,0.5 + parent: 104 +- proto: VendingMachineYouTool + entities: + - uid: 204 + components: + - type: Transform + pos: 15.5,-10.5 + parent: 104 +- proto: WallIce + entities: + - uid: 236 + components: + - type: Transform + pos: -0.5,12.5 + parent: 104 + - uid: 356 + components: + - type: Transform + pos: -6.5,21.5 + parent: 104 + - uid: 357 + components: + - type: Transform + pos: -3.5,21.5 + parent: 104 + - uid: 416 + components: + - type: Transform + pos: -4.5,21.5 + parent: 104 + - uid: 727 + components: + - type: Transform + pos: -6.5,19.5 + parent: 104 + - uid: 1132 + components: + - type: Transform + pos: -4.5,22.5 + parent: 104 + - uid: 1133 + components: + - type: Transform + pos: -5.5,22.5 + parent: 104 + - uid: 1134 + components: + - type: Transform + pos: -6.5,22.5 + parent: 104 + - uid: 1221 + components: + - type: Transform + pos: -6.5,20.5 + parent: 104 + - uid: 1549 + components: + - type: Transform + pos: -0.5,10.5 + parent: 104 + - uid: 1670 + components: + - type: Transform + pos: -0.5,14.5 + parent: 104 +- proto: WallmountTelevision + entities: + - uid: 977 + components: + - type: Transform + pos: 6.5,1.5 + parent: 104 +- proto: WallPlastitanium + entities: + - uid: 38 + components: + - type: Transform + pos: 12.5,-4.5 + parent: 104 + - uid: 39 + components: + - type: Transform + pos: 12.5,-3.5 + parent: 104 + - uid: 40 + components: + - type: Transform + pos: 9.5,-5.5 + parent: 104 + - uid: 41 + components: + - type: Transform + pos: 10.5,-5.5 + parent: 104 + - uid: 42 + components: + - type: Transform + pos: 11.5,-5.5 + parent: 104 + - uid: 43 + components: + - type: Transform + pos: 12.5,-5.5 + parent: 104 + - uid: 44 + components: + - type: Transform + pos: 12.5,-5.5 + parent: 104 + - uid: 45 + components: + - type: Transform + pos: 16.5,-5.5 + parent: 104 + - uid: 46 + components: + - type: Transform + pos: 20.5,-2.5 + parent: 104 + - uid: 47 + components: + - type: Transform + pos: 22.5,-2.5 + parent: 104 + - uid: 50 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 17.5,-12.5 + parent: 104 + - uid: 51 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 19.5,-12.5 + parent: 104 + - uid: 53 + components: + - type: Transform + pos: 17.5,-2.5 + parent: 104 + - uid: 54 + components: + - type: Transform + pos: 18.5,-2.5 + parent: 104 + - uid: 57 + components: + - type: Transform + pos: 13.5,-5.5 + parent: 104 + - uid: 59 + components: + - type: Transform + pos: 8.5,-5.5 + parent: 104 + - uid: 60 + components: + - type: Transform + pos: 8.5,-4.5 + parent: 104 + - uid: 61 + components: + - type: Transform + pos: 8.5,-3.5 + parent: 104 + - uid: 62 + components: + - type: Transform + pos: 12.5,-9.5 + parent: 104 + - uid: 63 + components: + - type: Transform + pos: 11.5,-9.5 + parent: 104 + - uid: 64 + components: + - type: Transform + pos: 10.5,-9.5 + parent: 104 + - uid: 66 + components: + - type: Transform + pos: 8.5,-1.5 + parent: 104 + - uid: 67 + components: + - type: Transform + pos: 8.5,-2.5 + parent: 104 + - uid: 68 + components: + - type: Transform + pos: 9.5,-2.5 + parent: 104 + - uid: 69 + components: + - type: Transform + pos: 11.5,-2.5 + parent: 104 + - uid: 70 + components: + - type: Transform + pos: 14.5,1.5 + parent: 104 + - uid: 71 + components: + - type: Transform + pos: 13.5,1.5 + parent: 104 + - uid: 72 + components: + - type: Transform + pos: 12.5,1.5 + parent: 104 + - uid: 74 + components: + - type: Transform + pos: 10.5,1.5 + parent: 104 + - uid: 75 + components: + - type: Transform + pos: 9.5,1.5 + parent: 104 + - uid: 76 + components: + - type: Transform + pos: 8.5,0.5 + parent: 104 + - uid: 77 + components: + - type: Transform + pos: 8.5,1.5 + parent: 104 + - uid: 78 + components: + - type: Transform + pos: 7.5,1.5 + parent: 104 + - uid: 79 + components: + - type: Transform + pos: 8.5,-11.5 + parent: 104 + - uid: 80 + components: + - type: Transform + pos: 8.5,-9.5 + parent: 104 + - uid: 82 + components: + - type: Transform + pos: 1.5,-9.5 + parent: 104 + - uid: 83 + components: + - type: Transform + pos: 0.5,-9.5 + parent: 104 + - uid: 84 + components: + - type: Transform + pos: -0.5,-9.5 + parent: 104 + - uid: 86 + components: + - type: Transform + pos: 2.5,-9.5 + parent: 104 + - uid: 87 + components: + - type: Transform + pos: 3.5,-9.5 + parent: 104 + - uid: 88 + components: + - type: Transform + pos: 4.5,-9.5 + parent: 104 + - uid: 90 + components: + - type: Transform + pos: -1.5,-8.5 + parent: 104 + - uid: 91 + components: + - type: Transform + pos: -1.5,-7.5 + parent: 104 + - uid: 92 + components: + - type: Transform + pos: -1.5,-6.5 + parent: 104 + - uid: 93 + components: + - type: Transform + pos: -1.5,-5.5 + parent: 104 + - uid: 94 + components: + - type: Transform + pos: -1.5,-1.5 + parent: 104 + - uid: 97 + components: + - type: Transform + pos: 6.5,1.5 + parent: 104 + - uid: 98 + components: + - type: Transform + pos: 5.5,1.5 + parent: 104 + - uid: 101 + components: + - type: Transform + pos: -0.5,0.5 + parent: 104 + - uid: 102 + components: + - type: Transform + pos: -0.5,1.5 + parent: 104 + - uid: 105 + components: + - type: Transform + pos: -1.5,0.5 + parent: 104 + - uid: 106 + components: + - type: Transform + pos: 14.5,0.5 + parent: 104 + - uid: 110 + components: + - type: Transform + pos: 12.5,-2.5 + parent: 104 + - uid: 113 + components: + - type: Transform + pos: 8.5,-10.5 + parent: 104 + - uid: 114 + components: + - type: Transform + pos: 16.5,-1.5 + parent: 104 + - uid: 119 + components: + - type: Transform + pos: 24.5,-2.5 + parent: 104 + - uid: 121 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 18.5,-12.5 + parent: 104 + - uid: 123 + components: + - type: Transform + pos: 21.5,-2.5 + parent: 104 + - uid: 124 + components: + - type: Transform + pos: 19.5,-2.5 + parent: 104 + - uid: 125 + components: + - type: Transform + pos: 16.5,-3.5 + parent: 104 + - uid: 126 + components: + - type: Transform + pos: 16.5,-4.5 + parent: 104 + - uid: 132 + components: + - type: Transform + pos: 23.5,-2.5 + parent: 104 + - uid: 133 + components: + - type: Transform + pos: 1.5,1.5 + parent: 104 + - uid: 137 + components: + - type: Transform + pos: 8.5,-12.5 + parent: 104 + - uid: 168 + components: + - type: Transform + pos: 14.5,-9.5 + parent: 104 + - uid: 171 + components: + - type: Transform + pos: 13.5,-9.5 + parent: 104 + - uid: 174 + components: + - type: Transform + pos: 15.5,-9.5 + parent: 104 + - uid: 175 + components: + - type: Transform + pos: 16.5,-9.5 + parent: 104 + - uid: 176 + components: + - type: Transform + pos: 15.5,-5.5 + parent: 104 + - uid: 177 + components: + - type: Transform + pos: 8.5,-13.5 + parent: 104 + - uid: 179 + components: + - type: Transform + pos: 8.5,-14.5 + parent: 104 + - uid: 180 + components: + - type: Transform + pos: 8.5,-15.5 + parent: 104 + - uid: 181 + components: + - type: Transform + pos: 9.5,-15.5 + parent: 104 + - uid: 182 + components: + - type: Transform + pos: 10.5,-15.5 + parent: 104 + - uid: 183 + components: + - type: Transform + pos: 11.5,-15.5 + parent: 104 + - uid: 184 + components: + - type: Transform + pos: 12.5,-15.5 + parent: 104 + - uid: 185 + components: + - type: Transform + pos: 13.5,-15.5 + parent: 104 + - uid: 186 + components: + - type: Transform + pos: 14.5,-15.5 + parent: 104 + - uid: 187 + components: + - type: Transform + pos: 15.5,-15.5 + parent: 104 + - uid: 188 + components: + - type: Transform + pos: 16.5,-15.5 + parent: 104 + - uid: 189 + components: + - type: Transform + pos: 16.5,-14.5 + parent: 104 + - uid: 191 + components: + - type: Transform + pos: 16.5,-12.5 + parent: 104 + - uid: 192 + components: + - type: Transform + pos: 16.5,-11.5 + parent: 104 + - uid: 193 + components: + - type: Transform + pos: 16.5,-10.5 + parent: 104 + - uid: 199 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-17.5 + parent: 104 + - uid: 215 + components: + - type: Transform + pos: 16.5,-0.5 + parent: 104 + - uid: 242 + components: + - type: Transform + pos: -4.5,-5.5 + parent: 104 + - uid: 252 + components: + - type: Transform + pos: -3.5,-15.5 + parent: 104 + - uid: 288 + components: + - type: Transform + pos: 14.5,-0.5 + parent: 104 + - uid: 290 + components: + - type: Transform + pos: 15.5,-0.5 + parent: 104 + - uid: 298 + components: + - type: Transform + pos: 13.5,-2.5 + parent: 104 + - uid: 299 + components: + - type: Transform + pos: 14.5,-2.5 + parent: 104 + - uid: 300 + components: + - type: Transform + pos: 15.5,-2.5 + parent: 104 + - uid: 301 + components: + - type: Transform + pos: 16.5,-2.5 + parent: 104 + - uid: 307 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 23.5,-8.5 + parent: 104 + - uid: 308 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 19.5,-10.5 + parent: 104 + - uid: 309 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-11.5 + parent: 104 + - uid: 310 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-12.5 + parent: 104 + - uid: 311 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 24.5,-12.5 + parent: 104 + - uid: 312 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 21.5,-12.5 + parent: 104 + - uid: 313 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 23.5,-12.5 + parent: 104 + - uid: 314 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 22.5,-12.5 + parent: 104 + - uid: 315 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 20.5,-12.5 + parent: 104 + - uid: 316 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 23.5,-6.5 + parent: 104 + - uid: 317 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 21.5,-4.5 + parent: 104 + - uid: 318 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 17.5,-6.5 + parent: 104 + - uid: 319 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 19.5,-4.5 + parent: 104 + - uid: 320 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 17.5,-8.5 + parent: 104 + - uid: 321 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 21.5,-10.5 + parent: 104 + - uid: 322 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 20.5,-7.5 + parent: 104 + - uid: 339 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-10.5 + parent: 104 + - uid: 340 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-9.5 + parent: 104 + - uid: 341 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-8.5 + parent: 104 + - uid: 342 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-7.5 + parent: 104 + - uid: 343 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-6.5 + parent: 104 + - uid: 344 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-5.5 + parent: 104 + - uid: 345 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-4.5 + parent: 104 + - uid: 346 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-3.5 + parent: 104 + - uid: 347 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 25.5,-2.5 + parent: 104 + - uid: 378 + components: + - type: Transform + pos: -3.5,-16.5 + parent: 104 + - uid: 379 + components: + - type: Transform + pos: -3.5,-13.5 + parent: 104 + - uid: 380 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-11.5 + parent: 104 + - uid: 418 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -12.5,8.5 + parent: 104 + - uid: 424 + components: + - type: Transform + pos: -10.5,8.5 + parent: 104 + - uid: 427 + components: + - type: Transform + pos: -1.5,-9.5 + parent: 104 + - uid: 441 + components: + - type: Transform + pos: -2.5,-1.5 + parent: 104 + - uid: 443 + components: + - type: Transform + pos: -3.5,-1.5 + parent: 104 + - uid: 462 + components: + - type: Transform + pos: -5.5,-5.5 + parent: 104 + - uid: 463 + components: + - type: Transform + pos: -4.5,-1.5 + parent: 104 + - uid: 474 + components: + - type: Transform + pos: -5.5,-1.5 + parent: 104 + - uid: 482 + components: + - type: Transform + pos: -9.5,8.5 + parent: 104 + - uid: 486 + components: + - type: Transform + pos: -16.5,8.5 + parent: 104 + - uid: 493 + components: + - type: Transform + pos: -3.5,-12.5 + parent: 104 + - uid: 495 + components: + - type: Transform + pos: -2.5,-5.5 + parent: 104 + - uid: 497 + components: + - type: Transform + pos: -2.5,-17.5 + parent: 104 + - uid: 591 + components: + - type: Transform + pos: 15.5,-23.5 + parent: 104 + - uid: 592 + components: + - type: Transform + pos: 9.5,-20.5 + parent: 104 + - uid: 593 + components: + - type: Transform + pos: 9.5,-22.5 + parent: 104 + - uid: 594 + components: + - type: Transform + pos: 9.5,-18.5 + parent: 104 + - uid: 611 + components: + - type: Transform + pos: -5.5,-6.5 + parent: 104 + - uid: 613 + components: + - type: Transform + pos: -13.5,19.5 + parent: 104 + - uid: 623 + components: + - type: Transform + pos: -17.5,19.5 + parent: 104 + - uid: 651 + components: + - type: Transform + pos: -17.5,23.5 + parent: 104 + - uid: 671 + components: + - type: Transform + pos: -12.5,24.5 + parent: 104 + - uid: 675 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -12.5,9.5 + parent: 104 + - uid: 683 + components: + - type: Transform + pos: -15.5,10.5 + parent: 104 + - uid: 698 + components: + - type: Transform + pos: -14.5,10.5 + parent: 104 + - uid: 710 + components: + - type: Transform + pos: -13.5,10.5 + parent: 104 + - uid: 730 + components: + - type: Transform + pos: -12.5,10.5 + parent: 104 + - uid: 731 + components: + - type: Transform + pos: -12.5,22.5 + parent: 104 + - uid: 732 + components: + - type: Transform + pos: -15.5,19.5 + parent: 104 + - uid: 739 + components: + - type: Transform + pos: -17.5,21.5 + parent: 104 + - uid: 743 + components: + - type: Transform + pos: -16.5,9.5 + parent: 104 + - uid: 744 + components: + - type: Transform + pos: -16.5,7.5 + parent: 104 + - uid: 750 + components: + - type: Transform + pos: -3.5,-14.5 + parent: 104 + - uid: 767 + components: + - type: Transform + pos: -16.5,10.5 + parent: 104 + - uid: 773 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-11.5 + parent: 104 + - uid: 1025 + components: + - type: Transform + pos: 3.5,-18.5 + parent: 104 + - uid: 1026 + components: + - type: Transform + pos: 4.5,-18.5 + parent: 104 + - uid: 1091 + components: + - type: Transform + pos: -5.5,-2.5 + parent: 104 + - uid: 1104 + components: + - type: Transform + pos: 9.5,-19.5 + parent: 104 + - uid: 1110 + components: + - type: Transform + pos: -5.5,-4.5 + parent: 104 + - uid: 1123 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-11.5 + parent: 104 + - uid: 1126 + components: + - type: Transform + pos: -17.5,22.5 + parent: 104 + - uid: 1128 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -11.5,8.5 + parent: 104 + - uid: 1136 + components: + - type: Transform + pos: -11.5,5.5 + parent: 104 + - uid: 1137 + components: + - type: Transform + pos: -15.5,7.5 + parent: 104 + - uid: 1138 + components: + - type: Transform + pos: -15.5,6.5 + parent: 104 + - uid: 1139 + components: + - type: Transform + pos: -15.5,5.5 + parent: 104 + - uid: 1147 + components: + - type: Transform + pos: -11.5,7.5 + parent: 104 + - uid: 1181 + components: + - type: Transform + pos: 2.5,-16.5 + parent: 104 + - uid: 1184 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-11.5 + parent: 104 + - uid: 1188 + components: + - type: Transform + pos: 2.5,-18.5 + parent: 104 + - uid: 1190 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -2.5,-11.5 + parent: 104 + - uid: 1197 + components: + - type: Transform + pos: 7.5,-18.5 + parent: 104 + - uid: 1199 + components: + - type: Transform + pos: 5.5,-18.5 + parent: 104 + - uid: 1201 + components: + - type: Transform + pos: 6.5,-18.5 + parent: 104 + - uid: 1205 + components: + - type: Transform + pos: 2.5,-17.5 + parent: 104 + - uid: 1206 + components: + - type: Transform + pos: 8.5,-18.5 + parent: 104 + - uid: 1207 + components: + - type: Transform + pos: -17.5,24.5 + parent: 104 + - uid: 1209 + components: + - type: Transform + pos: -17.5,20.5 + parent: 104 + - uid: 1210 + components: + - type: Transform + pos: -14.5,19.5 + parent: 104 + - uid: 1211 + components: + - type: Transform + pos: -12.5,23.5 + parent: 104 + - uid: 1212 + components: + - type: Transform + pos: -16.5,19.5 + parent: 104 + - uid: 1213 + components: + - type: Transform + pos: -12.5,19.5 + parent: 104 + - uid: 1214 + components: + - type: Transform + pos: -16.5,24.5 + parent: 104 + - uid: 1215 + components: + - type: Transform + pos: -12.5,20.5 + parent: 104 + - uid: 1216 + components: + - type: Transform + pos: -15.5,24.5 + parent: 104 + - uid: 1218 + components: + - type: Transform + pos: -14.5,24.5 + parent: 104 + - uid: 1219 + components: + - type: Transform + pos: -12.5,21.5 + parent: 104 + - uid: 1220 + components: + - type: Transform + pos: -13.5,24.5 + parent: 104 + - uid: 1381 + components: + - type: Transform + pos: 16.5,-16.5 + parent: 104 + - uid: 1409 + components: + - type: Transform + pos: 9.5,-21.5 + parent: 104 + - uid: 1411 + components: + - type: Transform + pos: 9.5,-23.5 + parent: 104 + - uid: 1412 + components: + - type: Transform + pos: 15.5,-16.5 + parent: 104 + - uid: 1426 + components: + - type: Transform + pos: -3.5,-5.5 + parent: 104 + - uid: 1783 + components: + - type: Transform + pos: -3.5,-11.5 + parent: 104 + - uid: 1784 + components: + - type: Transform + pos: -3.5,-17.5 + parent: 104 + - uid: 1811 + components: + - type: Transform + pos: 15.5,-19.5 + parent: 104 + - uid: 1833 + components: + - type: Transform + pos: 15.5,-17.5 + parent: 104 + - uid: 1834 + components: + - type: Transform + pos: 15.5,-18.5 + parent: 104 + - uid: 1843 + components: + - type: Transform + pos: 15.5,-22.5 + parent: 104 + - uid: 1850 + components: + - type: Transform + pos: 15.5,-21.5 + parent: 104 + - uid: 1856 + components: + - type: Transform + pos: 15.5,-20.5 + parent: 104 + - uid: 1861 + components: + - type: Transform + pos: 26.5,-12.5 + parent: 104 + - uid: 1862 + components: + - type: Transform + pos: 27.5,-12.5 + parent: 104 + - uid: 1863 + components: + - type: Transform + pos: 28.5,-12.5 + parent: 104 + - uid: 1864 + components: + - type: Transform + pos: 28.5,-14.5 + parent: 104 + - uid: 1866 + components: + - type: Transform + pos: 28.5,-13.5 + parent: 104 + - uid: 1871 + components: + - type: Transform + pos: 28.5,-19.5 + parent: 104 + - uid: 1876 + components: + - type: Transform + pos: 28.5,-15.5 + parent: 104 + - uid: 1881 + components: + - type: Transform + pos: 28.5,-16.5 + parent: 104 + - uid: 1885 + components: + - type: Transform + pos: 28.5,-17.5 + parent: 104 + - uid: 1889 + components: + - type: Transform + pos: 28.5,-18.5 + parent: 104 + - uid: 1893 + components: + - type: Transform + pos: 28.5,-20.5 + parent: 104 + - uid: 1897 + components: + - type: Transform + pos: 27.5,-20.5 + parent: 104 + - uid: 1901 + components: + - type: Transform + pos: 26.5,-20.5 + parent: 104 + - uid: 1905 + components: + - type: Transform + pos: 18.5,-20.5 + parent: 104 + - uid: 1906 + components: + - type: Transform + pos: 17.5,-20.5 + parent: 104 + - uid: 1907 + components: + - type: Transform + pos: 16.5,-20.5 + parent: 104 + - uid: 1908 + components: + - type: Transform + pos: 16.5,-19.5 + parent: 104 + - uid: 1914 + components: + - type: Transform + pos: 25.5,-20.5 + parent: 104 + - uid: 1915 + components: + - type: Transform + pos: 16.5,-17.5 + parent: 104 + - uid: 1943 + components: + - type: Transform + pos: 24.5,-20.5 + parent: 104 + - uid: 1944 + components: + - type: Transform + pos: 16.5,-18.5 + parent: 104 + - uid: 1968 + components: + - type: Transform + pos: 23.5,-20.5 + parent: 104 + - uid: 1969 + components: + - type: Transform + pos: 22.5,-20.5 + parent: 104 + - uid: 1970 + components: + - type: Transform + pos: 21.5,-20.5 + parent: 104 + - uid: 1971 + components: + - type: Transform + pos: 20.5,-20.5 + parent: 104 + - uid: 1972 + components: + - type: Transform + pos: 19.5,-20.5 + parent: 104 + - uid: 2017 + components: + - type: Transform + pos: 2.5,-10.5 + parent: 104 + - uid: 2018 + components: + - type: Transform + pos: 2.5,-11.5 + parent: 104 + - uid: 2019 + components: + - type: Transform + pos: 2.5,-12.5 + parent: 104 + - uid: 2299 + components: + - type: Transform + pos: -1.5,-2.5 + parent: 104 + - uid: 2300 + components: + - type: Transform + pos: -1.5,-4.5 + parent: 104 +- proto: WallPlastitaniumDiagonal + entities: + - uid: 323 + components: + - type: Transform + pos: 18.5,-4.5 + parent: 104 + - uid: 324 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 22.5,-5.5 + parent: 104 + - uid: 325 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 23.5,-9.5 + parent: 104 + - uid: 338 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 17.5,-9.5 + parent: 104 + - uid: 348 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 18.5,-9.5 + parent: 104 + - uid: 650 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 18.5,-10.5 + parent: 104 + - uid: 1088 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 18.5,-5.5 + parent: 104 + - uid: 1089 + components: + - type: Transform + pos: 17.5,-5.5 + parent: 104 + - uid: 1119 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 22.5,-10.5 + parent: 104 + - uid: 1689 + components: + - type: Transform + pos: 22.5,-9.5 + parent: 104 + - uid: 1690 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 23.5,-5.5 + parent: 104 + - uid: 1697 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 22.5,-4.5 + parent: 104 +- proto: WaterTankFull + entities: + - uid: 1547 + components: + - type: Transform + pos: 11.5,-4.5 + parent: 104 +- proto: WeaponPistolCobra + entities: + - uid: 1705 + components: + - type: Transform + pos: 5.4793262,12.166912 + parent: 104 +- proto: WeaponShotgunSawn + entities: + - uid: 228 + components: + - type: Transform + pos: 5.347948,13.528896 + parent: 104 + - type: BallisticAmmoProvider + unspawnedCount: 2 +- proto: WeaponSprayNozzle + entities: + - uid: 152 + components: + - type: Transform + pos: 9.67418,-4.571714 + parent: 104 +- proto: WeaponSubMachineGunC20r + entities: + - uid: 1704 + components: + - type: Transform + pos: 5.497257,12.817707 + parent: 104 +- proto: WelderIndustrial + entities: + - uid: 207 + components: + - type: Transform + pos: 14.5,-10.5 + parent: 104 +- proto: Windoor + entities: + - uid: 27 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 4.5,-7.5 + parent: 104 +- proto: WindoorSecure + entities: + - uid: 1145 + components: + - type: Transform + pos: -13.5,5.5 + parent: 104 + - uid: 1827 + components: + - type: Transform + pos: 10.5,-18.5 + parent: 104 +- proto: WindowReinforcedDirectional + entities: + - uid: 766 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-12.5 + parent: 104 + - uid: 1135 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-16.5 + parent: 104 + - uid: 1217 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 7.5,9.5 + parent: 104 + - uid: 1247 + components: + - type: Transform + pos: 4.5,-12.5 + parent: 104 + - uid: 1370 + components: + - type: Transform + pos: 3.5,-12.5 + parent: 104 + - uid: 2082 + components: + - type: Transform + pos: 7.5,15.5 + parent: 104 + - uid: 2182 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 5.5,-12.5 + parent: 104 + - uid: 2258 + components: + - type: Transform + pos: 5.5,-12.5 + parent: 104 + - uid: 2290 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,9.5 + parent: 104 + - uid: 2293 + components: + - type: Transform + pos: 5.5,15.5 + parent: 104 + - uid: 2294 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 5.5,-10.5 + parent: 104 +... diff --git a/Resources/Maps/Salvage/vegan-meatball.yml b/Resources/Maps/Salvage/vegan-meatball.yml index ab67aabadbf..824e3d0b7ea 100644 --- a/Resources/Maps/Salvage/vegan-meatball.yml +++ b/Resources/Maps/Salvage/vegan-meatball.yml @@ -505,7 +505,7 @@ entities: chemicals: THC: Inherent: True - potencyDivisor: 10 + PotencyDivisor: 10 Max: 10 Min: 1 productPrototypes: @@ -578,7 +578,7 @@ entities: chemicals: Stellibinin: Inherent: True - potencyDivisor: 4 + PotencyDivisor: 4 Max: 25 Min: 1 productPrototypes: diff --git a/Resources/Maps/Shuttles/infiltrator.yml b/Resources/Maps/Shuttles/infiltrator.yml index c6211b74e44..fb2045d7018 100644 --- a/Resources/Maps/Shuttles/infiltrator.yml +++ b/Resources/Maps/Shuttles/infiltrator.yml @@ -1,5678 +1,5678 @@ -meta: - format: 6 - postmapinit: false -tilemap: - 0: Space - 3: FloorArcadeRed - 31: FloorDark - 36: FloorDarkMono - 40: FloorDarkPlastic - 56: FloorGreenCircuit - 79: FloorReinforced - 88: FloorShuttleRed - 89: FloorShuttleWhite - 93: FloorSteel - 104: FloorSteelMono - 110: FloorTechMaint3 - 122: FloorWood - 125: Lattice - 126: Plating -entities: -- proto: "" - entities: - - uid: 1 - components: - - type: MetaData - name: GX-13 Infiltrator - - type: Transform - pos: 0.64252126,4.1776605 - parent: invalid - - type: Tag - tags: - - Syndicate - - type: MapGrid - chunks: - -1,-1: - ind: -1,-1 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADHwAAAAABHwAAAAACHwAAAAABHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAHwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAHwAAAAACfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAHwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACHwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfgAAAAAAHwAAAAABHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAHwAAAAACHwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAACWAAAAAAAHwAAAAACHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAXQAAAAACaAAAAAAAWQAAAAAAHwAAAAADHwAAAAAAHwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAXQAAAAADWQAAAAAAaAAAAAABWAAAAAAAHwAAAAAAHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAXQAAAAAAXQAAAAADWAAAAAAAHwAAAAACHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAWAAAAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - 0,-1: - ind: 0,-1 - tiles: HwAAAAABHwAAAAABHwAAAAABHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABWAAAAAAAegAAAAADfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADHwAAAAAAHwAAAAAAegAAAAADegAAAAABfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAWAAAAAAAegAAAAAAAwAAAAAAegAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADWAAAAAAAfgAAAAAAAwAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAAAAAAWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - -1,-2: - ind: -1,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAWAAAAAAAWAAAAAAAWAAAAAAAfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAOAAAAAAAJAAAAAAAOAAAAAAAfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAOAAAAAAAJAAAAAAAOAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAOAAAAAAAJAAAAAAAOAAAAAAAWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAHwAAAAACHwAAAAAAHwAAAAADWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAHwAAAAACTwAAAAAAHwAAAAABWAAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAWAAAAAAAfgAAAAAAHwAAAAAAHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAKAAAAAAAKAAAAAAAWAAAAAAAHwAAAAADHwAAAAAAHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAWAAAAAAAHwAAAAAAWAAAAAAAfgAAAAAAHwAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACHwAAAAABHwAAAAAAHwAAAAADHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAHwAAAAAAHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAA - version: 6 - 0,-2: - ind: 0,-2 - tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfgAAAAAATwAAAAAAfgAAAAAATwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfgAAAAAATwAAAAAAfgAAAAAATwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAATwAAAAAAfgAAAAAATwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABHwAAAAADHwAAAAABHwAAAAADHwAAAAABWAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADHwAAAAACHwAAAAAAHwAAAAACHwAAAAADfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAHwAAAAACHwAAAAAAHwAAAAADHwAAAAABfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABHwAAAAAAHwAAAAABHwAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAHwAAAAAAHwAAAAACbgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - version: 6 - - type: Broadphase - - type: Physics - bodyStatus: InAir - angularDamping: 0.05 - linearDamping: 0.05 - fixedRotation: False - bodyType: Dynamic - - type: Fixtures - fixtures: {} - - type: Gravity - gravityShakeSound: !type:SoundPathSpecifier - path: /Audio/Effects/alert.ogg - - type: GridAtmosphere - version: 2 - data: - tiles: - -1,-4: - 0: 65535 - -1,-3: - 0: 65535 - -1,-2: - 0: 65535 - -1,-1: - 0: 61439 - 0,-4: - 0: 65535 - 0,-3: - 0: 65535 - 0,-2: - 0: 65535 - 0,-1: - 0: 65535 - 1,-4: - 0: 30591 - 1,-3: - 0: 21879 - 1: 512 - 1,-2: - 0: 30325 - 2: 256 - 1,-1: - 0: 55 - 0,-5: - 0: 65535 - 1,-5: - 0: 65399 - -1,-5: - 0: 65535 - -3,-4: - 0: 12 - -2,-4: - 0: 61439 - -2,-2: - 0: 65516 - -2,-1: - 0: 2287 - -2,-3: - 0: 35054 - 1: 1536 - -1,0: - 0: 8 - -3,-5: - 0: 52224 - -2,-8: - 0: 65504 - -2,-7: - 0: 65535 - -2,-6: - 0: 65535 - -2,-5: - 0: 65535 - -1,-8: - 0: 65526 - -1,-7: - 0: 65535 - -1,-6: - 0: 65535 - 0,-8: - 0: 65523 - 0,-7: - 0: 61303 - 3: 4096 - 4: 136 - 0,-6: - 3: 1 - 0: 65534 - 1,-8: - 0: 30512 - 1,-7: - 0: 30549 - 5: 34 - 1,-6: - 0: 30583 - -1,-9: - 0: 26112 - 0,-9: - 0: 13056 - 2,-4: - 0: 1 - 2,-5: - 0: 4352 - uniqueMixes: - - volume: 2500 - temperature: 293.15 - moles: - - 21.824879 - - 82.10312 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.15 - moles: - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.14996 - moles: - - 20.078888 - - 75.53487 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.15 - moles: - - 20.619795 - - 77.56971 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.15 - moles: - - 6666.982 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - volume: 2500 - temperature: 293.15 - moles: - - 0 - - 6666.982 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - chunkSize: 4 - - type: DecalGrid - chunkCollection: - version: 2 - nodes: - - node: - color: '#FFFFFFFF' - id: Arrows - decals: - 115: -6,-24 - - node: - color: '#FFFFFFFF' - id: Bot - decals: - 112: -6,-27 - 113: -6,-26 - 114: -6,-25 - - node: - color: '#79150096' - id: Box - decals: - 110: -6,-21 - 111: -5,-21 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerNe - decals: - 10: 0,-4 - 43: 0,-9 - 51: 4,-20 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerNw - decals: - 11: -2,-4 - 42: -2,-9 - 45: -2,-20 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerSe - decals: - 36: 0,-14 - 50: 4,-22 - 76: 3,-18 - - node: - color: '#DE3A3A96' - id: BrickTileSteelCornerSw - decals: - 37: -2,-14 - 46: -2,-22 - 75: -5,-18 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineE - decals: - 6: 0,-7 - 9: 0,-5 - 32: 0,-11 - 33: 0,-10 - 34: 0,-12 - 35: 0,-13 - 52: 4,-21 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineN - decals: - 12: -1,-4 - 53: 3,-20 - 54: 2,-20 - 55: 1,-20 - 56: 0,-20 - 63: 0,-16 - 64: 1,-16 - 65: 2,-16 - 66: -2,-16 - 67: -3,-16 - 68: -4,-16 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineS - decals: - 47: 0,-22 - 48: 1,-22 - 49: 2,-22 - 69: 2,-18 - 70: 1,-18 - 71: 0,-18 - 72: -2,-18 - 73: -3,-18 - 74: -4,-18 - - node: - color: '#DE3A3A96' - id: BrickTileSteelLineW - decals: - 7: -2,-7 - 8: -2,-5 - 38: -2,-13 - 39: -2,-12 - 40: -2,-11 - 41: -2,-10 - - node: - color: '#79150096' - id: BrickTileWhiteCornerNe - decals: - 104: -5,-22 - - node: - color: '#79150096' - id: BrickTileWhiteCornerNw - decals: - 103: -7,-22 - - node: - color: '#79150096' - id: BrickTileWhiteCornerSe - decals: - 105: -5,-24 - - node: - color: '#79150096' - id: BrickTileWhiteCornerSw - decals: - 106: -7,-24 - - node: - color: '#79150096' - id: BrickTileWhiteLineE - decals: - 102: -5,-23 - - node: - color: '#79150096' - id: BrickTileWhiteLineN - decals: - 109: -6,-22 - - node: - color: '#79150096' - id: BrickTileWhiteLineS - decals: - 107: -6,-24 - - node: - color: '#79150096' - id: BrickTileWhiteLineW - decals: - 108: -7,-23 - - node: - color: '#79150096' - id: Delivery - decals: - 116: -1,-23 - - node: - color: '#DE3A3A96' - id: DeliveryGreyscale - decals: - 13: 1,-6 - 31: -3,-6 - 83: -1,-19 - 84: -3,-21 - 85: -1,-15 - 86: 4,-17 - 87: -6,-17 - 88: -5,-15 - 89: 3,-15 - 90: -1,-8 - - node: - color: '#DE3A3A96' - id: HalfTileOverlayGreyscale - decals: - 29: -4,-7 - - node: - color: '#DE3A3A96' - id: HalfTileOverlayGreyscale180 - decals: - 25: -4,-4 - 26: -5,-4 - - node: - color: '#DE3A3A96' - id: HalfTileOverlayGreyscale90 - decals: - 27: -6,-6 - 28: -6,-5 - - node: - color: '#DE3A3A96' - id: MonoOverlay - decals: - 23: -5,-6 - 24: -4,-5 - - node: - color: '#DE3A3A96' - id: WarnCornerGreyscaleNE - decals: - 81: 3,-16 - - node: - color: '#DE3A3A96' - id: WarnCornerGreyscaleNW - decals: - 80: -5,-16 - - node: - color: '#FFFFFFFF' - id: WarnCornerSmallNE - decals: - 99: -7,-24 - - node: - color: '#FFFFFFFF' - id: WarnCornerSmallNW - decals: - 98: -5,-24 - - node: - color: '#FFFFFFFF' - id: WarnCornerSmallSE - decals: - 97: -7,-22 - - node: - color: '#FFFFFFFF' - id: WarnCornerSmallSW - decals: - 101: -5,-22 - - node: - color: '#FFFFFFFF' - id: WarnLineE - decals: - 3: -4,-23 - 4: -4,-24 - 5: -4,-25 - 95: -7,-23 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleE - decals: - 78: 3,-17 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleN - decals: - 44: -1,-9 - 59: -1,-20 - 82: -1,-16 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleS - decals: - 57: -1,-22 - 60: 3,-22 - 77: -1,-18 - - node: - color: '#DE3A3A96' - id: WarnLineGreyscaleW - decals: - 58: -2,-21 - 79: -5,-17 - - node: - color: '#FFFFFFFF' - id: WarnLineN - decals: - 91: -5,-28 - 92: -6,-28 - 93: -7,-28 - 100: -6,-22 - - node: - color: '#FFFFFFFF' - id: WarnLineS - decals: - 94: -5,-23 - - node: - color: '#FFFFFFFF' - id: WarnLineW - decals: - 96: -6,-24 - - node: - angle: 1.5707963267948966 rad - color: '#FFFFFFFF' - id: WarningLine - decals: - 0: -4,-22 - 1: -4,-21 - 2: -4,-20 - - node: - color: '#FFFFFFFF' - id: WoodTrimThinEndN - decals: - 62: 3,-4 - - node: - color: '#FFFFFFFF' - id: WoodTrimThinEndS - decals: - 61: 3,-5 - - node: - color: '#FFFFFFFF' - id: syndlogo10 - decals: - 20: -2,-7 - - node: - color: '#FFFFFFFF' - id: syndlogo11 - decals: - 21: -1,-7 - - node: - color: '#FFFFFFFF' - id: syndlogo12 - decals: - 22: 0,-7 - - node: - color: '#FFFFFFFF' - id: syndlogo2 - decals: - 14: -2,-5 - - node: - color: '#FFFFFFFF' - id: syndlogo3 - decals: - 15: -1,-5 - - node: - color: '#FFFFFFFF' - id: syndlogo4 - decals: - 16: 0,-5 - - node: - color: '#FFFFFFFF' - id: syndlogo5 - decals: - 30: -3,-6 - - node: - color: '#FFFFFFFF' - id: syndlogo6 - decals: - 17: -2,-6 - - node: - color: '#FFFFFFFF' - id: syndlogo7 - decals: - 18: -1,-6 - - node: - color: '#FFFFFFFF' - id: syndlogo8 - decals: - 19: 0,-6 - - type: IFF - color: '#FFC000FF' - flags: Hide - - type: OccluderTree - - type: Shuttle - - type: RadiationGridResistance - - type: GravityShake - shakeTimes: 10 - - type: GasTileOverlay - - type: SpreaderGrid - - type: GridPathfinding -- proto: AirlockExternalGlassShuttleSyndicateLocked - entities: - - uid: 8 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 8.5,-16.5 - parent: 1 - - type: DeviceLinkSink - invokeCounter: 1 - links: - - 13 - - type: DeviceLinkSource - linkedPorts: - 13: - - DoorStatus: Close - - uid: 10 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -9.5,-16.5 - parent: 1 - - type: DeviceLinkSink - invokeCounter: 1 - links: - - 3 - - type: DeviceLinkSource - linkedPorts: - 3: - - DoorStatus: Close -- proto: AirlockExternalSyndicateLocked - entities: - - uid: 2 - components: - - type: Transform - pos: -0.5,-25.5 - parent: 1 - - type: DeviceLinkSink - links: - - 14 - - type: DeviceLinkSource - linkedPorts: - 14: - - DoorStatus: DoorBolt - - uid: 3 - components: - - type: Transform - pos: -5.5,-16.5 - parent: 1 - - type: DeviceLinkSink - invokeCounter: 1 - links: - - 10 - - type: DeviceLinkSource - linkedPorts: - 10: - - DoorStatus: Close - - uid: 7 - components: - - type: Transform - pos: -4.5,-14.5 - parent: 1 - - type: DeviceLinkSink - links: - - 12 - - type: DeviceLinkSource - linkedPorts: - 12: - - DoorStatus: DoorBolt - - uid: 9 - components: - - type: Transform - pos: 3.5,-14.5 - parent: 1 - - type: DeviceLinkSink - links: - - 22 - - type: DeviceLinkSource - linkedPorts: - 22: - - DoorStatus: DoorBolt - - uid: 12 - components: - - type: Transform - pos: -4.5,-10.5 - parent: 1 - - type: DeviceLinkSink - links: - - 7 - - type: DeviceLinkSource - linkedPorts: - 7: - - DoorStatus: DoorBolt - - uid: 13 - components: - - type: Transform - pos: 4.5,-16.5 - parent: 1 - - type: DeviceLinkSink - invokeCounter: 1 - links: - - 8 - - type: DeviceLinkSource - linkedPorts: - 8: - - DoorStatus: Close - - uid: 14 - components: - - type: Transform - pos: -0.5,-22.5 - parent: 1 - - type: DeviceLinkSink - links: - - 2 - - type: DeviceLinkSource - linkedPorts: - 2: - - DoorStatus: DoorBolt - - uid: 22 - components: - - type: Transform - pos: 3.5,-10.5 - parent: 1 - - type: DeviceLinkSink - links: - - 9 - - type: DeviceLinkSource - linkedPorts: - 9: - - DoorStatus: DoorBolt -- proto: AirlockSyndicateGlassLocked - entities: - - uid: 4 - components: - - type: Transform - pos: -0.5,-7.5 - parent: 1 - - uid: 5 - components: - - type: Transform - pos: 3.5,-22.5 - parent: 1 - - uid: 6 - components: - - type: Transform - pos: -0.5,-18.5 - parent: 1 - - uid: 17 - components: - - type: Transform - pos: -2.5,-20.5 - parent: 1 -- proto: AirlockSyndicateLocked - entities: - - uid: 15 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 1 - - uid: 16 - components: - - type: Transform - pos: 2.5,-5.5 - parent: 1 -- proto: APCBasic - entities: - - uid: 18 - components: - - type: Transform - pos: -3.5,-18.5 - parent: 1 - - uid: 19 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-8.5 - parent: 1 -- proto: AtmosFixNitrogenMarker - entities: - - uid: 25 - components: - - type: Transform - pos: 5.5,-26.5 - parent: 1 - - uid: 26 - components: - - type: Transform - pos: 5.5,-27.5 - parent: 1 -- proto: AtmosFixOxygenMarker - entities: - - uid: 27 - components: - - type: Transform - pos: 3.5,-26.5 - parent: 1 - - uid: 28 - components: - - type: Transform - pos: 3.5,-27.5 - parent: 1 -- proto: BannerSyndicate - entities: - - uid: 29 - components: - - type: Transform - pos: 2.5,-15.5 - parent: 1 - - uid: 30 - components: - - type: Transform - pos: -3.5,-15.5 - parent: 1 -- proto: Bed - entities: - - uid: 31 - components: - - type: Transform - pos: 3.5,-3.5 - parent: 1 -- proto: BedsheetSyndie - entities: - - uid: 32 - components: - - type: Transform - pos: 3.5,-3.5 - parent: 1 -- proto: BoxEncryptionKeySyndie - entities: - - uid: 34 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: BoxFlashbang - entities: - - uid: 42 - components: - - type: Transform - pos: 0.49331844,-13.366474 - parent: 1 -- proto: BoxHandcuff - entities: - - uid: 43 - components: - - type: Transform - pos: 1.4510483,-2.399527 - parent: 1 -- proto: Brutepack - entities: - - uid: 44 - components: - - type: Transform - pos: -3.292087,-4.1600046 - parent: 1 - - uid: 45 - components: - - type: Transform - pos: -3.354587,-4.4256296 - parent: 1 -- proto: C4 - entities: - - uid: 46 - components: - - type: Transform - pos: 1.7857682,-12.631323 - parent: 1 - - uid: 47 - components: - - type: Transform - pos: 1.5045182,-12.646948 - parent: 1 - - uid: 48 - components: - - type: Transform - pos: 1.5982682,-12.646948 - parent: 1 - - uid: 49 - components: - - type: Transform - pos: 1.4107682,-12.646948 - parent: 1 - - uid: 50 - components: - - type: Transform - pos: 1.6920182,-12.631323 - parent: 1 -- proto: CableApcExtension - entities: - - uid: 51 - components: - - type: Transform - pos: -3.5,-18.5 - parent: 1 - - uid: 52 - components: - - type: Transform - pos: -3.5,-17.5 - parent: 1 - - uid: 53 - components: - - type: Transform - pos: -3.5,-16.5 - parent: 1 - - uid: 54 - components: - - type: Transform - pos: -4.5,-16.5 - parent: 1 - - uid: 55 - components: - - type: Transform - pos: -5.5,-16.5 - parent: 1 - - uid: 56 - components: - - type: Transform - pos: -6.5,-16.5 - parent: 1 - - uid: 57 - components: - - type: Transform - pos: -7.5,-16.5 - parent: 1 - - uid: 58 - components: - - type: Transform - pos: -8.5,-16.5 - parent: 1 - - uid: 59 - components: - - type: Transform - pos: -6.5,-15.5 - parent: 1 - - uid: 60 - components: - - type: Transform - pos: -6.5,-17.5 - parent: 1 - - uid: 61 - components: - - type: Transform - pos: -4.5,-15.5 - parent: 1 - - uid: 62 - components: - - type: Transform - pos: -4.5,-14.5 - parent: 1 - - uid: 63 - components: - - type: Transform - pos: -4.5,-13.5 - parent: 1 - - uid: 64 - components: - - type: Transform - pos: -4.5,-12.5 - parent: 1 - - uid: 65 - components: - - type: Transform - pos: -4.5,-11.5 - parent: 1 - - uid: 66 - components: - - type: Transform - pos: -4.5,-10.5 - parent: 1 - - uid: 67 - components: - - type: Transform - pos: -5.5,-12.5 - parent: 1 - - uid: 68 - components: - - type: Transform - pos: -6.5,-12.5 - parent: 1 - - uid: 69 - components: - - type: Transform - pos: -2.5,-16.5 - parent: 1 - - uid: 70 - components: - - type: Transform - pos: -1.5,-16.5 - parent: 1 - - uid: 71 - components: - - type: Transform - pos: -0.5,-16.5 - parent: 1 - - uid: 72 - components: - - type: Transform - pos: 0.5,-16.5 - parent: 1 - - uid: 73 - components: - - type: Transform - pos: 1.5,-16.5 - parent: 1 - - uid: 74 - components: - - type: Transform - pos: 2.5,-16.5 - parent: 1 - - uid: 75 - components: - - type: Transform - pos: 3.5,-16.5 - parent: 1 - - uid: 76 - components: - - type: Transform - pos: 4.5,-16.5 - parent: 1 - - uid: 77 - components: - - type: Transform - pos: 5.5,-16.5 - parent: 1 - - uid: 78 - components: - - type: Transform - pos: 5.5,-15.5 - parent: 1 - - uid: 79 - components: - - type: Transform - pos: 5.5,-17.5 - parent: 1 - - uid: 80 - components: - - type: Transform - pos: 6.5,-16.5 - parent: 1 - - uid: 81 - components: - - type: Transform - pos: 7.5,-16.5 - parent: 1 - - uid: 82 - components: - - type: Transform - pos: 3.5,-15.5 - parent: 1 - - uid: 83 - components: - - type: Transform - pos: 3.5,-14.5 - parent: 1 - - uid: 84 - components: - - type: Transform - pos: 3.5,-13.5 - parent: 1 - - uid: 85 - components: - - type: Transform - pos: 3.5,-12.5 - parent: 1 - - uid: 86 - components: - - type: Transform - pos: 3.5,-11.5 - parent: 1 - - uid: 87 - components: - - type: Transform - pos: 3.5,-11.5 - parent: 1 - - uid: 88 - components: - - type: Transform - pos: 3.5,-10.5 - parent: 1 - - uid: 89 - components: - - type: Transform - pos: 4.5,-12.5 - parent: 1 - - uid: 90 - components: - - type: Transform - pos: 5.5,-12.5 - parent: 1 - - uid: 91 - components: - - type: Transform - pos: -0.5,-6.5 - parent: 1 - - uid: 92 - components: - - type: Transform - pos: -0.5,-7.5 - parent: 1 - - uid: 93 - components: - - type: Transform - pos: -0.5,-8.5 - parent: 1 - - uid: 94 - components: - - type: Transform - pos: -0.5,-9.5 - parent: 1 - - uid: 95 - components: - - type: Transform - pos: -0.5,-10.5 - parent: 1 - - uid: 96 - components: - - type: Transform - pos: -0.5,-11.5 - parent: 1 - - uid: 97 - components: - - type: Transform - pos: -0.5,-12.5 - parent: 1 - - uid: 98 - components: - - type: Transform - pos: -0.5,-13.5 - parent: 1 - - uid: 99 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 1 - - uid: 100 - components: - - type: Transform - pos: -1.5,-12.5 - parent: 1 - - uid: 101 - components: - - type: Transform - pos: 0.5,-12.5 - parent: 1 - - uid: 102 - components: - - type: Transform - pos: -0.5,-5.5 - parent: 1 - - uid: 103 - components: - - type: Transform - pos: 0.5,-5.5 - parent: 1 - - uid: 104 - components: - - type: Transform - pos: 1.5,-5.5 - parent: 1 - - uid: 105 - components: - - type: Transform - pos: 2.5,-5.5 - parent: 1 - - uid: 106 - components: - - type: Transform - pos: 3.5,-5.5 - parent: 1 - - uid: 107 - components: - - type: Transform - pos: 3.5,-4.5 - parent: 1 - - uid: 108 - components: - - type: Transform - pos: -0.5,-4.5 - parent: 1 - - uid: 109 - components: - - type: Transform - pos: -0.5,-3.5 - parent: 1 - - uid: 110 - components: - - type: Transform - pos: -0.5,-2.5 - parent: 1 - - uid: 111 - components: - - type: Transform - pos: 0.5,-2.5 - parent: 1 - - uid: 112 - components: - - type: Transform - pos: 1.5,-2.5 - parent: 1 - - uid: 113 - components: - - type: Transform - pos: -1.5,-2.5 - parent: 1 - - uid: 114 - components: - - type: Transform - pos: -2.5,-2.5 - parent: 1 - - uid: 115 - components: - - type: Transform - pos: -1.5,-4.5 - parent: 1 - - uid: 116 - components: - - type: Transform - pos: -3.5,-4.5 - parent: 1 - - uid: 117 - components: - - type: Transform - pos: -2.5,-4.5 - parent: 1 - - uid: 118 - components: - - type: Transform - pos: -4.5,-4.5 - parent: 1 - - uid: 119 - components: - - type: Transform - pos: -5.5,-4.5 - parent: 1 - - uid: 120 - components: - - type: Transform - pos: -5.5,-5.5 - parent: 1 - - uid: 121 - components: - - type: Transform - pos: -5.5,-6.5 - parent: 1 - - uid: 122 - components: - - type: Transform - pos: -5.5,-3.5 - parent: 1 - - uid: 123 - components: - - type: Transform - pos: 4.5,-4.5 - parent: 1 - - uid: 124 - components: - - type: Transform - pos: 4.5,-3.5 - parent: 1 - - uid: 125 - components: - - type: Transform - pos: 3.5,-6.5 - parent: 1 - - uid: 126 - components: - - type: Transform - pos: -0.5,-17.5 - parent: 1 - - uid: 127 - components: - - type: Transform - pos: -3.5,-20.5 - parent: 1 - - uid: 128 - components: - - type: Transform - pos: -3.5,-19.5 - parent: 1 - - uid: 129 - components: - - type: Transform - pos: -3.5,-21.5 - parent: 1 - - uid: 130 - components: - - type: Transform - pos: -4.5,-21.5 - parent: 1 - - uid: 131 - components: - - type: Transform - pos: -4.5,-22.5 - parent: 1 - - uid: 132 - components: - - type: Transform - pos: -4.5,-23.5 - parent: 1 - - uid: 133 - components: - - type: Transform - pos: -4.5,-24.5 - parent: 1 - - uid: 134 - components: - - type: Transform - pos: -4.5,-25.5 - parent: 1 - - uid: 135 - components: - - type: Transform - pos: -4.5,-25.5 - parent: 1 - - uid: 136 - components: - - type: Transform - pos: -4.5,-26.5 - parent: 1 - - uid: 137 - components: - - type: Transform - pos: -4.5,-27.5 - parent: 1 - - uid: 138 - components: - - type: Transform - pos: -5.5,-27.5 - parent: 1 - - uid: 139 - components: - - type: Transform - pos: -6.5,-27.5 - parent: 1 - - uid: 140 - components: - - type: Transform - pos: -6.5,-26.5 - parent: 1 - - uid: 141 - components: - - type: Transform - pos: -6.5,-25.5 - parent: 1 - - uid: 142 - components: - - type: Transform - pos: -6.5,-24.5 - parent: 1 - - uid: 143 - components: - - type: Transform - pos: -6.5,-23.5 - parent: 1 - - uid: 144 - components: - - type: Transform - pos: -6.5,-22.5 - parent: 1 - - uid: 145 - components: - - type: Transform - pos: -2.5,-20.5 - parent: 1 - - uid: 146 - components: - - type: Transform - pos: -1.5,-20.5 - parent: 1 - - uid: 147 - components: - - type: Transform - pos: -0.5,-20.5 - parent: 1 - - uid: 148 - components: - - type: Transform - pos: -0.5,-21.5 - parent: 1 - - uid: 149 - components: - - type: Transform - pos: -0.5,-23.5 - parent: 1 - - uid: 150 - components: - - type: Transform - pos: -0.5,-22.5 - parent: 1 - - uid: 151 - components: - - type: Transform - pos: -0.5,-24.5 - parent: 1 - - uid: 152 - components: - - type: Transform - pos: -0.5,-25.5 - parent: 1 - - uid: 153 - components: - - type: Transform - pos: -0.5,-19.5 - parent: 1 - - uid: 154 - components: - - type: Transform - pos: 0.5,-20.5 - parent: 1 - - uid: 155 - components: - - type: Transform - pos: 1.5,-20.5 - parent: 1 - - uid: 156 - components: - - type: Transform - pos: 2.5,-20.5 - parent: 1 - - uid: 157 - components: - - type: Transform - pos: 3.5,-20.5 - parent: 1 - - uid: 158 - components: - - type: Transform - pos: 4.5,-20.5 - parent: 1 - - uid: 159 - components: - - type: Transform - pos: 3.5,-21.5 - parent: 1 - - uid: 160 - components: - - type: Transform - pos: 3.5,-22.5 - parent: 1 - - uid: 161 - components: - - type: Transform - pos: 3.5,-23.5 - parent: 1 - - uid: 162 - components: - - type: Transform - pos: 3.5,-24.5 - parent: 1 - - uid: 163 - components: - - type: Transform - pos: 3.5,-25.5 - parent: 1 - - uid: 164 - components: - - type: Transform - pos: 3.5,-26.5 - parent: 1 - - uid: 165 - components: - - type: Transform - pos: 3.5,-27.5 - parent: 1 - - uid: 166 - components: - - type: Transform - pos: 4.5,-27.5 - parent: 1 - - uid: 167 - components: - - type: Transform - pos: 5.5,-27.5 - parent: 1 - - uid: 168 - components: - - type: Transform - pos: 5.5,-26.5 - parent: 1 - - uid: 169 - components: - - type: Transform - pos: 5.5,-25.5 - parent: 1 - - uid: 170 - components: - - type: Transform - pos: 5.5,-24.5 - parent: 1 - - uid: 171 - components: - - type: Transform - pos: 5.5,-23.5 - parent: 1 - - uid: 172 - components: - - type: Transform - pos: 0.5,-8.5 - parent: 1 - - uid: 173 - components: - - type: Transform - pos: 1.5,-26.5 - parent: 1 - - uid: 174 - components: - - type: Transform - pos: 1.5,-8.5 - parent: 1 - - uid: 175 - components: - - type: Transform - pos: -0.5,-26.5 - parent: 1 - - uid: 176 - components: - - type: Transform - pos: -1.5,-26.5 - parent: 1 - - uid: 177 - components: - - type: Transform - pos: 0.5,-26.5 - parent: 1 - - uid: 178 - components: - - type: Transform - pos: -2.5,-26.5 - parent: 1 - - uid: 179 - components: - - type: Transform - pos: -2.5,-27.5 - parent: 1 - - uid: 180 - components: - - type: Transform - pos: 1.5,-27.5 - parent: 1 - - uid: 181 - components: - - type: Transform - pos: -6.5,-28.5 - parent: 1 - - uid: 182 - components: - - type: Transform - pos: -6.5,-29.5 - parent: 1 - - uid: 183 - components: - - type: Transform - pos: -2.5,-28.5 - parent: 1 - - uid: 184 - components: - - type: Transform - pos: -2.5,-29.5 - parent: 1 - - uid: 185 - components: - - type: Transform - pos: -2.5,-30.5 - parent: 1 - - uid: 186 - components: - - type: Transform - pos: -3.5,-30.5 - parent: 1 - - uid: 187 - components: - - type: Transform - pos: -4.5,-30.5 - parent: 1 - - uid: 188 - components: - - type: Transform - pos: -5.5,-30.5 - parent: 1 - - uid: 189 - components: - - type: Transform - pos: 1.5,-28.5 - parent: 1 - - uid: 190 - components: - - type: Transform - pos: 1.5,-29.5 - parent: 1 - - uid: 191 - components: - - type: Transform - pos: 1.5,-30.5 - parent: 1 - - uid: 192 - components: - - type: Transform - pos: 2.5,-30.5 - parent: 1 - - uid: 193 - components: - - type: Transform - pos: 3.5,-30.5 - parent: 1 - - uid: 194 - components: - - type: Transform - pos: 4.5,-30.5 - parent: 1 - - uid: 195 - components: - - type: Transform - pos: 5.5,-28.5 - parent: 1 - - uid: 196 - components: - - type: Transform - pos: 5.5,-29.5 - parent: 1 -- proto: CableHV - entities: - - uid: 197 - components: - - type: Transform - pos: -2.5,-21.5 - parent: 1 - - uid: 198 - components: - - type: Transform - pos: -4.5,-26.5 - parent: 1 - - uid: 199 - components: - - type: Transform - pos: -4.5,-27.5 - parent: 1 - - uid: 200 - components: - - type: Transform - pos: -5.5,-27.5 - parent: 1 - - uid: 201 - components: - - type: Transform - pos: -6.5,-27.5 - parent: 1 - - uid: 202 - components: - - type: Transform - pos: -5.5,-23.5 - parent: 1 - - uid: 203 - components: - - type: Transform - pos: -5.5,-22.5 - parent: 1 - - uid: 204 - components: - - type: Transform - pos: -5.5,-20.5 - parent: 1 - - uid: 205 - components: - - type: Transform - pos: -4.5,-20.5 - parent: 1 - - uid: 206 - components: - - type: Transform - pos: -5.5,-19.5 - parent: 1 - - uid: 207 - components: - - type: Transform - pos: -4.5,-19.5 - parent: 1 - - uid: 208 - components: - - type: Transform - pos: -3.5,-19.5 - parent: 1 - - uid: 209 - components: - - type: Transform - pos: -5.5,-21.5 - parent: 1 - - uid: 210 - components: - - type: Transform - pos: -6.5,-26.5 - parent: 1 - - uid: 211 - components: - - type: Transform - pos: -6.5,-25.5 - parent: 1 - - uid: 212 - components: - - type: Transform - pos: -6.5,-24.5 - parent: 1 - - uid: 213 - components: - - type: Transform - pos: -4.5,-23.5 - parent: 1 - - uid: 214 - components: - - type: Transform - pos: -2.5,-20.5 - parent: 1 - - uid: 215 - components: - - type: Transform - pos: -2.5,-19.5 - parent: 1 - - uid: 216 - components: - - type: Transform - pos: -4.5,-25.5 - parent: 1 - - uid: 217 - components: - - type: Transform - pos: -6.5,-23.5 - parent: 1 - - uid: 218 - components: - - type: Transform - pos: -6.5,-20.5 - parent: 1 - - uid: 219 - components: - - type: Transform - pos: -4.5,-24.5 - parent: 1 - - uid: 220 - components: - - type: Transform - pos: -5.5,-18.5 - parent: 1 - - uid: 221 - components: - - type: Transform - pos: -4.5,-18.5 - parent: 1 - - uid: 222 - components: - - type: Transform - pos: -3.5,-18.5 - parent: 1 - - uid: 223 - components: - - type: Transform - pos: -3.5,-21.5 - parent: 1 - - uid: 224 - components: - - type: Transform - pos: -4.5,-21.5 - parent: 1 -- proto: CableMV - entities: - - uid: 225 - components: - - type: Transform - pos: -3.5,-19.5 - parent: 1 - - uid: 226 - components: - - type: Transform - pos: -3.5,-18.5 - parent: 1 - - uid: 227 - components: - - type: Transform - pos: -3.5,-17.5 - parent: 1 - - uid: 228 - components: - - type: Transform - pos: -3.5,-16.5 - parent: 1 - - uid: 229 - components: - - type: Transform - pos: -2.5,-16.5 - parent: 1 - - uid: 230 - components: - - type: Transform - pos: -0.5,-16.5 - parent: 1 - - uid: 231 - components: - - type: Transform - pos: -1.5,-16.5 - parent: 1 - - uid: 232 - components: - - type: Transform - pos: -0.5,-15.5 - parent: 1 - - uid: 233 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 1 - - uid: 234 - components: - - type: Transform - pos: -0.5,-13.5 - parent: 1 - - uid: 235 - components: - - type: Transform - pos: -0.5,-12.5 - parent: 1 - - uid: 236 - components: - - type: Transform - pos: -0.5,-11.5 - parent: 1 - - uid: 237 - components: - - type: Transform - pos: -0.5,-10.5 - parent: 1 - - uid: 238 - components: - - type: Transform - pos: -0.5,-9.5 - parent: 1 - - uid: 239 - components: - - type: Transform - pos: -0.5,-8.5 - parent: 1 - - uid: 240 - components: - - type: Transform - pos: -3.5,-20.5 - parent: 1 - - uid: 241 - components: - - type: Transform - pos: -3.5,-21.5 - parent: 1 - - uid: 242 - components: - - type: Transform - pos: -4.5,-21.5 - parent: 1 - - uid: 243 - components: - - type: Transform - pos: -4.5,-22.5 - parent: 1 - - uid: 244 - components: - - type: Transform - pos: -4.5,-23.5 - parent: 1 - - uid: 245 - components: - - type: Transform - pos: -4.5,-24.5 - parent: 1 - - uid: 246 - components: - - type: Transform - pos: -4.5,-25.5 - parent: 1 - - uid: 247 - components: - - type: Transform - pos: -4.5,-26.5 - parent: 1 - - uid: 248 - components: - - type: Transform - pos: -4.5,-27.5 - parent: 1 - - uid: 249 - components: - - type: Transform - pos: 1.5,-8.5 - parent: 1 - - uid: 250 - components: - - type: Transform - pos: 0.5,-8.5 - parent: 1 -- proto: CableTerminal - entities: - - uid: 251 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-20.5 - parent: 1 - - type: Physics - canCollide: False - - type: Fixtures - fixtures: {} - - uid: 252 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -4.5,-20.5 - parent: 1 - - type: Physics - canCollide: False - - type: Fixtures - fixtures: {} -- proto: Carpet - entities: - - uid: 253 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-5.5 - parent: 1 - - uid: 254 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-4.5 - parent: 1 -- proto: Catwalk - entities: - - uid: 255 - components: - - type: Transform - pos: -2.5,-26.5 - parent: 1 - - uid: 256 - components: - - type: Transform - pos: -2.5,-28.5 - parent: 1 - - uid: 257 - components: - - type: Transform - pos: -8.5,-16.5 - parent: 1 - - uid: 258 - components: - - type: Transform - pos: -1.5,-16.5 - parent: 1 - - uid: 259 - components: - - type: Transform - pos: -2.5,-16.5 - parent: 1 - - uid: 260 - components: - - type: Transform - pos: 4.5,-24.5 - parent: 1 - - uid: 261 - components: - - type: Transform - pos: 5.5,-24.5 - parent: 1 - - uid: 262 - components: - - type: Transform - pos: -6.5,-16.5 - parent: 1 - - uid: 263 - components: - - type: Transform - pos: -7.5,-16.5 - parent: 1 - - uid: 264 - components: - - type: Transform - pos: -4.5,-13.5 - parent: 1 - - uid: 265 - components: - - type: Transform - pos: -4.5,-12.5 - parent: 1 - - uid: 266 - components: - - type: Transform - pos: -4.5,-11.5 - parent: 1 - - uid: 267 - components: - - type: Transform - pos: 3.5,-13.5 - parent: 1 - - uid: 268 - components: - - type: Transform - pos: 3.5,-12.5 - parent: 1 - - uid: 269 - components: - - type: Transform - pos: 3.5,-11.5 - parent: 1 - - uid: 270 - components: - - type: Transform - pos: 5.5,-16.5 - parent: 1 - - uid: 271 - components: - - type: Transform - pos: 6.5,-16.5 - parent: 1 - - uid: 272 - components: - - type: Transform - pos: -0.5,-24.5 - parent: 1 - - uid: 273 - components: - - type: Transform - pos: -0.5,-23.5 - parent: 1 - - uid: 274 - components: - - type: Transform - pos: -0.5,-16.5 - parent: 1 - - uid: 275 - components: - - type: Transform - pos: 5.5,-9.5 - parent: 1 - - uid: 276 - components: - - type: Transform - pos: 3.5,-9.5 - parent: 1 - - uid: 277 - components: - - type: Transform - pos: 0.5,-16.5 - parent: 1 - - uid: 278 - components: - - type: Transform - pos: 0.5,-26.5 - parent: 1 - - uid: 279 - components: - - type: Transform - pos: 1.5,-27.5 - parent: 1 - - uid: 280 - components: - - type: Transform - pos: -2.5,-27.5 - parent: 1 - - uid: 281 - components: - - type: Transform - pos: -2.5,-29.5 - parent: 1 - - uid: 282 - components: - - type: Transform - pos: 1.5,-26.5 - parent: 1 - - uid: 283 - components: - - type: Transform - pos: 4.5,-9.5 - parent: 1 - - uid: 284 - components: - - type: Transform - pos: -0.5,-26.5 - parent: 1 - - uid: 285 - components: - - type: Transform - pos: 1.5,-28.5 - parent: 1 - - uid: 286 - components: - - type: Transform - pos: 1.5,-29.5 - parent: 1 - - uid: 287 - components: - - type: Transform - pos: -1.5,-26.5 - parent: 1 - - uid: 288 - components: - - type: Transform - pos: -3.5,-9.5 - parent: 1 - - uid: 289 - components: - - type: Transform - pos: 1.5,-16.5 - parent: 1 - - uid: 290 - components: - - type: Transform - pos: 2.5,-9.5 - parent: 1 - - uid: 291 - components: - - type: Transform - pos: -6.5,-9.5 - parent: 1 - - uid: 292 - components: - - type: Transform - pos: -5.5,-9.5 - parent: 1 - - uid: 293 - components: - - type: Transform - pos: -4.5,-9.5 - parent: 1 - - uid: 294 - components: - - type: Transform - pos: 7.5,-16.5 - parent: 1 - - uid: 295 - components: - - type: Transform - pos: -0.5,-13.5 - parent: 1 - - uid: 296 - components: - - type: Transform - pos: -0.5,-12.5 - parent: 1 - - uid: 297 - components: - - type: Transform - pos: -0.5,-11.5 - parent: 1 -- proto: Chair - entities: - - uid: 298 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 2.5,-21.5 - parent: 1 - - uid: 299 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-21.5 - parent: 1 -- proto: ChairOfficeDark - entities: - - uid: 300 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-3.5 - parent: 1 -- proto: ChairPilotSeat - entities: - - uid: 301 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-21.5 - parent: 1 - - uid: 302 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-8.5 - parent: 1 - - uid: 303 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-9.5 - parent: 1 - - uid: 304 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-10.5 - parent: 1 - - uid: 305 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-8.5 - parent: 1 - - uid: 306 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-10.5 - parent: 1 - - uid: 307 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-9.5 - parent: 1 - - uid: 308 - components: - - type: Transform - pos: -2.5,-15.5 - parent: 1 - - uid: 309 - components: - - type: Transform - pos: -1.5,-15.5 - parent: 1 - - uid: 310 - components: - - type: Transform - pos: 0.5,-15.5 - parent: 1 - - uid: 311 - components: - - type: Transform - pos: 1.5,-15.5 - parent: 1 - - uid: 312 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -0.5,-3.5 - parent: 1 -- proto: CigPackSyndicate - entities: - - uid: 313 - components: - - type: Transform - pos: -3.5658307,-17.516623 - parent: 1 -- proto: ClothingBackpackDuffelSyndicateFilledMedical - entities: - - uid: 314 - components: - - type: Transform - pos: -3.5044222,-6.293252 - parent: 1 -- proto: ClothingHeadHatSyndie - entities: - - uid: 315 - components: - - type: Transform - pos: -3.1200604,-17.289778 - parent: 1 -- proto: ClothingHeadHatSyndieMAA - entities: - - uid: 35 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ClothingHeadPyjamaSyndicateRed - entities: - - uid: 36 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ClothingHeadsetAltSyndicate - entities: - - uid: 316 - components: - - type: Transform - pos: 1.3157192,-13.513277 - parent: 1 -- proto: ClothingMaskGasSyndicate - entities: - - uid: 317 - components: - - type: Transform - pos: 0.94071925,-13.482027 - parent: 1 -- proto: ClothingNeckMantleHOS - entities: - - uid: 37 - components: - - type: MetaData - desc: Looted from a fallen enemy, the commander earned this in battle. - name: commander mantle - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ClothingOuterCoatSyndieCap - entities: - - uid: 38 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ClothingUniformJumpskirtSyndieFormalDress - entities: - - uid: 39 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ClothingUniformJumpsuitPyjamaSyndicateRed - entities: - - uid: 40 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ClothingUniformJumpsuitSyndieFormal - entities: - - uid: 41 - components: - - type: Transform - parent: 33 - - type: Physics - canCollide: False - - type: InsideEntityStorage -- proto: ComfyChair - entities: - - uid: 318 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-4.5 - parent: 1 -- proto: computerBodyScanner - entities: - - uid: 319 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -5.5,-5.5 - parent: 1 -- proto: ComputerIFFSyndicate - entities: - - uid: 320 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-3.5 - parent: 1 -- proto: ComputerPowerMonitoring - entities: - - uid: 321 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -3.5,-21.5 - parent: 1 -- proto: ComputerRadar - entities: - - uid: 322 - components: - - type: Transform - pos: 0.5,-2.5 - parent: 1 -- proto: ComputerShuttleSyndie - entities: - - uid: 323 - components: - - type: Transform - pos: -0.5,-2.5 - parent: 1 -- proto: CrowbarRed - entities: - - uid: 324 - components: - - type: Transform - pos: -3.492848,-22.485775 - parent: 1 -- proto: DisposalBend - entities: - - uid: 325 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-6.5 - parent: 1 -- proto: DisposalJunction - entities: - - uid: 326 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -0.5,-19.5 - parent: 1 -- proto: DisposalPipe - entities: - - uid: 327 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-6.5 - parent: 1 - - uid: 328 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-19.5 - parent: 1 - - uid: 329 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-19.5 - parent: 1 - - uid: 330 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -3.5,-19.5 - parent: 1 - - uid: 331 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -4.5,-19.5 - parent: 1 - - uid: 332 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -5.5,-19.5 - parent: 1 - - uid: 333 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -6.5,-19.5 - parent: 1 - - uid: 334 - components: - - type: Transform - pos: -0.5,-18.5 - parent: 1 - - uid: 335 - components: - - type: Transform - pos: -0.5,-17.5 - parent: 1 - - uid: 336 - components: - - type: Transform - pos: -0.5,-16.5 - parent: 1 - - uid: 337 - components: - - type: Transform - pos: -0.5,-15.5 - parent: 1 - - uid: 338 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 1 - - uid: 339 - components: - - type: Transform - pos: -0.5,-13.5 - parent: 1 - - uid: 340 - components: - - type: Transform - pos: -0.5,-12.5 - parent: 1 - - uid: 341 - components: - - type: Transform - pos: -0.5,-11.5 - parent: 1 - - uid: 342 - components: - - type: Transform - pos: -0.5,-10.5 - parent: 1 - - uid: 343 - components: - - type: Transform - pos: -0.5,-9.5 - parent: 1 - - uid: 344 - components: - - type: Transform - pos: -0.5,-8.5 - parent: 1 - - uid: 345 - components: - - type: Transform - pos: -0.5,-7.5 - parent: 1 -- proto: DisposalTrunk - entities: - - uid: 346 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-6.5 - parent: 1 - - uid: 347 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -7.5,-19.5 - parent: 1 - - uid: 348 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-19.5 - parent: 1 -- proto: DisposalUnit - entities: - - uid: 349 - components: - - type: Transform - pos: 1.5,-6.5 - parent: 1 - - uid: 350 - components: - - type: Transform - pos: 0.5,-19.5 - parent: 1 -- proto: DoubleEmergencyOxygenTankFilled - entities: - - uid: 351 - components: - - type: Transform - pos: -1.6924903,-23.407444 - parent: 1 - - uid: 352 - components: - - type: Transform - pos: -1.4112403,-23.458082 - parent: 1 - - uid: 353 - components: - - type: Transform - pos: 5.390987,-17.346693 - parent: 1 - - uid: 354 - components: - - type: Transform - pos: -6.6334953,-17.346693 - parent: 1 -- proto: DrinkGlass - entities: - - uid: 355 - components: - - type: Transform - pos: 2.0779252,-19.21155 - parent: 1 - - uid: 356 - components: - - type: Transform - pos: 2.3123002,-19.21155 - parent: 1 -- proto: DrinkMugDog - entities: - - uid: 357 - components: - - type: Transform - pos: 2.2843437,-19.542192 - parent: 1 -- proto: DrinkMugMetal - entities: - - uid: 358 - components: - - type: Transform - pos: 2.0968437,-19.526567 - parent: 1 -- proto: DrinkMugRed - entities: - - uid: 359 - components: - - type: Transform - pos: 1.9918958,-17.588755 - parent: 1 -- proto: DrinkVacuumFlask - entities: - - uid: 360 - components: - - type: Transform - pos: 5.6435027,-21.180143 - parent: 1 - - uid: 361 - components: - - type: Transform - pos: 5.7372527,-21.398893 - parent: 1 -- proto: ExtendedEmergencyOxygenTankFilled - entities: - - uid: 362 - components: - - type: Transform - pos: -5.678572,-12.319441 - parent: 1 - - uid: 363 - components: - - type: Transform - pos: 4.305803,-12.272566 - parent: 1 -- proto: FireAxeFlaming - entities: - - uid: 23 - components: - - type: Transform - pos: -1.5018963,-3.4569345 - parent: 1 -- proto: FoodBoxDonkpocketPizza - entities: - - uid: 364 - components: - - type: Transform - pos: 2.7185502,-19.320925 - parent: 1 -- proto: FoodBoxDonut - entities: - - uid: 365 - components: - - type: Transform - pos: 5.5401826,-21.187487 - parent: 1 -- proto: FoodPizzaDonkpocket - entities: - - uid: 366 - components: - - type: Transform - pos: 1.4776825,-21.296862 - parent: 1 -- proto: FoodSnackSyndi - entities: - - uid: 367 - components: - - type: Transform - pos: 1.5361897,-17.367903 - parent: 1 -- proto: GasMinerNitrogen - entities: - - uid: 368 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-27.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 -- proto: GasMinerOxygen - entities: - - uid: 369 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-27.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 -- proto: GasMixer - entities: - - uid: 370 - components: - - type: MetaData - name: O2+N2 mixer - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-24.5 - parent: 1 - - type: GasMixer - inletTwoConcentration: 0.78 - inletOneConcentration: 0.22 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' -- proto: GasPassiveVent - entities: - - uid: 371 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-26.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - uid: 372 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-26.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - uid: 373 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 6.5,-19.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 -- proto: GasPipeBend - entities: - - uid: 374 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-5.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 375 - components: - - type: Transform - pos: 5.5,-24.5 - parent: 1 - - uid: 376 - components: - - type: Transform - pos: 3.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 377 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -4.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 378 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-5.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 379 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-4.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 380 - components: - - type: Transform - pos: 0.5,-8.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 381 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -1.5,-8.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 382 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -0.5,-4.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 383 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-21.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' -- proto: GasPipeFourway - entities: - - uid: 384 - components: - - type: Transform - pos: 3.5,-23.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 385 - components: - - type: Transform - pos: -0.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 386 - components: - - type: Transform - pos: -0.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 387 - components: - - type: Transform - pos: 0.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' -- proto: GasPipeStraight - entities: - - uid: 388 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-25.5 - parent: 1 - - uid: 389 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-25.5 - parent: 1 - - uid: 390 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-24.5 - parent: 1 - - uid: 391 - components: - - type: Transform - pos: 3.5,-22.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 392 - components: - - type: Transform - pos: 3.5,-21.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 393 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 2.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 394 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 395 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 396 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 397 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 398 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -3.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 399 - components: - - type: Transform - pos: -0.5,-19.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 400 - components: - - type: Transform - pos: -0.5,-18.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 401 - components: - - type: Transform - pos: -0.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 402 - components: - - type: Transform - pos: -0.5,-15.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 403 - components: - - type: Transform - pos: -0.5,-14.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 404 - components: - - type: Transform - pos: -0.5,-13.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 405 - components: - - type: Transform - pos: -0.5,-12.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 406 - components: - - type: Transform - pos: -0.5,-10.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 407 - components: - - type: Transform - pos: -0.5,-9.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 408 - components: - - type: Transform - pos: -0.5,-8.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 409 - components: - - type: Transform - pos: -0.5,-7.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 410 - components: - - type: Transform - pos: -0.5,-6.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 411 - components: - - type: Transform - pos: 0.5,-18.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 412 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 413 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 414 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 415 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 416 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -3.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 417 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 2.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 418 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 5.5,-19.5 - parent: 1 - - uid: 419 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 3.5,-19.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 420 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-19.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 421 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -1.5,-6.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 422 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -0.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 423 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 424 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 425 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -3.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 426 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 427 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 2.5,-17.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 428 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-16.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 429 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-15.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 430 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-14.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 431 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-13.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 432 - components: - - type: Transform - pos: 0.5,-11.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 433 - components: - - type: Transform - pos: 0.5,-10.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 434 - components: - - type: Transform - pos: 0.5,-9.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 435 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -0.5,-8.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 436 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -1.5,-7.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 437 - components: - - type: Transform - pos: 0.5,-20.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 438 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -0.5,-21.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 439 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-21.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 440 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-21.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 441 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -1.5,-5.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 442 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 0.5,-4.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' -- proto: GasPipeTJunction - entities: - - uid: 443 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -0.5,-11.5 - parent: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 444 - components: - - type: Transform - pos: 2.5,-19.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 445 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-19.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 446 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 0.5,-12.5 - parent: 1 - - type: AtmosPipeColor - color: '#FF1212FF' -- proto: GasPort - entities: - - uid: 447 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 2.5,-23.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' -- proto: GasPressurePump - entities: - - uid: 448 - components: - - type: MetaData - name: waste pump - - type: Transform - rot: 1.5707963267948966 rad - pos: 4.5,-19.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' -- proto: GasVentPump - entities: - - uid: 449 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 0.5,-11.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 450 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -0.5,-21.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 451 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-23.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 452 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -4.5,-21.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 453 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -4.5,-16.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 454 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 3.5,-16.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' - - uid: 455 - components: - - type: Transform - pos: 0.5,-3.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#0335FCFF' -- proto: GasVentScrubber - entities: - - uid: 456 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-12.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 457 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 3.5,-17.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 458 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 2.5,-20.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 459 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -4.5,-17.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 460 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -3.5,-21.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' - - uid: 461 - components: - - type: Transform - pos: -0.5,-3.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 - - type: AtmosPipeColor - color: '#FF1212FF' -- proto: GeneratorBasic15kW - entities: - - uid: 462 - components: - - type: Transform - pos: -4.5,-26.5 - parent: 1 - - uid: 463 - components: - - type: Transform - pos: -4.5,-25.5 - parent: 1 - - uid: 464 - components: - - type: Transform - pos: -4.5,-24.5 - parent: 1 - - uid: 465 - components: - - type: Transform - pos: -6.5,-24.5 - parent: 1 - - uid: 466 - components: - - type: Transform - pos: -6.5,-25.5 - parent: 1 - - uid: 467 - components: - - type: Transform - pos: -6.5,-26.5 - parent: 1 -- proto: GeneratorWallmountAPU - entities: - - uid: 468 - components: - - type: Transform - pos: -6.5,-20.5 - parent: 1 -- proto: GravityGeneratorMini - entities: - - uid: 469 - components: - - type: Transform - pos: -5.5,-22.5 - parent: 1 -- proto: Grille - entities: - - uid: 470 - components: - - type: Transform - pos: 2.5,-12.5 - parent: 1 - - uid: 471 - components: - - type: Transform - pos: -3.5,-12.5 - parent: 1 - - uid: 472 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-7.5 - parent: 1 - - uid: 473 - components: - - type: Transform - pos: -2.5,-1.5 - parent: 1 - - uid: 474 - components: - - type: Transform - pos: -0.5,-1.5 - parent: 1 - - uid: 475 - components: - - type: Transform - pos: -4.5,-2.5 - parent: 1 - - uid: 476 - components: - - type: Transform - pos: -6.5,-4.5 - parent: 1 - - uid: 477 - components: - - type: Transform - pos: 3.5,-2.5 - parent: 1 - - uid: 478 - components: - - type: Transform - pos: -1.5,-1.5 - parent: 1 - - uid: 479 - components: - - type: Transform - pos: 5.5,-4.5 - parent: 1 - - uid: 480 - components: - - type: Transform - pos: 0.5,-1.5 - parent: 1 - - uid: 481 - components: - - type: Transform - pos: 1.5,-9.5 - parent: 1 - - uid: 482 - components: - - type: Transform - pos: -2.5,-9.5 - parent: 1 - - uid: 483 - components: - - type: Transform - pos: 1.5,-1.5 - parent: 1 - - uid: 484 - components: - - type: Transform - pos: -2.5,-21.5 - parent: 1 - - uid: 485 - components: - - type: Transform - pos: -2.5,-19.5 - parent: 1 - - uid: 486 - components: - - type: Transform - pos: 2.5,-22.5 - parent: 1 - - uid: 487 - components: - - type: Transform - pos: 4.5,-22.5 - parent: 1 - - uid: 488 - components: - - type: Transform - pos: -1.5,-18.5 - parent: 1 - - uid: 489 - components: - - type: Transform - pos: 0.5,-7.5 - parent: 1 - - uid: 490 - components: - - type: Transform - pos: 3.5,-25.5 - parent: 1 - - uid: 491 - components: - - type: Transform - pos: 5.5,-25.5 - parent: 1 - - uid: 492 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 5.5,-19.5 - parent: 1 - - uid: 493 - components: - - type: Transform - pos: 0.5,-18.5 - parent: 1 -- proto: Gyroscope - entities: - - uid: 494 - components: - - type: Transform - pos: -5.5,-13.5 - parent: 1 - - uid: 495 - components: - - type: Transform - pos: 4.5,-13.5 - parent: 1 -- proto: HospitalCurtainsOpen - entities: - - uid: 496 - components: - - type: Transform - pos: 3.5,-3.5 - parent: 1 -- proto: KitchenMicrowave - entities: - - uid: 497 - components: - - type: Transform - pos: 3.5,-19.5 - parent: 1 -- proto: KnifePlastic - entities: - - uid: 498 - components: - - type: Transform - pos: 5.3509636,-21.445768 - parent: 1 -- proto: Lamp - entities: - - uid: 499 - components: - - type: Transform - pos: -1.483297,-2.2444057 - parent: 1 -- proto: LockerSyndicatePersonal - entities: - - uid: 33 - components: - - type: Transform - pos: 4.5,-5.5 - parent: 1 - - type: EntityStorage - air: - volume: 200 - immutable: False - temperature: 293.1496 - moles: - - 1.7459903 - - 6.568249 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - - type: ContainerContainer - containers: - entity_storage: !type:Container - showEnts: False - occludes: True - ents: - - 37 - - 39 - - 36 - - 40 - - 41 - - 38 - - 35 - - 34 - paper_label: !type:ContainerSlot - showEnts: False - occludes: True - ent: null -- proto: MedicalBed - entities: - - uid: 500 - components: - - type: Transform - pos: -4.5,-3.5 - parent: 1 -- proto: MedkitCombatFilled - entities: - - uid: 501 - components: - - type: Transform - pos: -3.401462,-3.5350046 - parent: 1 - - uid: 502 - components: - - type: Transform - pos: -3.557712,-3.4256296 - parent: 1 -- proto: Mirror - entities: - - uid: 503 - components: - - type: Transform - pos: 4.5,-3.5 - parent: 1 -- proto: Multitool - entities: - - uid: 504 - components: - - type: Transform - pos: -3.383473,-22.548275 - parent: 1 -- proto: NitrogenTankFilled - entities: - - uid: 505 - components: - - type: Transform - pos: 4.633928,-12.616316 - parent: 1 - - uid: 506 - components: - - type: Transform - pos: -5.397322,-12.569441 - parent: 1 - - uid: 507 - components: - - type: Transform - pos: -6.3522453,-17.549818 - parent: 1 - - uid: 508 - components: - - type: Transform - pos: 5.6633797,-17.565443 - parent: 1 -- proto: NuclearBombUnanchored - entities: - - uid: 509 - components: - - type: Transform - pos: -2.5,-12.5 - parent: 1 -- proto: NukeCodePaper - entities: - - uid: 510 - components: - - type: Transform - pos: -2.5286522,-11.44479 - parent: 1 -- proto: Ointment - entities: - - uid: 511 - components: - - type: Transform - pos: -3.651462,-4.5193796 - parent: 1 - - uid: 512 - components: - - type: Transform - pos: -3.667087,-4.2225046 - parent: 1 -- proto: OperatingTable - entities: - - uid: 513 - components: - - type: Transform - pos: -5.5,-4.5 - parent: 1 -- proto: OxygenCanister - entities: - - uid: 514 - components: - - type: Transform - pos: -1.5,-24.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 -- proto: OxygenTankFilled - entities: - - uid: 515 - components: - - type: Transform - pos: -5.600447,-12.569441 - parent: 1 - - uid: 516 - components: - - type: Transform - pos: 4.399553,-12.522566 - parent: 1 - - uid: 517 - components: - - type: Transform - pos: 5.5227547,-17.440443 - parent: 1 - - uid: 518 - components: - - type: Transform - pos: -6.4928703,-17.440443 - parent: 1 -- proto: PinpointerNuclear - entities: - - uid: 519 - components: - - type: Transform - pos: -2.4942985,-13.37949 - parent: 1 -- proto: PlasmaReinforcedWindowDirectional - entities: - - uid: 520 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-4.5 - parent: 1 - - uid: 521 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-6.5 - parent: 1 - - uid: 522 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-3.5 - parent: 1 -- proto: PlushieNuke - entities: - - uid: 523 - components: - - type: Transform - pos: -2.4227936,-2.3320491 - parent: 1 -- proto: PosterContrabandC20r - entities: - - uid: 524 - components: - - type: Transform - pos: 1.5,-14.5 - parent: 1 -- proto: PosterContrabandCC64KAd - entities: - - uid: 525 - components: - - type: Transform - pos: -5.5,-18.5 - parent: 1 -- proto: PosterContrabandCybersun600 - entities: - - uid: 526 - components: - - type: Transform - pos: 2.5,-6.5 - parent: 1 -- proto: PosterContrabandDonk - entities: - - uid: 527 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-18.5 - parent: 1 -- proto: PosterContrabandDonutCorp - entities: - - uid: 528 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 1.5,-22.5 - parent: 1 -- proto: PosterContrabandEnergySwords - entities: - - uid: 529 - components: - - type: Transform - pos: 2.5,-18.5 - parent: 1 -- proto: PosterContrabandEnlistGorlex - entities: - - uid: 530 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 2.5,-13.5 - parent: 1 -- proto: PosterContrabandFreeSyndicateEncryptionKey - entities: - - uid: 531 - components: - - type: Transform - pos: -2.5,-8.5 - parent: 1 -- proto: PosterContrabandInterdyne - entities: - - uid: 532 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -4.5,-6.5 - parent: 1 -- proto: PosterContrabandKosmicheskayaStantsiya - entities: - - uid: 533 - components: - - type: Transform - pos: -2.5,-22.5 - parent: 1 -- proto: PosterContrabandMoth - entities: - - uid: 534 - components: - - type: Transform - pos: -2.5,-14.5 - parent: 1 - - uid: 535 - components: - - type: Transform - pos: 2.5,-3.5 - parent: 1 -- proto: PosterContrabandNuclearDeviceInformational - entities: - - uid: 536 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -3.5,-13.5 - parent: 1 - - uid: 537 - components: - - type: Transform - pos: 0.5,-14.5 - parent: 1 -- proto: PosterContrabandSyndicatePistol - entities: - - uid: 538 - components: - - type: Transform - pos: 1.5,-10.5 - parent: 1 -- proto: PosterContrabandSyndicateRecruitment - entities: - - uid: 539 - components: - - type: Transform - pos: -1.5,-14.5 - parent: 1 -- proto: PosterContrabandWaffleCorp - entities: - - uid: 540 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -2.5,-23.5 - parent: 1 -- proto: Poweredlight - entities: - - uid: 541 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-8.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 542 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-4.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 543 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -6.5,-23.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 544 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -6.5,-26.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 545 - components: - - type: Transform - pos: -4.5,-19.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 546 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 1.5,-13.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 547 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-27.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 548 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-27.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 549 - components: - - type: Transform - pos: 2.5,-15.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 550 - components: - - type: Transform - pos: -3.5,-15.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 551 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -4.5,-5.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 552 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-2.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 553 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-6.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 -- proto: PoweredSmallLight - entities: - - uid: 554 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-13.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 555 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-21.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 556 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-5.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 557 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 5.5,-24.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 558 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-13.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 559 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -1.5,-24.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 560 - components: - - type: Transform - pos: -6.5,-15.5 - parent: 1 - - uid: 561 - components: - - type: Transform - pos: 5.5,-15.5 - parent: 1 - - uid: 562 - components: - - type: Transform - pos: 2.5,-19.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 563 - components: - - type: Transform - pos: -1.5,-26.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 564 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-9.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 - - uid: 565 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-9.5 - parent: 1 - - type: ApcPowerReceiver - powerLoad: 0 -- proto: Rack - entities: - - uid: 566 - components: - - type: Transform - pos: -3.5,-22.5 - parent: 1 - - uid: 567 - components: - - type: Transform - pos: 5.5,-23.5 - parent: 1 - - uid: 568 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-12.5 - parent: 1 - - uid: 569 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-12.5 - parent: 1 - - uid: 570 - components: - - type: Transform - pos: -1.5,-23.5 - parent: 1 - - uid: 571 - components: - - type: Transform - pos: -6.5,-17.5 - parent: 1 - - uid: 572 - components: - - type: Transform - pos: 5.5,-17.5 - parent: 1 -- proto: ReinforcedPlasmaWindow - entities: - - uid: 573 - components: - - type: Transform - pos: 3.5,-2.5 - parent: 1 - - uid: 574 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 5.5,-19.5 - parent: 1 - - uid: 575 - components: - - type: Transform - pos: -0.5,-1.5 - parent: 1 - - uid: 576 - components: - - type: Transform - pos: -6.5,-4.5 - parent: 1 - - uid: 577 - components: - - type: Transform - pos: -2.5,-1.5 - parent: 1 - - uid: 578 - components: - - type: Transform - pos: -4.5,-2.5 - parent: 1 - - uid: 579 - components: - - type: Transform - pos: 5.5,-4.5 - parent: 1 - - uid: 580 - components: - - type: Transform - pos: -1.5,-1.5 - parent: 1 - - uid: 581 - components: - - type: Transform - pos: 0.5,-1.5 - parent: 1 - - uid: 582 - components: - - type: Transform - pos: 1.5,-1.5 - parent: 1 - - uid: 583 - components: - - type: Transform - pos: -1.5,-7.5 - parent: 1 - - uid: 584 - components: - - type: Transform - pos: 4.5,-22.5 - parent: 1 - - uid: 585 - components: - - type: Transform - pos: 2.5,-22.5 - parent: 1 - - uid: 586 - components: - - type: Transform - pos: -2.5,-21.5 - parent: 1 - - uid: 587 - components: - - type: Transform - pos: -2.5,-19.5 - parent: 1 - - uid: 588 - components: - - type: Transform - pos: -2.5,-9.5 - parent: 1 - - uid: 589 - components: - - type: Transform - pos: 1.5,-9.5 - parent: 1 - - uid: 590 - components: - - type: Transform - pos: 0.5,-18.5 - parent: 1 - - uid: 591 - components: - - type: Transform - pos: 3.5,-25.5 - parent: 1 - - uid: 592 - components: - - type: Transform - pos: 5.5,-25.5 - parent: 1 - - uid: 593 - components: - - type: Transform - pos: 2.5,-12.5 - parent: 1 - - uid: 594 - components: - - type: Transform - pos: -3.5,-12.5 - parent: 1 - - uid: 595 - components: - - type: Transform - pos: 0.5,-7.5 - parent: 1 - - uid: 596 - components: - - type: Transform - pos: -1.5,-18.5 - parent: 1 -- proto: ShuttersNormalOpen - entities: - - uid: 597 - components: - - type: Transform - pos: -6.5,-4.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 - - uid: 598 - components: - - type: Transform - pos: 5.5,-4.5 - parent: 1 - - type: DeviceLinkSink - links: - - 20 - - uid: 599 - components: - - type: Transform - pos: 1.5,-9.5 - parent: 1 - - type: DeviceLinkSink - links: - - 609 - - uid: 600 - components: - - type: Transform - pos: -2.5,-9.5 - parent: 1 - - type: DeviceLinkSink - links: - - 609 - - uid: 601 - components: - - type: Transform - pos: -4.5,-2.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 - - uid: 602 - components: - - type: Transform - pos: 3.5,-2.5 - parent: 1 - - type: DeviceLinkSink - links: - - 20 - - uid: 603 - components: - - type: Transform - pos: 1.5,-1.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 - - uid: 604 - components: - - type: Transform - pos: -0.5,-1.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 - - uid: 605 - components: - - type: Transform - pos: 5.5,-19.5 - parent: 1 - - type: DeviceLinkSink - links: - - 611 - - uid: 606 - components: - - type: Transform - pos: -1.5,-1.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 - - uid: 607 - components: - - type: Transform - pos: 0.5,-1.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 - - uid: 608 - components: - - type: Transform - pos: -2.5,-1.5 - parent: 1 - - type: DeviceLinkSink - links: - - 21 -- proto: SignalButton - entities: - - uid: 609 - components: - - type: Transform - pos: -2.5,-10.5 - parent: 1 - - type: DeviceLinkSource - linkedPorts: - 600: - - Pressed: Toggle - 599: - - Pressed: Toggle - - uid: 611 - components: - - type: Transform - pos: 5.5,-20.5 - parent: 1 - - type: DeviceLinkSource - linkedPorts: - 605: - - Pressed: Toggle -- proto: SignalButtonDirectional - entities: - - uid: 20 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 2.5,-4.5 - parent: 1 - - type: DeviceLinkSource - linkedPorts: - 602: - - Pressed: Toggle - 598: - - Pressed: Toggle - - uid: 21 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -2.5,-7.5 - parent: 1 - - type: DeviceLinkSource - linkedPorts: - 597: - - Pressed: Toggle - 601: - - Pressed: Toggle - 608: - - Pressed: Toggle - 606: - - Pressed: Toggle - 604: - - Pressed: Toggle - 607: - - Pressed: Toggle - 603: - - Pressed: Toggle -- proto: SignDirectionalEvac - entities: - - uid: 613 - components: - - type: Transform - pos: 0.5,-22.5 - parent: 1 - - uid: 614 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 4.5,-15.5 - parent: 1 - - uid: 615 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-14.5 - parent: 1 - - uid: 616 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-14.5 - parent: 1 - - uid: 617 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -5.5,-15.5 - parent: 1 -- proto: SignElectricalMed - entities: - - uid: 618 - components: - - type: Transform - pos: -2.5,-18.5 - parent: 1 -- proto: SignNosmoking - entities: - - uid: 619 - components: - - type: Transform - pos: 5.5,-22.5 - parent: 1 -- proto: SignSecureSmallRed - entities: - - uid: 620 - components: - - type: Transform - pos: -3.5,-11.5 - parent: 1 - - uid: 621 - components: - - type: Transform - pos: 4.5,-17.5 - parent: 1 - - uid: 622 - components: - - type: Transform - pos: 2.5,-11.5 - parent: 1 -- proto: SMESBasic - entities: - - uid: 623 - components: - - type: Transform - pos: -5.5,-19.5 - parent: 1 - - uid: 624 - components: - - type: Transform - pos: -4.5,-19.5 - parent: 1 -- proto: SoapSyndie - entities: - - uid: 625 - components: - - type: Transform - pos: 2.4424396,-17.430403 - parent: 1 -- proto: soda_dispenser - entities: - - uid: 626 - components: - - type: Transform - pos: 1.5,-19.5 - parent: 1 -- proto: StorageCanister - entities: - - uid: 627 - components: - - type: Transform - pos: 2.5,-23.5 - parent: 1 - - type: AtmosDevice - joinedGrid: 1 -- proto: SubstationBasic - entities: - - uid: 628 - components: - - type: Transform - pos: -3.5,-19.5 - parent: 1 -- proto: SuitStorageEVASyndicate - entities: - - uid: 629 - components: - - type: Transform - pos: 0.5,-24.5 - parent: 1 - - uid: 630 - components: - - type: Transform - pos: 0.5,-23.5 - parent: 1 - - uid: 631 - components: - - type: Transform - pos: 3.5,-17.5 - parent: 1 - - uid: 632 - components: - - type: Transform - pos: -4.5,-17.5 - parent: 1 -- proto: SyndieMiniBomb - entities: - - uid: 633 - components: - - type: Transform - pos: 1.4127003,-11.973867 - parent: 1 - - uid: 634 - components: - - type: Transform - pos: 1.6939503,-11.973867 - parent: 1 -- proto: SyringeInaprovaline - entities: - - uid: 635 - components: - - type: Transform - pos: -3.510837,-4.3787546 - parent: 1 - - uid: 636 - components: - - type: Transform - pos: -3.510837,-4.0193796 - parent: 1 -- proto: Table - entities: - - uid: 637 - components: - - type: Transform - pos: 5.5,-21.5 - parent: 1 - - uid: 638 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 1.5,-21.5 - parent: 1 - - uid: 639 - components: - - type: Transform - pos: 3.5,-19.5 - parent: 1 - - uid: 640 - components: - - type: Transform - pos: 2.5,-19.5 - parent: 1 - - uid: 641 - components: - - type: Transform - pos: 1.5,-19.5 - parent: 1 -- proto: TablePlasmaGlass - entities: - - uid: 642 - components: - - type: Transform - pos: -3.5,-4.5 - parent: 1 - - uid: 643 - components: - - type: Transform - pos: -3.5,-6.5 - parent: 1 - - uid: 644 - components: - - type: Transform - pos: -3.5,-3.5 - parent: 1 -- proto: TableReinforced - entities: - - uid: 645 - components: - - type: Transform - pos: -2.5,-11.5 - parent: 1 - - uid: 646 - components: - - type: Transform - pos: -2.5,-13.5 - parent: 1 - - uid: 647 - components: - - type: Transform - pos: -1.5,-3.5 - parent: 1 - - uid: 648 - components: - - type: Transform - pos: -2.5,-2.5 - parent: 1 - - uid: 649 - components: - - type: Transform - pos: 0.5,-13.5 - parent: 1 - - uid: 650 - components: - - type: Transform - pos: 1.5,-13.5 - parent: 1 - - uid: 651 - components: - - type: Transform - pos: 1.5,-12.5 - parent: 1 - - uid: 652 - components: - - type: Transform - pos: 1.5,-11.5 - parent: 1 - - uid: 653 - components: - - type: Transform - pos: 2.5,-17.5 - parent: 1 - - uid: 654 - components: - - type: Transform - pos: 1.5,-17.5 - parent: 1 - - uid: 655 - components: - - type: Transform - pos: -2.5,-17.5 - parent: 1 - - uid: 656 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 1.5,-2.5 - parent: 1 - - uid: 657 - components: - - type: Transform - pos: -3.5,-17.5 - parent: 1 - - uid: 658 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -1.5,-2.5 - parent: 1 -- proto: Thruster - entities: - - uid: 659 - components: - - type: Transform - pos: -6.5,-2.5 - parent: 1 - - uid: 660 - components: - - type: Transform - pos: 5.5,-2.5 - parent: 1 - - uid: 661 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -5.5,-7.5 - parent: 1 - - uid: 662 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-7.5 - parent: 1 - - uid: 663 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -7.5,-13.5 - parent: 1 - - uid: 664 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 6.5,-13.5 - parent: 1 - - uid: 665 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-30.5 - parent: 1 - - uid: 666 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -4.5,-30.5 - parent: 1 - - uid: 667 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 4.5,-30.5 - parent: 1 - - uid: 668 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 3.5,-30.5 - parent: 1 -- proto: ToolboxSyndicate - entities: - - uid: 669 - components: - - type: Transform - pos: -2.5731854,-17.414778 - parent: 1 -- proto: ToolboxSyndicateFilled - entities: - - uid: 670 - components: - - type: Transform - pos: 1.5034143,-11.298322 - parent: 1 -- proto: VendingMachineTankDispenserEVA - entities: - - uid: 671 - components: - - type: Transform - pos: 5.5,-15.5 - parent: 1 - - uid: 672 - components: - - type: MetaData - name: tank dispenser - - type: Transform - pos: 2.5,-24.5 - parent: 1 - - uid: 673 - components: - - type: Transform - pos: -6.5,-15.5 - parent: 1 -- proto: VendingMachineYouTool - entities: - - uid: 674 - components: - - type: Transform - pos: -3.5,-24.5 - parent: 1 -- proto: WallPlastitanium - entities: - - uid: 11 - components: - - type: Transform - pos: -1.5,-25.5 - parent: 1 - - uid: 675 - components: - - type: Transform - pos: 2.5,-2.5 - parent: 1 - - uid: 676 - components: - - type: Transform - pos: 4.5,-3.5 - parent: 1 - - uid: 677 - components: - - type: Transform - pos: -3.5,-11.5 - parent: 1 - - uid: 678 - components: - - type: Transform - pos: 2.5,-14.5 - parent: 1 - - uid: 679 - components: - - type: Transform - pos: 4.5,-14.5 - parent: 1 - - uid: 680 - components: - - type: Transform - pos: -2.5,-14.5 - parent: 1 - - uid: 681 - components: - - type: Transform - pos: 2.5,-18.5 - parent: 1 - - uid: 682 - components: - - type: Transform - pos: 1.5,-25.5 - parent: 1 - - uid: 683 - components: - - type: Transform - pos: 1.5,-18.5 - parent: 1 - - uid: 684 - components: - - type: Transform - pos: 7.5,-17.5 - parent: 1 - - uid: 685 - components: - - type: Transform - pos: -3.5,-26.5 - parent: 1 - - uid: 686 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 3.5,-7.5 - parent: 1 - - uid: 687 - components: - - type: Transform - pos: 2.5,-13.5 - parent: 1 - - uid: 688 - components: - - type: Transform - pos: -3.5,-28.5 - parent: 1 - - uid: 689 - components: - - type: Transform - pos: 2.5,-10.5 - parent: 1 - - uid: 690 - components: - - type: Transform - pos: 4.5,-18.5 - parent: 1 - - uid: 691 - components: - - type: Transform - pos: -2.5,-10.5 - parent: 1 - - uid: 692 - components: - - type: Transform - pos: -3.5,-10.5 - parent: 1 - - uid: 693 - components: - - type: Transform - pos: -6.5,-11.5 - parent: 1 - - uid: 694 - components: - - type: Transform - pos: 4.5,-10.5 - parent: 1 - - uid: 695 - components: - - type: Transform - pos: 4.5,-2.5 - parent: 1 - - uid: 696 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -4.5,-7.5 - parent: 1 - - uid: 697 - components: - - type: Transform - pos: -6.5,-6.5 - parent: 1 - - uid: 698 - components: - - type: Transform - pos: -5.5,-6.5 - parent: 1 - - uid: 699 - components: - - type: Transform - pos: 4.5,-6.5 - parent: 1 - - uid: 700 - components: - - type: Transform - pos: -6.5,-5.5 - parent: 1 - - uid: 701 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -2.5,-8.5 - parent: 1 - - uid: 702 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-7.5 - parent: 1 - - uid: 703 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 1.5,-8.5 - parent: 1 - - uid: 704 - components: - - type: Transform - pos: 2.5,-11.5 - parent: 1 - - uid: 705 - components: - - type: Transform - pos: 5.5,-18.5 - parent: 1 - - uid: 706 - components: - - type: Transform - pos: -8.5,-15.5 - parent: 1 - - uid: 707 - components: - - type: Transform - pos: -8.5,-17.5 - parent: 1 - - uid: 708 - components: - - type: Transform - pos: -7.5,-17.5 - parent: 1 - - uid: 709 - components: - - type: Transform - pos: -5.5,-10.5 - parent: 1 - - uid: 710 - components: - - type: Transform - pos: -3.5,-13.5 - parent: 1 - - uid: 711 - components: - - type: Transform - pos: 3.5,-18.5 - parent: 1 - - uid: 712 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -4.5,-6.5 - parent: 1 - - uid: 713 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -3.5,-7.5 - parent: 1 - - uid: 714 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -3.5,-8.5 - parent: 1 - - uid: 715 - components: - - type: Transform - pos: 5.5,-14.5 - parent: 1 - - uid: 716 - components: - - type: Transform - pos: 7.5,-15.5 - parent: 1 - - uid: 717 - components: - - type: Transform - pos: -6.5,-19.5 - parent: 1 - - uid: 718 - components: - - type: Transform - pos: -6.5,-18.5 - parent: 1 - - uid: 719 - components: - - type: Transform - pos: -3.5,-25.5 - parent: 1 - - uid: 720 - components: - - type: Transform - pos: 2.5,-25.5 - parent: 1 - - uid: 721 - components: - - type: Transform - pos: 2.5,-27.5 - parent: 1 - - uid: 722 - components: - - type: Transform - pos: 4.5,-17.5 - parent: 1 - - uid: 723 - components: - - type: Transform - pos: -7.5,-21.5 - parent: 1 - - uid: 724 - components: - - type: Transform - pos: -7.5,-27.5 - parent: 1 - - uid: 725 - components: - - type: Transform - pos: -7.5,-24.5 - parent: 1 - - uid: 726 - components: - - type: Transform - pos: 6.5,-21.5 - parent: 1 - - uid: 727 - components: - - type: Transform - pos: 6.5,-24.5 - parent: 1 - - uid: 728 - components: - - type: Transform - pos: 6.5,-26.5 - parent: 1 - - uid: 729 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 2.5,-3.5 - parent: 1 - - uid: 730 - components: - - type: Transform - pos: 1.5,-10.5 - parent: 1 - - uid: 731 - components: - - type: Transform - pos: -3.5,-27.5 - parent: 1 - - uid: 732 - components: - - type: Transform - pos: -6.5,-12.5 - parent: 1 - - uid: 733 - components: - - type: Transform - pos: 5.5,-12.5 - parent: 1 - - uid: 734 - components: - - type: Transform - pos: 5.5,-11.5 - parent: 1 - - uid: 735 - components: - - type: Transform - pos: -5.5,-11.5 - parent: 1 - - uid: 736 - components: - - type: Transform - pos: -6.5,-20.5 - parent: 1 - - uid: 737 - components: - - type: Transform - pos: 0.5,-14.5 - parent: 1 - - uid: 738 - components: - - type: Transform - pos: -1.5,-14.5 - parent: 1 - - uid: 739 - components: - - type: Transform - pos: 5.5,-20.5 - parent: 1 - - uid: 740 - components: - - type: Transform - pos: -5.5,-14.5 - parent: 1 - - uid: 741 - components: - - type: Transform - pos: 6.5,-17.5 - parent: 1 - - uid: 742 - components: - - type: Transform - pos: -3.5,-14.5 - parent: 1 - - uid: 743 - components: - - type: Transform - pos: -2.5,-18.5 - parent: 1 - - uid: 744 - components: - - type: Transform - pos: 2.5,-26.5 - parent: 1 - - uid: 745 - components: - - type: Transform - pos: 2.5,-28.5 - parent: 1 - - uid: 746 - components: - - type: Transform - pos: 6.5,-25.5 - parent: 1 - - uid: 747 - components: - - type: Transform - pos: 6.5,-22.5 - parent: 1 - - uid: 748 - components: - - type: Transform - pos: -7.5,-23.5 - parent: 1 - - uid: 749 - components: - - type: Transform - pos: -7.5,-26.5 - parent: 1 - - uid: 750 - components: - - type: Transform - pos: -7.5,-22.5 - parent: 1 - - uid: 751 - components: - - type: Transform - pos: -7.5,-20.5 - parent: 1 - - uid: 752 - components: - - type: Transform - pos: 4.5,-15.5 - parent: 1 - - uid: 753 - components: - - type: Transform - pos: 6.5,-28.5 - parent: 1 - - uid: 754 - components: - - type: Transform - pos: 6.5,-27.5 - parent: 1 - - uid: 755 - components: - - type: Transform - pos: 6.5,-23.5 - parent: 1 - - uid: 756 - components: - - type: Transform - pos: 6.5,-20.5 - parent: 1 - - uid: 757 - components: - - type: Transform - pos: -7.5,-25.5 - parent: 1 - - uid: 758 - components: - - type: Transform - pos: -7.5,-28.5 - parent: 1 - - uid: 759 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-15.5 - parent: 1 - - uid: 760 - components: - - type: Transform - pos: -5.5,-18.5 - parent: 1 - - uid: 761 - components: - - type: Transform - pos: -5.5,-3.5 - parent: 1 - - uid: 762 - components: - - type: Transform - pos: 5.5,-5.5 - parent: 1 - - uid: 763 - components: - - type: Transform - pos: -3.5,-2.5 - parent: 1 - - uid: 764 - components: - - type: Transform - pos: -6.5,-3.5 - parent: 1 - - uid: 765 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: -2.5,-7.5 - parent: 1 - - uid: 766 - components: - - type: Transform - pos: -3.5,-18.5 - parent: 1 - - uid: 767 - components: - - type: Transform - pos: -3.5,-1.5 - parent: 1 - - uid: 768 - components: - - type: Transform - pos: 2.5,-1.5 - parent: 1 - - uid: 769 - components: - - type: Transform - pos: -6.5,-14.5 - parent: 1 - - uid: 770 - components: - - type: Transform - pos: -5.5,-2.5 - parent: 1 - - uid: 771 - components: - - type: Transform - pos: 5.5,-6.5 - parent: 1 - - uid: 772 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 2.5,-7.5 - parent: 1 - - uid: 773 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 2.5,-8.5 - parent: 1 - - uid: 774 - components: - - type: Transform - pos: 4.5,-11.5 - parent: 1 - - uid: 775 - components: - - type: Transform - pos: -6.5,-13.5 - parent: 1 - - uid: 776 - components: - - type: Transform - pos: -3.5,-30.5 - parent: 1 - - uid: 777 - components: - - type: Transform - pos: 6.5,-15.5 - parent: 1 - - uid: 778 - components: - - type: Transform - pos: 5.5,-13.5 - parent: 1 - - uid: 779 - components: - - type: Transform - pos: -7.5,-15.5 - parent: 1 - - uid: 780 - components: - - type: Transform - pos: 1.5,-14.5 - parent: 1 - - uid: 781 - components: - - type: Transform - pos: -4.5,-18.5 - parent: 1 - - uid: 782 - components: - - type: Transform - pos: -2.5,-25.5 - parent: 1 - - uid: 783 - components: - - type: Transform - pos: 0.5,-25.5 - parent: 1 - - uid: 785 - components: - - type: Transform - pos: -1.5,-22.5 - parent: 1 - - uid: 786 - components: - - type: Transform - pos: 0.5,-22.5 - parent: 1 - - uid: 787 - components: - - type: Transform - pos: 1.5,-24.5 - parent: 1 - - uid: 788 - components: - - type: Transform - pos: 1.5,-23.5 - parent: 1 - - uid: 789 - components: - - type: Transform - pos: 1.5,-22.5 - parent: 1 - - uid: 790 - components: - - type: Transform - pos: -2.5,-24.5 - parent: 1 - - uid: 791 - components: - - type: Transform - pos: -2.5,-23.5 - parent: 1 - - uid: 792 - components: - - type: Transform - pos: -2.5,-22.5 - parent: 1 - - uid: 793 - components: - - type: Transform - pos: 5.5,-22.5 - parent: 1 - - uid: 794 - components: - - type: Transform - pos: -7.5,-14.5 - parent: 1 - - uid: 795 - components: - - type: Transform - pos: -7.5,-18.5 - parent: 1 - - uid: 796 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -4.5,-28.5 - parent: 1 - - uid: 797 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -5.5,-28.5 - parent: 1 - - uid: 798 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -6.5,-28.5 - parent: 1 - - uid: 799 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 3.5,-28.5 - parent: 1 - - uid: 800 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 4.5,-28.5 - parent: 1 - - uid: 801 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 5.5,-28.5 - parent: 1 - - uid: 802 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 2.5,-6.5 - parent: 1 - - uid: 803 - components: - - type: Transform - pos: 4.5,-27.5 - parent: 1 - - uid: 804 - components: - - type: Transform - pos: 4.5,-26.5 - parent: 1 - - uid: 805 - components: - - type: Transform - pos: 4.5,-25.5 - parent: 1 - - uid: 806 - components: - - type: Transform - pos: 2.5,-4.5 - parent: 1 - - uid: 807 - components: - - type: Transform - pos: 3.5,-6.5 - parent: 1 - - uid: 808 - components: - - type: Transform - pos: 2.5,-30.5 - parent: 1 - - uid: 809 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -5.5,-17.5 - parent: 1 - - uid: 810 - components: - - type: Transform - pos: 3.5,-29.5 - parent: 1 - - uid: 811 - components: - - type: Transform - pos: -5.5,-29.5 - parent: 1 - - uid: 812 - components: - - type: Transform - pos: 4.5,-29.5 - parent: 1 - - uid: 813 - components: - - type: Transform - pos: -4.5,-29.5 - parent: 1 - - uid: 814 - components: - - type: Transform - pos: 5.5,-29.5 - parent: 1 - - uid: 815 - components: - - type: Transform - pos: -3.5,-29.5 - parent: 1 - - uid: 816 - components: - - type: Transform - pos: 2.5,-29.5 - parent: 1 - - uid: 817 - components: - - type: Transform - pos: 6.5,-14.5 - parent: 1 - - uid: 818 - components: - - type: Transform - pos: 6.5,-18.5 - parent: 1 - - uid: 819 - components: - - type: Transform - pos: 5.5,-3.5 - parent: 1 - - uid: 820 - components: - - type: Transform - pos: -6.5,-29.5 - parent: 1 - - uid: 821 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -6.5,-30.5 - parent: 1 - - uid: 822 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 5.5,-30.5 - parent: 1 - - uid: 823 - components: - - type: Transform - pos: 8.5,-15.5 - parent: 1 - - uid: 824 - components: - - type: Transform - pos: 8.5,-17.5 - parent: 1 - - uid: 825 - components: - - type: Transform - pos: -9.5,-17.5 - parent: 1 - - uid: 826 - components: - - type: Transform - pos: -9.5,-15.5 - parent: 1 -- proto: WarningN2 - entities: - - uid: 827 - components: - - type: Transform - pos: 4.5,-25.5 - parent: 1 -- proto: WarningO2 - entities: - - uid: 828 - components: - - type: Transform - pos: 2.5,-25.5 - parent: 1 -- proto: WarningWaste - entities: - - uid: 829 - components: - - type: Transform - pos: 4.5,-18.5 - parent: 1 -- proto: WaterCooler - entities: - - uid: 830 - components: - - type: Transform - pos: 0.5,-17.5 - parent: 1 -- proto: WeaponTurretSyndicate - entities: - - uid: 24 - components: - - type: Transform - pos: -2.5,-6.5 - parent: 1 - - uid: 610 - components: - - type: Transform - pos: -1.5,-19.5 - parent: 1 -- proto: WindoorSecure - entities: - - uid: 831 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: -2.5,-5.5 - parent: 1 -- proto: Wrench - entities: - - uid: 832 - components: - - type: Transform - pos: 5.4749,-23.512577 - parent: 1 - - uid: 833 - components: - - type: Transform - pos: 5.63115,-23.481327 - parent: 1 - - uid: 834 - components: - - type: Transform - pos: 1.6061028,-13.284962 - parent: 1 -... +meta: + format: 6 + postmapinit: false +tilemap: + 0: Space + 3: FloorArcadeRed + 31: FloorDark + 36: FloorDarkMono + 40: FloorDarkPlastic + 56: FloorGreenCircuit + 79: FloorReinforced + 88: FloorShuttleRed + 89: FloorShuttleWhite + 93: FloorSteel + 104: FloorSteelMono + 110: FloorTechMaint3 + 122: FloorWood + 125: Lattice + 126: Plating +entities: +- proto: "" + entities: + - uid: 1 + components: + - type: MetaData + name: GX-13 Infiltrator + - type: Transform + pos: 0.64252126,4.1776605 + parent: invalid + - type: Tag + tags: + - Syndicate + - type: MapGrid + chunks: + -1,-1: + ind: -1,-1 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADHwAAAAABHwAAAAACHwAAAAABHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAHwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAHwAAAAACfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAHwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACHwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfgAAAAAAHwAAAAABHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAHwAAAAACHwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAACWAAAAAAAHwAAAAACHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAXQAAAAACaAAAAAAAWQAAAAAAHwAAAAADHwAAAAAAHwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAXQAAAAADWQAAAAAAaAAAAAABWAAAAAAAHwAAAAAAHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAXQAAAAAAXQAAAAADWAAAAAAAHwAAAAACHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAWAAAAAAAWAAAAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + 0,-1: + ind: 0,-1 + tiles: HwAAAAABHwAAAAABHwAAAAABHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABWAAAAAAAegAAAAADfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADHwAAAAAAHwAAAAAAegAAAAADegAAAAABfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAWAAAAAAAegAAAAAAAwAAAAAAegAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADWAAAAAAAfgAAAAAAAwAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAAAAAAWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + -1,-2: + ind: -1,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAWAAAAAAAWAAAAAAAWAAAAAAAfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAOAAAAAAAJAAAAAAAOAAAAAAAfgAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAOAAAAAAAJAAAAAAAOAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAOAAAAAAAJAAAAAAAOAAAAAAAWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAHwAAAAACHwAAAAAAHwAAAAADWAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAHwAAAAACTwAAAAAAHwAAAAABWAAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAWAAAAAAAfgAAAAAAHwAAAAAAHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAKAAAAAAAKAAAAAAAWAAAAAAAHwAAAAADHwAAAAAAHwAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAWAAAAAAAHwAAAAAAWAAAAAAAfgAAAAAAHwAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACHwAAAAABHwAAAAAAHwAAAAADHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbgAAAAAAHwAAAAAAHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAA + version: 6 + 0,-2: + ind: 0,-2 + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfQAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfgAAAAAATwAAAAAAfgAAAAAATwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfQAAAAAAfQAAAAAAfgAAAAAATwAAAAAAfgAAAAAATwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAATwAAAAAAfgAAAAAATwAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABHwAAAAADHwAAAAABHwAAAAADHwAAAAABWAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAADHwAAAAACHwAAAAAAHwAAAAACHwAAAAADfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAHwAAAAACHwAAAAAAHwAAAAADHwAAAAABfgAAAAAAfQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAABHwAAAAAAHwAAAAABHwAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAfgAAAAAAHwAAAAAAHwAAAAACbgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + version: 6 + - type: Broadphase + - type: Physics + bodyStatus: InAir + angularDamping: 0.05 + linearDamping: 0.05 + fixedRotation: False + bodyType: Dynamic + - type: Fixtures + fixtures: {} + - type: Gravity + gravityShakeSound: !type:SoundPathSpecifier + path: /Audio/Effects/alert.ogg + - type: GridAtmosphere + version: 2 + data: + tiles: + -1,-4: + 0: 65535 + -1,-3: + 0: 65535 + -1,-2: + 0: 65535 + -1,-1: + 0: 61439 + 0,-4: + 0: 65535 + 0,-3: + 0: 65535 + 0,-2: + 0: 65535 + 0,-1: + 0: 65535 + 1,-4: + 0: 30591 + 1,-3: + 0: 21879 + 1: 512 + 1,-2: + 0: 30325 + 2: 256 + 1,-1: + 0: 55 + 0,-5: + 0: 65535 + 1,-5: + 0: 65399 + -1,-5: + 0: 65535 + -3,-4: + 0: 12 + -2,-4: + 0: 61439 + -2,-2: + 0: 65516 + -2,-1: + 0: 2287 + -2,-3: + 0: 35054 + 1: 1536 + -1,0: + 0: 8 + -3,-5: + 0: 52224 + -2,-8: + 0: 65504 + -2,-7: + 0: 65535 + -2,-6: + 0: 65535 + -2,-5: + 0: 65535 + -1,-8: + 0: 65526 + -1,-7: + 0: 65535 + -1,-6: + 0: 65535 + 0,-8: + 0: 65523 + 0,-7: + 0: 61303 + 3: 4096 + 4: 136 + 0,-6: + 3: 1 + 0: 65534 + 1,-8: + 0: 30512 + 1,-7: + 0: 30549 + 5: 34 + 1,-6: + 0: 30583 + -1,-9: + 0: 26112 + 0,-9: + 0: 13056 + 2,-4: + 0: 1 + 2,-5: + 0: 4352 + uniqueMixes: + - volume: 2500 + temperature: 293.15 + moles: + - 21.824879 + - 82.10312 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.15 + moles: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.14996 + moles: + - 20.078888 + - 75.53487 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.15 + moles: + - 20.619795 + - 77.56971 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.15 + moles: + - 6666.982 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - volume: 2500 + temperature: 293.15 + moles: + - 0 + - 6666.982 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + chunkSize: 4 + - type: DecalGrid + chunkCollection: + version: 2 + nodes: + - node: + color: '#FFFFFFFF' + id: Arrows + decals: + 115: -6,-24 + - node: + color: '#FFFFFFFF' + id: Bot + decals: + 112: -6,-27 + 113: -6,-26 + 114: -6,-25 + - node: + color: '#79150096' + id: Box + decals: + 110: -6,-21 + 111: -5,-21 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerNe + decals: + 10: 0,-4 + 43: 0,-9 + 51: 4,-20 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerNw + decals: + 11: -2,-4 + 42: -2,-9 + 45: -2,-20 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerSe + decals: + 36: 0,-14 + 50: 4,-22 + 76: 3,-18 + - node: + color: '#DE3A3A96' + id: BrickTileSteelCornerSw + decals: + 37: -2,-14 + 46: -2,-22 + 75: -5,-18 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineE + decals: + 6: 0,-7 + 9: 0,-5 + 32: 0,-11 + 33: 0,-10 + 34: 0,-12 + 35: 0,-13 + 52: 4,-21 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineN + decals: + 12: -1,-4 + 53: 3,-20 + 54: 2,-20 + 55: 1,-20 + 56: 0,-20 + 63: 0,-16 + 64: 1,-16 + 65: 2,-16 + 66: -2,-16 + 67: -3,-16 + 68: -4,-16 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineS + decals: + 47: 0,-22 + 48: 1,-22 + 49: 2,-22 + 69: 2,-18 + 70: 1,-18 + 71: 0,-18 + 72: -2,-18 + 73: -3,-18 + 74: -4,-18 + - node: + color: '#DE3A3A96' + id: BrickTileSteelLineW + decals: + 7: -2,-7 + 8: -2,-5 + 38: -2,-13 + 39: -2,-12 + 40: -2,-11 + 41: -2,-10 + - node: + color: '#79150096' + id: BrickTileWhiteCornerNe + decals: + 104: -5,-22 + - node: + color: '#79150096' + id: BrickTileWhiteCornerNw + decals: + 103: -7,-22 + - node: + color: '#79150096' + id: BrickTileWhiteCornerSe + decals: + 105: -5,-24 + - node: + color: '#79150096' + id: BrickTileWhiteCornerSw + decals: + 106: -7,-24 + - node: + color: '#79150096' + id: BrickTileWhiteLineE + decals: + 102: -5,-23 + - node: + color: '#79150096' + id: BrickTileWhiteLineN + decals: + 109: -6,-22 + - node: + color: '#79150096' + id: BrickTileWhiteLineS + decals: + 107: -6,-24 + - node: + color: '#79150096' + id: BrickTileWhiteLineW + decals: + 108: -7,-23 + - node: + color: '#79150096' + id: Delivery + decals: + 116: -1,-23 + - node: + color: '#DE3A3A96' + id: DeliveryGreyscale + decals: + 13: 1,-6 + 31: -3,-6 + 83: -1,-19 + 84: -3,-21 + 85: -1,-15 + 86: 4,-17 + 87: -6,-17 + 88: -5,-15 + 89: 3,-15 + 90: -1,-8 + - node: + color: '#DE3A3A96' + id: HalfTileOverlayGreyscale + decals: + 29: -4,-7 + - node: + color: '#DE3A3A96' + id: HalfTileOverlayGreyscale180 + decals: + 25: -4,-4 + 26: -5,-4 + - node: + color: '#DE3A3A96' + id: HalfTileOverlayGreyscale90 + decals: + 27: -6,-6 + 28: -6,-5 + - node: + color: '#DE3A3A96' + id: MonoOverlay + decals: + 23: -5,-6 + 24: -4,-5 + - node: + color: '#DE3A3A96' + id: WarnCornerGreyscaleNE + decals: + 81: 3,-16 + - node: + color: '#DE3A3A96' + id: WarnCornerGreyscaleNW + decals: + 80: -5,-16 + - node: + color: '#FFFFFFFF' + id: WarnCornerSmallNE + decals: + 99: -7,-24 + - node: + color: '#FFFFFFFF' + id: WarnCornerSmallNW + decals: + 98: -5,-24 + - node: + color: '#FFFFFFFF' + id: WarnCornerSmallSE + decals: + 97: -7,-22 + - node: + color: '#FFFFFFFF' + id: WarnCornerSmallSW + decals: + 101: -5,-22 + - node: + color: '#FFFFFFFF' + id: WarnLineE + decals: + 3: -4,-23 + 4: -4,-24 + 5: -4,-25 + 95: -7,-23 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleE + decals: + 78: 3,-17 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleN + decals: + 44: -1,-9 + 59: -1,-20 + 82: -1,-16 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleS + decals: + 57: -1,-22 + 60: 3,-22 + 77: -1,-18 + - node: + color: '#DE3A3A96' + id: WarnLineGreyscaleW + decals: + 58: -2,-21 + 79: -5,-17 + - node: + color: '#FFFFFFFF' + id: WarnLineN + decals: + 91: -5,-28 + 92: -6,-28 + 93: -7,-28 + 100: -6,-22 + - node: + color: '#FFFFFFFF' + id: WarnLineS + decals: + 94: -5,-23 + - node: + color: '#FFFFFFFF' + id: WarnLineW + decals: + 96: -6,-24 + - node: + angle: 1.5707963267948966 rad + color: '#FFFFFFFF' + id: WarningLine + decals: + 0: -4,-22 + 1: -4,-21 + 2: -4,-20 + - node: + color: '#FFFFFFFF' + id: WoodTrimThinEndN + decals: + 62: 3,-4 + - node: + color: '#FFFFFFFF' + id: WoodTrimThinEndS + decals: + 61: 3,-5 + - node: + color: '#FFFFFFFF' + id: syndlogo10 + decals: + 20: -2,-7 + - node: + color: '#FFFFFFFF' + id: syndlogo11 + decals: + 21: -1,-7 + - node: + color: '#FFFFFFFF' + id: syndlogo12 + decals: + 22: 0,-7 + - node: + color: '#FFFFFFFF' + id: syndlogo2 + decals: + 14: -2,-5 + - node: + color: '#FFFFFFFF' + id: syndlogo3 + decals: + 15: -1,-5 + - node: + color: '#FFFFFFFF' + id: syndlogo4 + decals: + 16: 0,-5 + - node: + color: '#FFFFFFFF' + id: syndlogo5 + decals: + 30: -3,-6 + - node: + color: '#FFFFFFFF' + id: syndlogo6 + decals: + 17: -2,-6 + - node: + color: '#FFFFFFFF' + id: syndlogo7 + decals: + 18: -1,-6 + - node: + color: '#FFFFFFFF' + id: syndlogo8 + decals: + 19: 0,-6 + - type: IFF + color: '#FFC000FF' + flags: Hide + - type: OccluderTree + - type: Shuttle + - type: RadiationGridResistance + - type: GravityShake + shakeTimes: 10 + - type: GasTileOverlay + - type: SpreaderGrid + - type: GridPathfinding +- proto: AirlockExternalGlassShuttleSyndicateLocked + entities: + - uid: 8 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 8.5,-16.5 + parent: 1 + - type: DeviceLinkSink + invokeCounter: 1 + links: + - 13 + - type: DeviceLinkSource + linkedPorts: + 13: + - DoorStatus: Close + - uid: 10 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -9.5,-16.5 + parent: 1 + - type: DeviceLinkSink + invokeCounter: 1 + links: + - 3 + - type: DeviceLinkSource + linkedPorts: + 3: + - DoorStatus: Close +- proto: AirlockExternalSyndicateLocked + entities: + - uid: 2 + components: + - type: Transform + pos: -0.5,-25.5 + parent: 1 + - type: DeviceLinkSink + links: + - 14 + - type: DeviceLinkSource + linkedPorts: + 14: + - DoorStatus: DoorBolt + - uid: 3 + components: + - type: Transform + pos: -5.5,-16.5 + parent: 1 + - type: DeviceLinkSink + invokeCounter: 1 + links: + - 10 + - type: DeviceLinkSource + linkedPorts: + 10: + - DoorStatus: Close + - uid: 7 + components: + - type: Transform + pos: -4.5,-14.5 + parent: 1 + - type: DeviceLinkSink + links: + - 12 + - type: DeviceLinkSource + linkedPorts: + 12: + - DoorStatus: DoorBolt + - uid: 9 + components: + - type: Transform + pos: 3.5,-14.5 + parent: 1 + - type: DeviceLinkSink + links: + - 22 + - type: DeviceLinkSource + linkedPorts: + 22: + - DoorStatus: DoorBolt + - uid: 12 + components: + - type: Transform + pos: -4.5,-10.5 + parent: 1 + - type: DeviceLinkSink + links: + - 7 + - type: DeviceLinkSource + linkedPorts: + 7: + - DoorStatus: DoorBolt + - uid: 13 + components: + - type: Transform + pos: 4.5,-16.5 + parent: 1 + - type: DeviceLinkSink + invokeCounter: 1 + links: + - 8 + - type: DeviceLinkSource + linkedPorts: + 8: + - DoorStatus: Close + - uid: 14 + components: + - type: Transform + pos: -0.5,-22.5 + parent: 1 + - type: DeviceLinkSink + links: + - 2 + - type: DeviceLinkSource + linkedPorts: + 2: + - DoorStatus: DoorBolt + - uid: 22 + components: + - type: Transform + pos: 3.5,-10.5 + parent: 1 + - type: DeviceLinkSink + links: + - 9 + - type: DeviceLinkSource + linkedPorts: + 9: + - DoorStatus: DoorBolt +- proto: AirlockSyndicateGlassLocked + entities: + - uid: 4 + components: + - type: Transform + pos: -0.5,-7.5 + parent: 1 + - uid: 5 + components: + - type: Transform + pos: 3.5,-22.5 + parent: 1 + - uid: 6 + components: + - type: Transform + pos: -0.5,-18.5 + parent: 1 + - uid: 17 + components: + - type: Transform + pos: -2.5,-20.5 + parent: 1 +- proto: AirlockSyndicateLocked + entities: + - uid: 15 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 1 + - uid: 16 + components: + - type: Transform + pos: 2.5,-5.5 + parent: 1 +- proto: APCBasic + entities: + - uid: 18 + components: + - type: Transform + pos: -3.5,-18.5 + parent: 1 + - uid: 19 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-8.5 + parent: 1 +- proto: AtmosFixNitrogenMarker + entities: + - uid: 25 + components: + - type: Transform + pos: 5.5,-26.5 + parent: 1 + - uid: 26 + components: + - type: Transform + pos: 5.5,-27.5 + parent: 1 +- proto: AtmosFixOxygenMarker + entities: + - uid: 27 + components: + - type: Transform + pos: 3.5,-26.5 + parent: 1 + - uid: 28 + components: + - type: Transform + pos: 3.5,-27.5 + parent: 1 +- proto: BannerSyndicate + entities: + - uid: 29 + components: + - type: Transform + pos: 2.5,-15.5 + parent: 1 + - uid: 30 + components: + - type: Transform + pos: -3.5,-15.5 + parent: 1 +- proto: Bed + entities: + - uid: 31 + components: + - type: Transform + pos: 3.5,-3.5 + parent: 1 +- proto: BedsheetSyndie + entities: + - uid: 32 + components: + - type: Transform + pos: 3.5,-3.5 + parent: 1 +- proto: BoxEncryptionKeySyndie + entities: + - uid: 34 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: BoxFlashbang + entities: + - uid: 42 + components: + - type: Transform + pos: 0.49331844,-13.366474 + parent: 1 +- proto: BoxHandcuff + entities: + - uid: 43 + components: + - type: Transform + pos: 1.4510483,-2.399527 + parent: 1 +- proto: Brutepack + entities: + - uid: 44 + components: + - type: Transform + pos: -3.292087,-4.1600046 + parent: 1 + - uid: 45 + components: + - type: Transform + pos: -3.354587,-4.4256296 + parent: 1 +- proto: C4 + entities: + - uid: 46 + components: + - type: Transform + pos: 1.7857682,-12.631323 + parent: 1 + - uid: 47 + components: + - type: Transform + pos: 1.5045182,-12.646948 + parent: 1 + - uid: 48 + components: + - type: Transform + pos: 1.5982682,-12.646948 + parent: 1 + - uid: 49 + components: + - type: Transform + pos: 1.4107682,-12.646948 + parent: 1 + - uid: 50 + components: + - type: Transform + pos: 1.6920182,-12.631323 + parent: 1 +- proto: CableApcExtension + entities: + - uid: 51 + components: + - type: Transform + pos: -3.5,-18.5 + parent: 1 + - uid: 52 + components: + - type: Transform + pos: -3.5,-17.5 + parent: 1 + - uid: 53 + components: + - type: Transform + pos: -3.5,-16.5 + parent: 1 + - uid: 54 + components: + - type: Transform + pos: -4.5,-16.5 + parent: 1 + - uid: 55 + components: + - type: Transform + pos: -5.5,-16.5 + parent: 1 + - uid: 56 + components: + - type: Transform + pos: -6.5,-16.5 + parent: 1 + - uid: 57 + components: + - type: Transform + pos: -7.5,-16.5 + parent: 1 + - uid: 58 + components: + - type: Transform + pos: -8.5,-16.5 + parent: 1 + - uid: 59 + components: + - type: Transform + pos: -6.5,-15.5 + parent: 1 + - uid: 60 + components: + - type: Transform + pos: -6.5,-17.5 + parent: 1 + - uid: 61 + components: + - type: Transform + pos: -4.5,-15.5 + parent: 1 + - uid: 62 + components: + - type: Transform + pos: -4.5,-14.5 + parent: 1 + - uid: 63 + components: + - type: Transform + pos: -4.5,-13.5 + parent: 1 + - uid: 64 + components: + - type: Transform + pos: -4.5,-12.5 + parent: 1 + - uid: 65 + components: + - type: Transform + pos: -4.5,-11.5 + parent: 1 + - uid: 66 + components: + - type: Transform + pos: -4.5,-10.5 + parent: 1 + - uid: 67 + components: + - type: Transform + pos: -5.5,-12.5 + parent: 1 + - uid: 68 + components: + - type: Transform + pos: -6.5,-12.5 + parent: 1 + - uid: 69 + components: + - type: Transform + pos: -2.5,-16.5 + parent: 1 + - uid: 70 + components: + - type: Transform + pos: -1.5,-16.5 + parent: 1 + - uid: 71 + components: + - type: Transform + pos: -0.5,-16.5 + parent: 1 + - uid: 72 + components: + - type: Transform + pos: 0.5,-16.5 + parent: 1 + - uid: 73 + components: + - type: Transform + pos: 1.5,-16.5 + parent: 1 + - uid: 74 + components: + - type: Transform + pos: 2.5,-16.5 + parent: 1 + - uid: 75 + components: + - type: Transform + pos: 3.5,-16.5 + parent: 1 + - uid: 76 + components: + - type: Transform + pos: 4.5,-16.5 + parent: 1 + - uid: 77 + components: + - type: Transform + pos: 5.5,-16.5 + parent: 1 + - uid: 78 + components: + - type: Transform + pos: 5.5,-15.5 + parent: 1 + - uid: 79 + components: + - type: Transform + pos: 5.5,-17.5 + parent: 1 + - uid: 80 + components: + - type: Transform + pos: 6.5,-16.5 + parent: 1 + - uid: 81 + components: + - type: Transform + pos: 7.5,-16.5 + parent: 1 + - uid: 82 + components: + - type: Transform + pos: 3.5,-15.5 + parent: 1 + - uid: 83 + components: + - type: Transform + pos: 3.5,-14.5 + parent: 1 + - uid: 84 + components: + - type: Transform + pos: 3.5,-13.5 + parent: 1 + - uid: 85 + components: + - type: Transform + pos: 3.5,-12.5 + parent: 1 + - uid: 86 + components: + - type: Transform + pos: 3.5,-11.5 + parent: 1 + - uid: 87 + components: + - type: Transform + pos: 3.5,-11.5 + parent: 1 + - uid: 88 + components: + - type: Transform + pos: 3.5,-10.5 + parent: 1 + - uid: 89 + components: + - type: Transform + pos: 4.5,-12.5 + parent: 1 + - uid: 90 + components: + - type: Transform + pos: 5.5,-12.5 + parent: 1 + - uid: 91 + components: + - type: Transform + pos: -0.5,-6.5 + parent: 1 + - uid: 92 + components: + - type: Transform + pos: -0.5,-7.5 + parent: 1 + - uid: 93 + components: + - type: Transform + pos: -0.5,-8.5 + parent: 1 + - uid: 94 + components: + - type: Transform + pos: -0.5,-9.5 + parent: 1 + - uid: 95 + components: + - type: Transform + pos: -0.5,-10.5 + parent: 1 + - uid: 96 + components: + - type: Transform + pos: -0.5,-11.5 + parent: 1 + - uid: 97 + components: + - type: Transform + pos: -0.5,-12.5 + parent: 1 + - uid: 98 + components: + - type: Transform + pos: -0.5,-13.5 + parent: 1 + - uid: 99 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 1 + - uid: 100 + components: + - type: Transform + pos: -1.5,-12.5 + parent: 1 + - uid: 101 + components: + - type: Transform + pos: 0.5,-12.5 + parent: 1 + - uid: 102 + components: + - type: Transform + pos: -0.5,-5.5 + parent: 1 + - uid: 103 + components: + - type: Transform + pos: 0.5,-5.5 + parent: 1 + - uid: 104 + components: + - type: Transform + pos: 1.5,-5.5 + parent: 1 + - uid: 105 + components: + - type: Transform + pos: 2.5,-5.5 + parent: 1 + - uid: 106 + components: + - type: Transform + pos: 3.5,-5.5 + parent: 1 + - uid: 107 + components: + - type: Transform + pos: 3.5,-4.5 + parent: 1 + - uid: 108 + components: + - type: Transform + pos: -0.5,-4.5 + parent: 1 + - uid: 109 + components: + - type: Transform + pos: -0.5,-3.5 + parent: 1 + - uid: 110 + components: + - type: Transform + pos: -0.5,-2.5 + parent: 1 + - uid: 111 + components: + - type: Transform + pos: 0.5,-2.5 + parent: 1 + - uid: 112 + components: + - type: Transform + pos: 1.5,-2.5 + parent: 1 + - uid: 113 + components: + - type: Transform + pos: -1.5,-2.5 + parent: 1 + - uid: 114 + components: + - type: Transform + pos: -2.5,-2.5 + parent: 1 + - uid: 115 + components: + - type: Transform + pos: -1.5,-4.5 + parent: 1 + - uid: 116 + components: + - type: Transform + pos: -3.5,-4.5 + parent: 1 + - uid: 117 + components: + - type: Transform + pos: -2.5,-4.5 + parent: 1 + - uid: 118 + components: + - type: Transform + pos: -4.5,-4.5 + parent: 1 + - uid: 119 + components: + - type: Transform + pos: -5.5,-4.5 + parent: 1 + - uid: 120 + components: + - type: Transform + pos: -5.5,-5.5 + parent: 1 + - uid: 121 + components: + - type: Transform + pos: -5.5,-6.5 + parent: 1 + - uid: 122 + components: + - type: Transform + pos: -5.5,-3.5 + parent: 1 + - uid: 123 + components: + - type: Transform + pos: 4.5,-4.5 + parent: 1 + - uid: 124 + components: + - type: Transform + pos: 4.5,-3.5 + parent: 1 + - uid: 125 + components: + - type: Transform + pos: 3.5,-6.5 + parent: 1 + - uid: 126 + components: + - type: Transform + pos: -0.5,-17.5 + parent: 1 + - uid: 127 + components: + - type: Transform + pos: -3.5,-20.5 + parent: 1 + - uid: 128 + components: + - type: Transform + pos: -3.5,-19.5 + parent: 1 + - uid: 129 + components: + - type: Transform + pos: -3.5,-21.5 + parent: 1 + - uid: 130 + components: + - type: Transform + pos: -4.5,-21.5 + parent: 1 + - uid: 131 + components: + - type: Transform + pos: -4.5,-22.5 + parent: 1 + - uid: 132 + components: + - type: Transform + pos: -4.5,-23.5 + parent: 1 + - uid: 133 + components: + - type: Transform + pos: -4.5,-24.5 + parent: 1 + - uid: 134 + components: + - type: Transform + pos: -4.5,-25.5 + parent: 1 + - uid: 135 + components: + - type: Transform + pos: -4.5,-25.5 + parent: 1 + - uid: 136 + components: + - type: Transform + pos: -4.5,-26.5 + parent: 1 + - uid: 137 + components: + - type: Transform + pos: -4.5,-27.5 + parent: 1 + - uid: 138 + components: + - type: Transform + pos: -5.5,-27.5 + parent: 1 + - uid: 139 + components: + - type: Transform + pos: -6.5,-27.5 + parent: 1 + - uid: 140 + components: + - type: Transform + pos: -6.5,-26.5 + parent: 1 + - uid: 141 + components: + - type: Transform + pos: -6.5,-25.5 + parent: 1 + - uid: 142 + components: + - type: Transform + pos: -6.5,-24.5 + parent: 1 + - uid: 143 + components: + - type: Transform + pos: -6.5,-23.5 + parent: 1 + - uid: 144 + components: + - type: Transform + pos: -6.5,-22.5 + parent: 1 + - uid: 145 + components: + - type: Transform + pos: -2.5,-20.5 + parent: 1 + - uid: 146 + components: + - type: Transform + pos: -1.5,-20.5 + parent: 1 + - uid: 147 + components: + - type: Transform + pos: -0.5,-20.5 + parent: 1 + - uid: 148 + components: + - type: Transform + pos: -0.5,-21.5 + parent: 1 + - uid: 149 + components: + - type: Transform + pos: -0.5,-23.5 + parent: 1 + - uid: 150 + components: + - type: Transform + pos: -0.5,-22.5 + parent: 1 + - uid: 151 + components: + - type: Transform + pos: -0.5,-24.5 + parent: 1 + - uid: 152 + components: + - type: Transform + pos: -0.5,-25.5 + parent: 1 + - uid: 153 + components: + - type: Transform + pos: -0.5,-19.5 + parent: 1 + - uid: 154 + components: + - type: Transform + pos: 0.5,-20.5 + parent: 1 + - uid: 155 + components: + - type: Transform + pos: 1.5,-20.5 + parent: 1 + - uid: 156 + components: + - type: Transform + pos: 2.5,-20.5 + parent: 1 + - uid: 157 + components: + - type: Transform + pos: 3.5,-20.5 + parent: 1 + - uid: 158 + components: + - type: Transform + pos: 4.5,-20.5 + parent: 1 + - uid: 159 + components: + - type: Transform + pos: 3.5,-21.5 + parent: 1 + - uid: 160 + components: + - type: Transform + pos: 3.5,-22.5 + parent: 1 + - uid: 161 + components: + - type: Transform + pos: 3.5,-23.5 + parent: 1 + - uid: 162 + components: + - type: Transform + pos: 3.5,-24.5 + parent: 1 + - uid: 163 + components: + - type: Transform + pos: 3.5,-25.5 + parent: 1 + - uid: 164 + components: + - type: Transform + pos: 3.5,-26.5 + parent: 1 + - uid: 165 + components: + - type: Transform + pos: 3.5,-27.5 + parent: 1 + - uid: 166 + components: + - type: Transform + pos: 4.5,-27.5 + parent: 1 + - uid: 167 + components: + - type: Transform + pos: 5.5,-27.5 + parent: 1 + - uid: 168 + components: + - type: Transform + pos: 5.5,-26.5 + parent: 1 + - uid: 169 + components: + - type: Transform + pos: 5.5,-25.5 + parent: 1 + - uid: 170 + components: + - type: Transform + pos: 5.5,-24.5 + parent: 1 + - uid: 171 + components: + - type: Transform + pos: 5.5,-23.5 + parent: 1 + - uid: 172 + components: + - type: Transform + pos: 0.5,-8.5 + parent: 1 + - uid: 173 + components: + - type: Transform + pos: 1.5,-26.5 + parent: 1 + - uid: 174 + components: + - type: Transform + pos: 1.5,-8.5 + parent: 1 + - uid: 175 + components: + - type: Transform + pos: -0.5,-26.5 + parent: 1 + - uid: 176 + components: + - type: Transform + pos: -1.5,-26.5 + parent: 1 + - uid: 177 + components: + - type: Transform + pos: 0.5,-26.5 + parent: 1 + - uid: 178 + components: + - type: Transform + pos: -2.5,-26.5 + parent: 1 + - uid: 179 + components: + - type: Transform + pos: -2.5,-27.5 + parent: 1 + - uid: 180 + components: + - type: Transform + pos: 1.5,-27.5 + parent: 1 + - uid: 181 + components: + - type: Transform + pos: -6.5,-28.5 + parent: 1 + - uid: 182 + components: + - type: Transform + pos: -6.5,-29.5 + parent: 1 + - uid: 183 + components: + - type: Transform + pos: -2.5,-28.5 + parent: 1 + - uid: 184 + components: + - type: Transform + pos: -2.5,-29.5 + parent: 1 + - uid: 185 + components: + - type: Transform + pos: -2.5,-30.5 + parent: 1 + - uid: 186 + components: + - type: Transform + pos: -3.5,-30.5 + parent: 1 + - uid: 187 + components: + - type: Transform + pos: -4.5,-30.5 + parent: 1 + - uid: 188 + components: + - type: Transform + pos: -5.5,-30.5 + parent: 1 + - uid: 189 + components: + - type: Transform + pos: 1.5,-28.5 + parent: 1 + - uid: 190 + components: + - type: Transform + pos: 1.5,-29.5 + parent: 1 + - uid: 191 + components: + - type: Transform + pos: 1.5,-30.5 + parent: 1 + - uid: 192 + components: + - type: Transform + pos: 2.5,-30.5 + parent: 1 + - uid: 193 + components: + - type: Transform + pos: 3.5,-30.5 + parent: 1 + - uid: 194 + components: + - type: Transform + pos: 4.5,-30.5 + parent: 1 + - uid: 195 + components: + - type: Transform + pos: 5.5,-28.5 + parent: 1 + - uid: 196 + components: + - type: Transform + pos: 5.5,-29.5 + parent: 1 +- proto: CableHV + entities: + - uid: 197 + components: + - type: Transform + pos: -2.5,-21.5 + parent: 1 + - uid: 198 + components: + - type: Transform + pos: -4.5,-26.5 + parent: 1 + - uid: 199 + components: + - type: Transform + pos: -4.5,-27.5 + parent: 1 + - uid: 200 + components: + - type: Transform + pos: -5.5,-27.5 + parent: 1 + - uid: 201 + components: + - type: Transform + pos: -6.5,-27.5 + parent: 1 + - uid: 202 + components: + - type: Transform + pos: -5.5,-23.5 + parent: 1 + - uid: 203 + components: + - type: Transform + pos: -5.5,-22.5 + parent: 1 + - uid: 204 + components: + - type: Transform + pos: -5.5,-20.5 + parent: 1 + - uid: 205 + components: + - type: Transform + pos: -4.5,-20.5 + parent: 1 + - uid: 206 + components: + - type: Transform + pos: -5.5,-19.5 + parent: 1 + - uid: 207 + components: + - type: Transform + pos: -4.5,-19.5 + parent: 1 + - uid: 208 + components: + - type: Transform + pos: -3.5,-19.5 + parent: 1 + - uid: 209 + components: + - type: Transform + pos: -5.5,-21.5 + parent: 1 + - uid: 210 + components: + - type: Transform + pos: -6.5,-26.5 + parent: 1 + - uid: 211 + components: + - type: Transform + pos: -6.5,-25.5 + parent: 1 + - uid: 212 + components: + - type: Transform + pos: -6.5,-24.5 + parent: 1 + - uid: 213 + components: + - type: Transform + pos: -4.5,-23.5 + parent: 1 + - uid: 214 + components: + - type: Transform + pos: -2.5,-20.5 + parent: 1 + - uid: 215 + components: + - type: Transform + pos: -2.5,-19.5 + parent: 1 + - uid: 216 + components: + - type: Transform + pos: -4.5,-25.5 + parent: 1 + - uid: 217 + components: + - type: Transform + pos: -6.5,-23.5 + parent: 1 + - uid: 218 + components: + - type: Transform + pos: -6.5,-20.5 + parent: 1 + - uid: 219 + components: + - type: Transform + pos: -4.5,-24.5 + parent: 1 + - uid: 220 + components: + - type: Transform + pos: -5.5,-18.5 + parent: 1 + - uid: 221 + components: + - type: Transform + pos: -4.5,-18.5 + parent: 1 + - uid: 222 + components: + - type: Transform + pos: -3.5,-18.5 + parent: 1 + - uid: 223 + components: + - type: Transform + pos: -3.5,-21.5 + parent: 1 + - uid: 224 + components: + - type: Transform + pos: -4.5,-21.5 + parent: 1 +- proto: CableMV + entities: + - uid: 225 + components: + - type: Transform + pos: -3.5,-19.5 + parent: 1 + - uid: 226 + components: + - type: Transform + pos: -3.5,-18.5 + parent: 1 + - uid: 227 + components: + - type: Transform + pos: -3.5,-17.5 + parent: 1 + - uid: 228 + components: + - type: Transform + pos: -3.5,-16.5 + parent: 1 + - uid: 229 + components: + - type: Transform + pos: -2.5,-16.5 + parent: 1 + - uid: 230 + components: + - type: Transform + pos: -0.5,-16.5 + parent: 1 + - uid: 231 + components: + - type: Transform + pos: -1.5,-16.5 + parent: 1 + - uid: 232 + components: + - type: Transform + pos: -0.5,-15.5 + parent: 1 + - uid: 233 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 1 + - uid: 234 + components: + - type: Transform + pos: -0.5,-13.5 + parent: 1 + - uid: 235 + components: + - type: Transform + pos: -0.5,-12.5 + parent: 1 + - uid: 236 + components: + - type: Transform + pos: -0.5,-11.5 + parent: 1 + - uid: 237 + components: + - type: Transform + pos: -0.5,-10.5 + parent: 1 + - uid: 238 + components: + - type: Transform + pos: -0.5,-9.5 + parent: 1 + - uid: 239 + components: + - type: Transform + pos: -0.5,-8.5 + parent: 1 + - uid: 240 + components: + - type: Transform + pos: -3.5,-20.5 + parent: 1 + - uid: 241 + components: + - type: Transform + pos: -3.5,-21.5 + parent: 1 + - uid: 242 + components: + - type: Transform + pos: -4.5,-21.5 + parent: 1 + - uid: 243 + components: + - type: Transform + pos: -4.5,-22.5 + parent: 1 + - uid: 244 + components: + - type: Transform + pos: -4.5,-23.5 + parent: 1 + - uid: 245 + components: + - type: Transform + pos: -4.5,-24.5 + parent: 1 + - uid: 246 + components: + - type: Transform + pos: -4.5,-25.5 + parent: 1 + - uid: 247 + components: + - type: Transform + pos: -4.5,-26.5 + parent: 1 + - uid: 248 + components: + - type: Transform + pos: -4.5,-27.5 + parent: 1 + - uid: 249 + components: + - type: Transform + pos: 1.5,-8.5 + parent: 1 + - uid: 250 + components: + - type: Transform + pos: 0.5,-8.5 + parent: 1 +- proto: CableTerminal + entities: + - uid: 251 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-20.5 + parent: 1 + - type: Physics + canCollide: False + - type: Fixtures + fixtures: {} + - uid: 252 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -4.5,-20.5 + parent: 1 + - type: Physics + canCollide: False + - type: Fixtures + fixtures: {} +- proto: Carpet + entities: + - uid: 253 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-5.5 + parent: 1 + - uid: 254 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-4.5 + parent: 1 +- proto: Catwalk + entities: + - uid: 255 + components: + - type: Transform + pos: -2.5,-26.5 + parent: 1 + - uid: 256 + components: + - type: Transform + pos: -2.5,-28.5 + parent: 1 + - uid: 257 + components: + - type: Transform + pos: -8.5,-16.5 + parent: 1 + - uid: 258 + components: + - type: Transform + pos: -1.5,-16.5 + parent: 1 + - uid: 259 + components: + - type: Transform + pos: -2.5,-16.5 + parent: 1 + - uid: 260 + components: + - type: Transform + pos: 4.5,-24.5 + parent: 1 + - uid: 261 + components: + - type: Transform + pos: 5.5,-24.5 + parent: 1 + - uid: 262 + components: + - type: Transform + pos: -6.5,-16.5 + parent: 1 + - uid: 263 + components: + - type: Transform + pos: -7.5,-16.5 + parent: 1 + - uid: 264 + components: + - type: Transform + pos: -4.5,-13.5 + parent: 1 + - uid: 265 + components: + - type: Transform + pos: -4.5,-12.5 + parent: 1 + - uid: 266 + components: + - type: Transform + pos: -4.5,-11.5 + parent: 1 + - uid: 267 + components: + - type: Transform + pos: 3.5,-13.5 + parent: 1 + - uid: 268 + components: + - type: Transform + pos: 3.5,-12.5 + parent: 1 + - uid: 269 + components: + - type: Transform + pos: 3.5,-11.5 + parent: 1 + - uid: 270 + components: + - type: Transform + pos: 5.5,-16.5 + parent: 1 + - uid: 271 + components: + - type: Transform + pos: 6.5,-16.5 + parent: 1 + - uid: 272 + components: + - type: Transform + pos: -0.5,-24.5 + parent: 1 + - uid: 273 + components: + - type: Transform + pos: -0.5,-23.5 + parent: 1 + - uid: 274 + components: + - type: Transform + pos: -0.5,-16.5 + parent: 1 + - uid: 275 + components: + - type: Transform + pos: 5.5,-9.5 + parent: 1 + - uid: 276 + components: + - type: Transform + pos: 3.5,-9.5 + parent: 1 + - uid: 277 + components: + - type: Transform + pos: 0.5,-16.5 + parent: 1 + - uid: 278 + components: + - type: Transform + pos: 0.5,-26.5 + parent: 1 + - uid: 279 + components: + - type: Transform + pos: 1.5,-27.5 + parent: 1 + - uid: 280 + components: + - type: Transform + pos: -2.5,-27.5 + parent: 1 + - uid: 281 + components: + - type: Transform + pos: -2.5,-29.5 + parent: 1 + - uid: 282 + components: + - type: Transform + pos: 1.5,-26.5 + parent: 1 + - uid: 283 + components: + - type: Transform + pos: 4.5,-9.5 + parent: 1 + - uid: 284 + components: + - type: Transform + pos: -0.5,-26.5 + parent: 1 + - uid: 285 + components: + - type: Transform + pos: 1.5,-28.5 + parent: 1 + - uid: 286 + components: + - type: Transform + pos: 1.5,-29.5 + parent: 1 + - uid: 287 + components: + - type: Transform + pos: -1.5,-26.5 + parent: 1 + - uid: 288 + components: + - type: Transform + pos: -3.5,-9.5 + parent: 1 + - uid: 289 + components: + - type: Transform + pos: 1.5,-16.5 + parent: 1 + - uid: 290 + components: + - type: Transform + pos: 2.5,-9.5 + parent: 1 + - uid: 291 + components: + - type: Transform + pos: -6.5,-9.5 + parent: 1 + - uid: 292 + components: + - type: Transform + pos: -5.5,-9.5 + parent: 1 + - uid: 293 + components: + - type: Transform + pos: -4.5,-9.5 + parent: 1 + - uid: 294 + components: + - type: Transform + pos: 7.5,-16.5 + parent: 1 + - uid: 295 + components: + - type: Transform + pos: -0.5,-13.5 + parent: 1 + - uid: 296 + components: + - type: Transform + pos: -0.5,-12.5 + parent: 1 + - uid: 297 + components: + - type: Transform + pos: -0.5,-11.5 + parent: 1 +- proto: Chair + entities: + - uid: 298 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 2.5,-21.5 + parent: 1 + - uid: 299 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-21.5 + parent: 1 +- proto: ChairOfficeDark + entities: + - uid: 300 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-3.5 + parent: 1 +- proto: ChairPilotSeat + entities: + - uid: 301 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-21.5 + parent: 1 + - uid: 302 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-8.5 + parent: 1 + - uid: 303 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-9.5 + parent: 1 + - uid: 304 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-10.5 + parent: 1 + - uid: 305 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-8.5 + parent: 1 + - uid: 306 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-10.5 + parent: 1 + - uid: 307 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-9.5 + parent: 1 + - uid: 308 + components: + - type: Transform + pos: -2.5,-15.5 + parent: 1 + - uid: 309 + components: + - type: Transform + pos: -1.5,-15.5 + parent: 1 + - uid: 310 + components: + - type: Transform + pos: 0.5,-15.5 + parent: 1 + - uid: 311 + components: + - type: Transform + pos: 1.5,-15.5 + parent: 1 + - uid: 312 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -0.5,-3.5 + parent: 1 +- proto: CigPackSyndicate + entities: + - uid: 313 + components: + - type: Transform + pos: -3.5658307,-17.516623 + parent: 1 +- proto: ClothingBackpackDuffelSyndicateFilledMedical + entities: + - uid: 314 + components: + - type: Transform + pos: -3.5044222,-6.293252 + parent: 1 +- proto: ClothingHeadHatSyndie + entities: + - uid: 315 + components: + - type: Transform + pos: -3.1200604,-17.289778 + parent: 1 +- proto: ClothingHeadHatSyndieMAA + entities: + - uid: 35 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ClothingHeadPyjamaSyndicateRed + entities: + - uid: 36 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ClothingHeadsetAltSyndicate + entities: + - uid: 316 + components: + - type: Transform + pos: 1.3157192,-13.513277 + parent: 1 +- proto: ClothingMaskGasSyndicate + entities: + - uid: 317 + components: + - type: Transform + pos: 0.94071925,-13.482027 + parent: 1 +- proto: ClothingNeckMantleHOS + entities: + - uid: 37 + components: + - type: MetaData + desc: Looted from a fallen enemy, the commander earned this in battle. + name: commander mantle + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ClothingOuterCoatSyndieCap + entities: + - uid: 38 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ClothingUniformJumpskirtSyndieFormalDress + entities: + - uid: 39 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ClothingUniformJumpsuitPyjamaSyndicateRed + entities: + - uid: 40 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ClothingUniformJumpsuitSyndieFormal + entities: + - uid: 41 + components: + - type: Transform + parent: 33 + - type: Physics + canCollide: False + - type: InsideEntityStorage +- proto: ComfyChair + entities: + - uid: 318 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-4.5 + parent: 1 +- proto: computerBodyScanner + entities: + - uid: 319 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -5.5,-5.5 + parent: 1 +- proto: ComputerIFFSyndicate + entities: + - uid: 320 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-3.5 + parent: 1 +- proto: ComputerPowerMonitoring + entities: + - uid: 321 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -3.5,-21.5 + parent: 1 +- proto: ComputerRadar + entities: + - uid: 322 + components: + - type: Transform + pos: 0.5,-2.5 + parent: 1 +- proto: ComputerShuttleSyndie + entities: + - uid: 323 + components: + - type: Transform + pos: -0.5,-2.5 + parent: 1 +- proto: CrowbarRed + entities: + - uid: 324 + components: + - type: Transform + pos: -3.492848,-22.485775 + parent: 1 +- proto: DisposalBend + entities: + - uid: 325 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-6.5 + parent: 1 +- proto: DisposalJunction + entities: + - uid: 326 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -0.5,-19.5 + parent: 1 +- proto: DisposalPipe + entities: + - uid: 327 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-6.5 + parent: 1 + - uid: 328 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,-19.5 + parent: 1 + - uid: 329 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-19.5 + parent: 1 + - uid: 330 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -3.5,-19.5 + parent: 1 + - uid: 331 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -4.5,-19.5 + parent: 1 + - uid: 332 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -5.5,-19.5 + parent: 1 + - uid: 333 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -6.5,-19.5 + parent: 1 + - uid: 334 + components: + - type: Transform + pos: -0.5,-18.5 + parent: 1 + - uid: 335 + components: + - type: Transform + pos: -0.5,-17.5 + parent: 1 + - uid: 336 + components: + - type: Transform + pos: -0.5,-16.5 + parent: 1 + - uid: 337 + components: + - type: Transform + pos: -0.5,-15.5 + parent: 1 + - uid: 338 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 1 + - uid: 339 + components: + - type: Transform + pos: -0.5,-13.5 + parent: 1 + - uid: 340 + components: + - type: Transform + pos: -0.5,-12.5 + parent: 1 + - uid: 341 + components: + - type: Transform + pos: -0.5,-11.5 + parent: 1 + - uid: 342 + components: + - type: Transform + pos: -0.5,-10.5 + parent: 1 + - uid: 343 + components: + - type: Transform + pos: -0.5,-9.5 + parent: 1 + - uid: 344 + components: + - type: Transform + pos: -0.5,-8.5 + parent: 1 + - uid: 345 + components: + - type: Transform + pos: -0.5,-7.5 + parent: 1 +- proto: DisposalTrunk + entities: + - uid: 346 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-6.5 + parent: 1 + - uid: 347 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -7.5,-19.5 + parent: 1 + - uid: 348 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-19.5 + parent: 1 +- proto: DisposalUnit + entities: + - uid: 349 + components: + - type: Transform + pos: 1.5,-6.5 + parent: 1 + - uid: 350 + components: + - type: Transform + pos: 0.5,-19.5 + parent: 1 +- proto: DoubleEmergencyOxygenTankFilled + entities: + - uid: 351 + components: + - type: Transform + pos: -1.6924903,-23.407444 + parent: 1 + - uid: 352 + components: + - type: Transform + pos: -1.4112403,-23.458082 + parent: 1 + - uid: 353 + components: + - type: Transform + pos: 5.390987,-17.346693 + parent: 1 + - uid: 354 + components: + - type: Transform + pos: -6.6334953,-17.346693 + parent: 1 +- proto: DrinkGlass + entities: + - uid: 355 + components: + - type: Transform + pos: 2.0779252,-19.21155 + parent: 1 + - uid: 356 + components: + - type: Transform + pos: 2.3123002,-19.21155 + parent: 1 +- proto: DrinkMugDog + entities: + - uid: 357 + components: + - type: Transform + pos: 2.2843437,-19.542192 + parent: 1 +- proto: DrinkMugMetal + entities: + - uid: 358 + components: + - type: Transform + pos: 2.0968437,-19.526567 + parent: 1 +- proto: DrinkMugRed + entities: + - uid: 359 + components: + - type: Transform + pos: 1.9918958,-17.588755 + parent: 1 +- proto: DrinkVacuumFlask + entities: + - uid: 360 + components: + - type: Transform + pos: 5.6435027,-21.180143 + parent: 1 + - uid: 361 + components: + - type: Transform + pos: 5.7372527,-21.398893 + parent: 1 +- proto: ExtendedEmergencyOxygenTankFilled + entities: + - uid: 362 + components: + - type: Transform + pos: -5.678572,-12.319441 + parent: 1 + - uid: 363 + components: + - type: Transform + pos: 4.305803,-12.272566 + parent: 1 +- proto: FireAxeFlaming + entities: + - uid: 23 + components: + - type: Transform + pos: -1.5018963,-3.4569345 + parent: 1 +- proto: FoodBoxDonkpocketPizza + entities: + - uid: 364 + components: + - type: Transform + pos: 2.7185502,-19.320925 + parent: 1 +- proto: FoodBoxDonut + entities: + - uid: 365 + components: + - type: Transform + pos: 5.5401826,-21.187487 + parent: 1 +- proto: FoodPizzaDonkpocket + entities: + - uid: 366 + components: + - type: Transform + pos: 1.4776825,-21.296862 + parent: 1 +- proto: FoodSnackSyndi + entities: + - uid: 367 + components: + - type: Transform + pos: 1.5361897,-17.367903 + parent: 1 +- proto: GasMinerNitrogen + entities: + - uid: 368 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-27.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 +- proto: GasMinerOxygen + entities: + - uid: 369 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-27.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 +- proto: GasMixer + entities: + - uid: 370 + components: + - type: MetaData + name: O2+N2 mixer + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-24.5 + parent: 1 + - type: GasMixer + inletTwoConcentration: 0.78 + inletOneConcentration: 0.22 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' +- proto: GasPassiveVent + entities: + - uid: 371 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-26.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - uid: 372 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-26.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - uid: 373 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 6.5,-19.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 +- proto: GasPipeBend + entities: + - uid: 374 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-5.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 375 + components: + - type: Transform + pos: 5.5,-24.5 + parent: 1 + - uid: 376 + components: + - type: Transform + pos: 3.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 377 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -4.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 378 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-5.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 379 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-4.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 380 + components: + - type: Transform + pos: 0.5,-8.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 381 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -1.5,-8.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 382 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -0.5,-4.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 383 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-21.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' +- proto: GasPipeFourway + entities: + - uid: 384 + components: + - type: Transform + pos: 3.5,-23.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 385 + components: + - type: Transform + pos: -0.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 386 + components: + - type: Transform + pos: -0.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 387 + components: + - type: Transform + pos: 0.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' +- proto: GasPipeStraight + entities: + - uid: 388 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-25.5 + parent: 1 + - uid: 389 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-25.5 + parent: 1 + - uid: 390 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-24.5 + parent: 1 + - uid: 391 + components: + - type: Transform + pos: 3.5,-22.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 392 + components: + - type: Transform + pos: 3.5,-21.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 393 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 2.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 394 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 395 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 396 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 397 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 398 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -3.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 399 + components: + - type: Transform + pos: -0.5,-19.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 400 + components: + - type: Transform + pos: -0.5,-18.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 401 + components: + - type: Transform + pos: -0.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 402 + components: + - type: Transform + pos: -0.5,-15.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 403 + components: + - type: Transform + pos: -0.5,-14.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 404 + components: + - type: Transform + pos: -0.5,-13.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 405 + components: + - type: Transform + pos: -0.5,-12.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 406 + components: + - type: Transform + pos: -0.5,-10.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 407 + components: + - type: Transform + pos: -0.5,-9.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 408 + components: + - type: Transform + pos: -0.5,-8.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 409 + components: + - type: Transform + pos: -0.5,-7.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 410 + components: + - type: Transform + pos: -0.5,-6.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 411 + components: + - type: Transform + pos: 0.5,-18.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 412 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 413 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 414 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 415 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 416 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -3.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 417 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 2.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 418 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 5.5,-19.5 + parent: 1 + - uid: 419 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 3.5,-19.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 420 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-19.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 421 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -1.5,-6.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 422 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -0.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 423 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 424 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 425 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -3.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 426 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 427 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 2.5,-17.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 428 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-16.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 429 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-15.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 430 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-14.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 431 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-13.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 432 + components: + - type: Transform + pos: 0.5,-11.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 433 + components: + - type: Transform + pos: 0.5,-10.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 434 + components: + - type: Transform + pos: 0.5,-9.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 435 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -0.5,-8.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 436 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -1.5,-7.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 437 + components: + - type: Transform + pos: 0.5,-20.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 438 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -0.5,-21.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 439 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,-21.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 440 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-21.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 441 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -1.5,-5.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 442 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 0.5,-4.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' +- proto: GasPipeTJunction + entities: + - uid: 443 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -0.5,-11.5 + parent: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 444 + components: + - type: Transform + pos: 2.5,-19.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 445 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-19.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 446 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 0.5,-12.5 + parent: 1 + - type: AtmosPipeColor + color: '#FF1212FF' +- proto: GasPort + entities: + - uid: 447 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.5,-23.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' +- proto: GasPressurePump + entities: + - uid: 448 + components: + - type: MetaData + name: waste pump + - type: Transform + rot: 1.5707963267948966 rad + pos: 4.5,-19.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' +- proto: GasVentPump + entities: + - uid: 449 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 0.5,-11.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 450 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -0.5,-21.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 451 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-23.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 452 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -4.5,-21.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 453 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -4.5,-16.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 454 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 3.5,-16.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' + - uid: 455 + components: + - type: Transform + pos: 0.5,-3.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#0335FCFF' +- proto: GasVentScrubber + entities: + - uid: 456 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-12.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 457 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 3.5,-17.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 458 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 2.5,-20.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 459 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -4.5,-17.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 460 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -3.5,-21.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' + - uid: 461 + components: + - type: Transform + pos: -0.5,-3.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 + - type: AtmosPipeColor + color: '#FF1212FF' +- proto: GeneratorBasic15kW + entities: + - uid: 462 + components: + - type: Transform + pos: -4.5,-26.5 + parent: 1 + - uid: 463 + components: + - type: Transform + pos: -4.5,-25.5 + parent: 1 + - uid: 464 + components: + - type: Transform + pos: -4.5,-24.5 + parent: 1 + - uid: 465 + components: + - type: Transform + pos: -6.5,-24.5 + parent: 1 + - uid: 466 + components: + - type: Transform + pos: -6.5,-25.5 + parent: 1 + - uid: 467 + components: + - type: Transform + pos: -6.5,-26.5 + parent: 1 +- proto: GeneratorWallmountAPU + entities: + - uid: 468 + components: + - type: Transform + pos: -6.5,-20.5 + parent: 1 +- proto: GravityGeneratorMini + entities: + - uid: 469 + components: + - type: Transform + pos: -5.5,-22.5 + parent: 1 +- proto: Grille + entities: + - uid: 470 + components: + - type: Transform + pos: 2.5,-12.5 + parent: 1 + - uid: 471 + components: + - type: Transform + pos: -3.5,-12.5 + parent: 1 + - uid: 472 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-7.5 + parent: 1 + - uid: 473 + components: + - type: Transform + pos: -2.5,-1.5 + parent: 1 + - uid: 474 + components: + - type: Transform + pos: -0.5,-1.5 + parent: 1 + - uid: 475 + components: + - type: Transform + pos: -4.5,-2.5 + parent: 1 + - uid: 476 + components: + - type: Transform + pos: -6.5,-4.5 + parent: 1 + - uid: 477 + components: + - type: Transform + pos: 3.5,-2.5 + parent: 1 + - uid: 478 + components: + - type: Transform + pos: -1.5,-1.5 + parent: 1 + - uid: 479 + components: + - type: Transform + pos: 5.5,-4.5 + parent: 1 + - uid: 480 + components: + - type: Transform + pos: 0.5,-1.5 + parent: 1 + - uid: 481 + components: + - type: Transform + pos: 1.5,-9.5 + parent: 1 + - uid: 482 + components: + - type: Transform + pos: -2.5,-9.5 + parent: 1 + - uid: 483 + components: + - type: Transform + pos: 1.5,-1.5 + parent: 1 + - uid: 484 + components: + - type: Transform + pos: -2.5,-21.5 + parent: 1 + - uid: 485 + components: + - type: Transform + pos: -2.5,-19.5 + parent: 1 + - uid: 486 + components: + - type: Transform + pos: 2.5,-22.5 + parent: 1 + - uid: 487 + components: + - type: Transform + pos: 4.5,-22.5 + parent: 1 + - uid: 488 + components: + - type: Transform + pos: -1.5,-18.5 + parent: 1 + - uid: 489 + components: + - type: Transform + pos: 0.5,-7.5 + parent: 1 + - uid: 490 + components: + - type: Transform + pos: 3.5,-25.5 + parent: 1 + - uid: 491 + components: + - type: Transform + pos: 5.5,-25.5 + parent: 1 + - uid: 492 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 5.5,-19.5 + parent: 1 + - uid: 493 + components: + - type: Transform + pos: 0.5,-18.5 + parent: 1 +- proto: Gyroscope + entities: + - uid: 494 + components: + - type: Transform + pos: -5.5,-13.5 + parent: 1 + - uid: 495 + components: + - type: Transform + pos: 4.5,-13.5 + parent: 1 +- proto: HospitalCurtainsOpen + entities: + - uid: 496 + components: + - type: Transform + pos: 3.5,-3.5 + parent: 1 +- proto: SyndicateMicrowave + entities: + - uid: 497 + components: + - type: Transform + pos: 3.5,-19.5 + parent: 1 +- proto: KnifePlastic + entities: + - uid: 498 + components: + - type: Transform + pos: 5.3509636,-21.445768 + parent: 1 +- proto: Lamp + entities: + - uid: 499 + components: + - type: Transform + pos: -1.483297,-2.2444057 + parent: 1 +- proto: LockerSyndicatePersonal + entities: + - uid: 33 + components: + - type: Transform + pos: 4.5,-5.5 + parent: 1 + - type: EntityStorage + air: + volume: 200 + immutable: False + temperature: 293.1496 + moles: + - 1.7459903 + - 6.568249 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - type: ContainerContainer + containers: + entity_storage: !type:Container + showEnts: False + occludes: True + ents: + - 37 + - 39 + - 36 + - 40 + - 41 + - 38 + - 35 + - 34 + paper_label: !type:ContainerSlot + showEnts: False + occludes: True + ent: null +- proto: MedicalBed + entities: + - uid: 500 + components: + - type: Transform + pos: -4.5,-3.5 + parent: 1 +- proto: MedkitCombatFilled + entities: + - uid: 501 + components: + - type: Transform + pos: -3.401462,-3.5350046 + parent: 1 + - uid: 502 + components: + - type: Transform + pos: -3.557712,-3.4256296 + parent: 1 +- proto: Mirror + entities: + - uid: 503 + components: + - type: Transform + pos: 4.5,-3.5 + parent: 1 +- proto: Multitool + entities: + - uid: 504 + components: + - type: Transform + pos: -3.383473,-22.548275 + parent: 1 +- proto: NitrogenTankFilled + entities: + - uid: 505 + components: + - type: Transform + pos: 4.633928,-12.616316 + parent: 1 + - uid: 506 + components: + - type: Transform + pos: -5.397322,-12.569441 + parent: 1 + - uid: 507 + components: + - type: Transform + pos: -6.3522453,-17.549818 + parent: 1 + - uid: 508 + components: + - type: Transform + pos: 5.6633797,-17.565443 + parent: 1 +- proto: NuclearBombUnanchored + entities: + - uid: 509 + components: + - type: Transform + pos: -2.5,-12.5 + parent: 1 +- proto: NukeCodePaper + entities: + - uid: 510 + components: + - type: Transform + pos: -2.5286522,-11.44479 + parent: 1 +- proto: Ointment + entities: + - uid: 511 + components: + - type: Transform + pos: -3.651462,-4.5193796 + parent: 1 + - uid: 512 + components: + - type: Transform + pos: -3.667087,-4.2225046 + parent: 1 +- proto: OperatingTable + entities: + - uid: 513 + components: + - type: Transform + pos: -5.5,-4.5 + parent: 1 +- proto: OxygenCanister + entities: + - uid: 514 + components: + - type: Transform + pos: -1.5,-24.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 +- proto: OxygenTankFilled + entities: + - uid: 515 + components: + - type: Transform + pos: -5.600447,-12.569441 + parent: 1 + - uid: 516 + components: + - type: Transform + pos: 4.399553,-12.522566 + parent: 1 + - uid: 517 + components: + - type: Transform + pos: 5.5227547,-17.440443 + parent: 1 + - uid: 518 + components: + - type: Transform + pos: -6.4928703,-17.440443 + parent: 1 +- proto: PinpointerNuclear + entities: + - uid: 519 + components: + - type: Transform + pos: -2.4942985,-13.37949 + parent: 1 +- proto: PlasmaReinforcedWindowDirectional + entities: + - uid: 520 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-4.5 + parent: 1 + - uid: 521 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-6.5 + parent: 1 + - uid: 522 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-3.5 + parent: 1 +- proto: PlushieNuke + entities: + - uid: 523 + components: + - type: Transform + pos: -2.4227936,-2.3320491 + parent: 1 +- proto: PosterContrabandC20r + entities: + - uid: 524 + components: + - type: Transform + pos: 1.5,-14.5 + parent: 1 +- proto: PosterContrabandCC64KAd + entities: + - uid: 525 + components: + - type: Transform + pos: -5.5,-18.5 + parent: 1 +- proto: PosterContrabandCybersun600 + entities: + - uid: 526 + components: + - type: Transform + pos: 2.5,-6.5 + parent: 1 +- proto: PosterContrabandDonk + entities: + - uid: 527 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-18.5 + parent: 1 +- proto: PosterContrabandDonutCorp + entities: + - uid: 528 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 1.5,-22.5 + parent: 1 +- proto: PosterContrabandEnergySwords + entities: + - uid: 529 + components: + - type: Transform + pos: 2.5,-18.5 + parent: 1 +- proto: PosterContrabandEnlistGorlex + entities: + - uid: 530 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 2.5,-13.5 + parent: 1 +- proto: PosterContrabandFreeSyndicateEncryptionKey + entities: + - uid: 531 + components: + - type: Transform + pos: -2.5,-8.5 + parent: 1 +- proto: PosterContrabandInterdyne + entities: + - uid: 532 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -4.5,-6.5 + parent: 1 +- proto: PosterContrabandKosmicheskayaStantsiya + entities: + - uid: 533 + components: + - type: Transform + pos: -2.5,-22.5 + parent: 1 +- proto: PosterContrabandMoth + entities: + - uid: 534 + components: + - type: Transform + pos: -2.5,-14.5 + parent: 1 + - uid: 535 + components: + - type: Transform + pos: 2.5,-3.5 + parent: 1 +- proto: PosterContrabandNuclearDeviceInformational + entities: + - uid: 536 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -3.5,-13.5 + parent: 1 + - uid: 537 + components: + - type: Transform + pos: 0.5,-14.5 + parent: 1 +- proto: PosterContrabandSyndicatePistol + entities: + - uid: 538 + components: + - type: Transform + pos: 1.5,-10.5 + parent: 1 +- proto: PosterContrabandSyndicateRecruitment + entities: + - uid: 539 + components: + - type: Transform + pos: -1.5,-14.5 + parent: 1 +- proto: PosterContrabandWaffleCorp + entities: + - uid: 540 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -2.5,-23.5 + parent: 1 +- proto: Poweredlight + entities: + - uid: 541 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-8.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 542 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-4.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 543 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -6.5,-23.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 544 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -6.5,-26.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 545 + components: + - type: Transform + pos: -4.5,-19.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 546 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 1.5,-13.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 547 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-27.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 548 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-27.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 549 + components: + - type: Transform + pos: 2.5,-15.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 550 + components: + - type: Transform + pos: -3.5,-15.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 551 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -4.5,-5.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 552 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-2.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 553 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-6.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 +- proto: PoweredSmallLight + entities: + - uid: 554 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-13.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 555 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-21.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 556 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-5.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 557 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 5.5,-24.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 558 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-13.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 559 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -1.5,-24.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 560 + components: + - type: Transform + pos: -6.5,-15.5 + parent: 1 + - uid: 561 + components: + - type: Transform + pos: 5.5,-15.5 + parent: 1 + - uid: 562 + components: + - type: Transform + pos: 2.5,-19.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 563 + components: + - type: Transform + pos: -1.5,-26.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 564 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-9.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 + - uid: 565 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-9.5 + parent: 1 + - type: ApcPowerReceiver + powerLoad: 0 +- proto: Rack + entities: + - uid: 566 + components: + - type: Transform + pos: -3.5,-22.5 + parent: 1 + - uid: 567 + components: + - type: Transform + pos: 5.5,-23.5 + parent: 1 + - uid: 568 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-12.5 + parent: 1 + - uid: 569 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-12.5 + parent: 1 + - uid: 570 + components: + - type: Transform + pos: -1.5,-23.5 + parent: 1 + - uid: 571 + components: + - type: Transform + pos: -6.5,-17.5 + parent: 1 + - uid: 572 + components: + - type: Transform + pos: 5.5,-17.5 + parent: 1 +- proto: ReinforcedPlasmaWindow + entities: + - uid: 573 + components: + - type: Transform + pos: 3.5,-2.5 + parent: 1 + - uid: 574 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 5.5,-19.5 + parent: 1 + - uid: 575 + components: + - type: Transform + pos: -0.5,-1.5 + parent: 1 + - uid: 576 + components: + - type: Transform + pos: -6.5,-4.5 + parent: 1 + - uid: 577 + components: + - type: Transform + pos: -2.5,-1.5 + parent: 1 + - uid: 578 + components: + - type: Transform + pos: -4.5,-2.5 + parent: 1 + - uid: 579 + components: + - type: Transform + pos: 5.5,-4.5 + parent: 1 + - uid: 580 + components: + - type: Transform + pos: -1.5,-1.5 + parent: 1 + - uid: 581 + components: + - type: Transform + pos: 0.5,-1.5 + parent: 1 + - uid: 582 + components: + - type: Transform + pos: 1.5,-1.5 + parent: 1 + - uid: 583 + components: + - type: Transform + pos: -1.5,-7.5 + parent: 1 + - uid: 584 + components: + - type: Transform + pos: 4.5,-22.5 + parent: 1 + - uid: 585 + components: + - type: Transform + pos: 2.5,-22.5 + parent: 1 + - uid: 586 + components: + - type: Transform + pos: -2.5,-21.5 + parent: 1 + - uid: 587 + components: + - type: Transform + pos: -2.5,-19.5 + parent: 1 + - uid: 588 + components: + - type: Transform + pos: -2.5,-9.5 + parent: 1 + - uid: 589 + components: + - type: Transform + pos: 1.5,-9.5 + parent: 1 + - uid: 590 + components: + - type: Transform + pos: 0.5,-18.5 + parent: 1 + - uid: 591 + components: + - type: Transform + pos: 3.5,-25.5 + parent: 1 + - uid: 592 + components: + - type: Transform + pos: 5.5,-25.5 + parent: 1 + - uid: 593 + components: + - type: Transform + pos: 2.5,-12.5 + parent: 1 + - uid: 594 + components: + - type: Transform + pos: -3.5,-12.5 + parent: 1 + - uid: 595 + components: + - type: Transform + pos: 0.5,-7.5 + parent: 1 + - uid: 596 + components: + - type: Transform + pos: -1.5,-18.5 + parent: 1 +- proto: ShuttersNormalOpen + entities: + - uid: 597 + components: + - type: Transform + pos: -6.5,-4.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 + - uid: 598 + components: + - type: Transform + pos: 5.5,-4.5 + parent: 1 + - type: DeviceLinkSink + links: + - 20 + - uid: 599 + components: + - type: Transform + pos: 1.5,-9.5 + parent: 1 + - type: DeviceLinkSink + links: + - 609 + - uid: 600 + components: + - type: Transform + pos: -2.5,-9.5 + parent: 1 + - type: DeviceLinkSink + links: + - 609 + - uid: 601 + components: + - type: Transform + pos: -4.5,-2.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 + - uid: 602 + components: + - type: Transform + pos: 3.5,-2.5 + parent: 1 + - type: DeviceLinkSink + links: + - 20 + - uid: 603 + components: + - type: Transform + pos: 1.5,-1.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 + - uid: 604 + components: + - type: Transform + pos: -0.5,-1.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 + - uid: 605 + components: + - type: Transform + pos: 5.5,-19.5 + parent: 1 + - type: DeviceLinkSink + links: + - 611 + - uid: 606 + components: + - type: Transform + pos: -1.5,-1.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 + - uid: 607 + components: + - type: Transform + pos: 0.5,-1.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 + - uid: 608 + components: + - type: Transform + pos: -2.5,-1.5 + parent: 1 + - type: DeviceLinkSink + links: + - 21 +- proto: SignalButton + entities: + - uid: 609 + components: + - type: Transform + pos: -2.5,-10.5 + parent: 1 + - type: DeviceLinkSource + linkedPorts: + 600: + - Pressed: Toggle + 599: + - Pressed: Toggle + - uid: 611 + components: + - type: Transform + pos: 5.5,-20.5 + parent: 1 + - type: DeviceLinkSource + linkedPorts: + 605: + - Pressed: Toggle +- proto: SignalButtonDirectional + entities: + - uid: 20 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.5,-4.5 + parent: 1 + - type: DeviceLinkSource + linkedPorts: + 602: + - Pressed: Toggle + 598: + - Pressed: Toggle + - uid: 21 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -2.5,-7.5 + parent: 1 + - type: DeviceLinkSource + linkedPorts: + 597: + - Pressed: Toggle + 601: + - Pressed: Toggle + 608: + - Pressed: Toggle + 606: + - Pressed: Toggle + 604: + - Pressed: Toggle + 607: + - Pressed: Toggle + 603: + - Pressed: Toggle +- proto: SignDirectionalEvac + entities: + - uid: 613 + components: + - type: Transform + pos: 0.5,-22.5 + parent: 1 + - uid: 614 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 4.5,-15.5 + parent: 1 + - uid: 615 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-14.5 + parent: 1 + - uid: 616 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-14.5 + parent: 1 + - uid: 617 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -5.5,-15.5 + parent: 1 +- proto: SignElectricalMed + entities: + - uid: 618 + components: + - type: Transform + pos: -2.5,-18.5 + parent: 1 +- proto: SignNosmoking + entities: + - uid: 619 + components: + - type: Transform + pos: 5.5,-22.5 + parent: 1 +- proto: SignSecureSmallRed + entities: + - uid: 620 + components: + - type: Transform + pos: -3.5,-11.5 + parent: 1 + - uid: 621 + components: + - type: Transform + pos: 4.5,-17.5 + parent: 1 + - uid: 622 + components: + - type: Transform + pos: 2.5,-11.5 + parent: 1 +- proto: SMESBasic + entities: + - uid: 623 + components: + - type: Transform + pos: -5.5,-19.5 + parent: 1 + - uid: 624 + components: + - type: Transform + pos: -4.5,-19.5 + parent: 1 +- proto: SoapSyndie + entities: + - uid: 625 + components: + - type: Transform + pos: 2.4424396,-17.430403 + parent: 1 +- proto: soda_dispenser + entities: + - uid: 626 + components: + - type: Transform + pos: 1.5,-19.5 + parent: 1 +- proto: StorageCanister + entities: + - uid: 627 + components: + - type: Transform + pos: 2.5,-23.5 + parent: 1 + - type: AtmosDevice + joinedGrid: 1 +- proto: SubstationBasic + entities: + - uid: 628 + components: + - type: Transform + pos: -3.5,-19.5 + parent: 1 +- proto: SuitStorageEVASyndicate + entities: + - uid: 629 + components: + - type: Transform + pos: 0.5,-24.5 + parent: 1 + - uid: 630 + components: + - type: Transform + pos: 0.5,-23.5 + parent: 1 + - uid: 631 + components: + - type: Transform + pos: 3.5,-17.5 + parent: 1 + - uid: 632 + components: + - type: Transform + pos: -4.5,-17.5 + parent: 1 +- proto: SyndieMiniBomb + entities: + - uid: 633 + components: + - type: Transform + pos: 1.4127003,-11.973867 + parent: 1 + - uid: 634 + components: + - type: Transform + pos: 1.6939503,-11.973867 + parent: 1 +- proto: SyringeInaprovaline + entities: + - uid: 635 + components: + - type: Transform + pos: -3.510837,-4.3787546 + parent: 1 + - uid: 636 + components: + - type: Transform + pos: -3.510837,-4.0193796 + parent: 1 +- proto: Table + entities: + - uid: 637 + components: + - type: Transform + pos: 5.5,-21.5 + parent: 1 + - uid: 638 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 1.5,-21.5 + parent: 1 + - uid: 639 + components: + - type: Transform + pos: 3.5,-19.5 + parent: 1 + - uid: 640 + components: + - type: Transform + pos: 2.5,-19.5 + parent: 1 + - uid: 641 + components: + - type: Transform + pos: 1.5,-19.5 + parent: 1 +- proto: TablePlasmaGlass + entities: + - uid: 642 + components: + - type: Transform + pos: -3.5,-4.5 + parent: 1 + - uid: 643 + components: + - type: Transform + pos: -3.5,-6.5 + parent: 1 + - uid: 644 + components: + - type: Transform + pos: -3.5,-3.5 + parent: 1 +- proto: TableReinforced + entities: + - uid: 645 + components: + - type: Transform + pos: -2.5,-11.5 + parent: 1 + - uid: 646 + components: + - type: Transform + pos: -2.5,-13.5 + parent: 1 + - uid: 647 + components: + - type: Transform + pos: -1.5,-3.5 + parent: 1 + - uid: 648 + components: + - type: Transform + pos: -2.5,-2.5 + parent: 1 + - uid: 649 + components: + - type: Transform + pos: 0.5,-13.5 + parent: 1 + - uid: 650 + components: + - type: Transform + pos: 1.5,-13.5 + parent: 1 + - uid: 651 + components: + - type: Transform + pos: 1.5,-12.5 + parent: 1 + - uid: 652 + components: + - type: Transform + pos: 1.5,-11.5 + parent: 1 + - uid: 653 + components: + - type: Transform + pos: 2.5,-17.5 + parent: 1 + - uid: 654 + components: + - type: Transform + pos: 1.5,-17.5 + parent: 1 + - uid: 655 + components: + - type: Transform + pos: -2.5,-17.5 + parent: 1 + - uid: 656 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 1.5,-2.5 + parent: 1 + - uid: 657 + components: + - type: Transform + pos: -3.5,-17.5 + parent: 1 + - uid: 658 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -1.5,-2.5 + parent: 1 +- proto: Thruster + entities: + - uid: 659 + components: + - type: Transform + pos: -6.5,-2.5 + parent: 1 + - uid: 660 + components: + - type: Transform + pos: 5.5,-2.5 + parent: 1 + - uid: 661 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -5.5,-7.5 + parent: 1 + - uid: 662 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-7.5 + parent: 1 + - uid: 663 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -7.5,-13.5 + parent: 1 + - uid: 664 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 6.5,-13.5 + parent: 1 + - uid: 665 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-30.5 + parent: 1 + - uid: 666 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -4.5,-30.5 + parent: 1 + - uid: 667 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 4.5,-30.5 + parent: 1 + - uid: 668 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 3.5,-30.5 + parent: 1 +- proto: ToolboxSyndicate + entities: + - uid: 669 + components: + - type: Transform + pos: -2.5731854,-17.414778 + parent: 1 +- proto: ToolboxSyndicateFilled + entities: + - uid: 670 + components: + - type: Transform + pos: 1.5034143,-11.298322 + parent: 1 +- proto: VendingMachineTankDispenserEVA + entities: + - uid: 671 + components: + - type: Transform + pos: 5.5,-15.5 + parent: 1 + - uid: 672 + components: + - type: MetaData + name: tank dispenser + - type: Transform + pos: 2.5,-24.5 + parent: 1 + - uid: 673 + components: + - type: Transform + pos: -6.5,-15.5 + parent: 1 +- proto: VendingMachineYouTool + entities: + - uid: 674 + components: + - type: Transform + pos: -3.5,-24.5 + parent: 1 +- proto: WallPlastitanium + entities: + - uid: 11 + components: + - type: Transform + pos: -1.5,-25.5 + parent: 1 + - uid: 675 + components: + - type: Transform + pos: 2.5,-2.5 + parent: 1 + - uid: 676 + components: + - type: Transform + pos: 4.5,-3.5 + parent: 1 + - uid: 677 + components: + - type: Transform + pos: -3.5,-11.5 + parent: 1 + - uid: 678 + components: + - type: Transform + pos: 2.5,-14.5 + parent: 1 + - uid: 679 + components: + - type: Transform + pos: 4.5,-14.5 + parent: 1 + - uid: 680 + components: + - type: Transform + pos: -2.5,-14.5 + parent: 1 + - uid: 681 + components: + - type: Transform + pos: 2.5,-18.5 + parent: 1 + - uid: 682 + components: + - type: Transform + pos: 1.5,-25.5 + parent: 1 + - uid: 683 + components: + - type: Transform + pos: 1.5,-18.5 + parent: 1 + - uid: 684 + components: + - type: Transform + pos: 7.5,-17.5 + parent: 1 + - uid: 685 + components: + - type: Transform + pos: -3.5,-26.5 + parent: 1 + - uid: 686 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 3.5,-7.5 + parent: 1 + - uid: 687 + components: + - type: Transform + pos: 2.5,-13.5 + parent: 1 + - uid: 688 + components: + - type: Transform + pos: -3.5,-28.5 + parent: 1 + - uid: 689 + components: + - type: Transform + pos: 2.5,-10.5 + parent: 1 + - uid: 690 + components: + - type: Transform + pos: 4.5,-18.5 + parent: 1 + - uid: 691 + components: + - type: Transform + pos: -2.5,-10.5 + parent: 1 + - uid: 692 + components: + - type: Transform + pos: -3.5,-10.5 + parent: 1 + - uid: 693 + components: + - type: Transform + pos: -6.5,-11.5 + parent: 1 + - uid: 694 + components: + - type: Transform + pos: 4.5,-10.5 + parent: 1 + - uid: 695 + components: + - type: Transform + pos: 4.5,-2.5 + parent: 1 + - uid: 696 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -4.5,-7.5 + parent: 1 + - uid: 697 + components: + - type: Transform + pos: -6.5,-6.5 + parent: 1 + - uid: 698 + components: + - type: Transform + pos: -5.5,-6.5 + parent: 1 + - uid: 699 + components: + - type: Transform + pos: 4.5,-6.5 + parent: 1 + - uid: 700 + components: + - type: Transform + pos: -6.5,-5.5 + parent: 1 + - uid: 701 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -2.5,-8.5 + parent: 1 + - uid: 702 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-7.5 + parent: 1 + - uid: 703 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 1.5,-8.5 + parent: 1 + - uid: 704 + components: + - type: Transform + pos: 2.5,-11.5 + parent: 1 + - uid: 705 + components: + - type: Transform + pos: 5.5,-18.5 + parent: 1 + - uid: 706 + components: + - type: Transform + pos: -8.5,-15.5 + parent: 1 + - uid: 707 + components: + - type: Transform + pos: -8.5,-17.5 + parent: 1 + - uid: 708 + components: + - type: Transform + pos: -7.5,-17.5 + parent: 1 + - uid: 709 + components: + - type: Transform + pos: -5.5,-10.5 + parent: 1 + - uid: 710 + components: + - type: Transform + pos: -3.5,-13.5 + parent: 1 + - uid: 711 + components: + - type: Transform + pos: 3.5,-18.5 + parent: 1 + - uid: 712 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -4.5,-6.5 + parent: 1 + - uid: 713 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -3.5,-7.5 + parent: 1 + - uid: 714 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -3.5,-8.5 + parent: 1 + - uid: 715 + components: + - type: Transform + pos: 5.5,-14.5 + parent: 1 + - uid: 716 + components: + - type: Transform + pos: 7.5,-15.5 + parent: 1 + - uid: 717 + components: + - type: Transform + pos: -6.5,-19.5 + parent: 1 + - uid: 718 + components: + - type: Transform + pos: -6.5,-18.5 + parent: 1 + - uid: 719 + components: + - type: Transform + pos: -3.5,-25.5 + parent: 1 + - uid: 720 + components: + - type: Transform + pos: 2.5,-25.5 + parent: 1 + - uid: 721 + components: + - type: Transform + pos: 2.5,-27.5 + parent: 1 + - uid: 722 + components: + - type: Transform + pos: 4.5,-17.5 + parent: 1 + - uid: 723 + components: + - type: Transform + pos: -7.5,-21.5 + parent: 1 + - uid: 724 + components: + - type: Transform + pos: -7.5,-27.5 + parent: 1 + - uid: 725 + components: + - type: Transform + pos: -7.5,-24.5 + parent: 1 + - uid: 726 + components: + - type: Transform + pos: 6.5,-21.5 + parent: 1 + - uid: 727 + components: + - type: Transform + pos: 6.5,-24.5 + parent: 1 + - uid: 728 + components: + - type: Transform + pos: 6.5,-26.5 + parent: 1 + - uid: 729 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.5,-3.5 + parent: 1 + - uid: 730 + components: + - type: Transform + pos: 1.5,-10.5 + parent: 1 + - uid: 731 + components: + - type: Transform + pos: -3.5,-27.5 + parent: 1 + - uid: 732 + components: + - type: Transform + pos: -6.5,-12.5 + parent: 1 + - uid: 733 + components: + - type: Transform + pos: 5.5,-12.5 + parent: 1 + - uid: 734 + components: + - type: Transform + pos: 5.5,-11.5 + parent: 1 + - uid: 735 + components: + - type: Transform + pos: -5.5,-11.5 + parent: 1 + - uid: 736 + components: + - type: Transform + pos: -6.5,-20.5 + parent: 1 + - uid: 737 + components: + - type: Transform + pos: 0.5,-14.5 + parent: 1 + - uid: 738 + components: + - type: Transform + pos: -1.5,-14.5 + parent: 1 + - uid: 739 + components: + - type: Transform + pos: 5.5,-20.5 + parent: 1 + - uid: 740 + components: + - type: Transform + pos: -5.5,-14.5 + parent: 1 + - uid: 741 + components: + - type: Transform + pos: 6.5,-17.5 + parent: 1 + - uid: 742 + components: + - type: Transform + pos: -3.5,-14.5 + parent: 1 + - uid: 743 + components: + - type: Transform + pos: -2.5,-18.5 + parent: 1 + - uid: 744 + components: + - type: Transform + pos: 2.5,-26.5 + parent: 1 + - uid: 745 + components: + - type: Transform + pos: 2.5,-28.5 + parent: 1 + - uid: 746 + components: + - type: Transform + pos: 6.5,-25.5 + parent: 1 + - uid: 747 + components: + - type: Transform + pos: 6.5,-22.5 + parent: 1 + - uid: 748 + components: + - type: Transform + pos: -7.5,-23.5 + parent: 1 + - uid: 749 + components: + - type: Transform + pos: -7.5,-26.5 + parent: 1 + - uid: 750 + components: + - type: Transform + pos: -7.5,-22.5 + parent: 1 + - uid: 751 + components: + - type: Transform + pos: -7.5,-20.5 + parent: 1 + - uid: 752 + components: + - type: Transform + pos: 4.5,-15.5 + parent: 1 + - uid: 753 + components: + - type: Transform + pos: 6.5,-28.5 + parent: 1 + - uid: 754 + components: + - type: Transform + pos: 6.5,-27.5 + parent: 1 + - uid: 755 + components: + - type: Transform + pos: 6.5,-23.5 + parent: 1 + - uid: 756 + components: + - type: Transform + pos: 6.5,-20.5 + parent: 1 + - uid: 757 + components: + - type: Transform + pos: -7.5,-25.5 + parent: 1 + - uid: 758 + components: + - type: Transform + pos: -7.5,-28.5 + parent: 1 + - uid: 759 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-15.5 + parent: 1 + - uid: 760 + components: + - type: Transform + pos: -5.5,-18.5 + parent: 1 + - uid: 761 + components: + - type: Transform + pos: -5.5,-3.5 + parent: 1 + - uid: 762 + components: + - type: Transform + pos: 5.5,-5.5 + parent: 1 + - uid: 763 + components: + - type: Transform + pos: -3.5,-2.5 + parent: 1 + - uid: 764 + components: + - type: Transform + pos: -6.5,-3.5 + parent: 1 + - uid: 765 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -2.5,-7.5 + parent: 1 + - uid: 766 + components: + - type: Transform + pos: -3.5,-18.5 + parent: 1 + - uid: 767 + components: + - type: Transform + pos: -3.5,-1.5 + parent: 1 + - uid: 768 + components: + - type: Transform + pos: 2.5,-1.5 + parent: 1 + - uid: 769 + components: + - type: Transform + pos: -6.5,-14.5 + parent: 1 + - uid: 770 + components: + - type: Transform + pos: -5.5,-2.5 + parent: 1 + - uid: 771 + components: + - type: Transform + pos: 5.5,-6.5 + parent: 1 + - uid: 772 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.5,-7.5 + parent: 1 + - uid: 773 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.5,-8.5 + parent: 1 + - uid: 774 + components: + - type: Transform + pos: 4.5,-11.5 + parent: 1 + - uid: 775 + components: + - type: Transform + pos: -6.5,-13.5 + parent: 1 + - uid: 776 + components: + - type: Transform + pos: -3.5,-30.5 + parent: 1 + - uid: 777 + components: + - type: Transform + pos: 6.5,-15.5 + parent: 1 + - uid: 778 + components: + - type: Transform + pos: 5.5,-13.5 + parent: 1 + - uid: 779 + components: + - type: Transform + pos: -7.5,-15.5 + parent: 1 + - uid: 780 + components: + - type: Transform + pos: 1.5,-14.5 + parent: 1 + - uid: 781 + components: + - type: Transform + pos: -4.5,-18.5 + parent: 1 + - uid: 782 + components: + - type: Transform + pos: -2.5,-25.5 + parent: 1 + - uid: 783 + components: + - type: Transform + pos: 0.5,-25.5 + parent: 1 + - uid: 785 + components: + - type: Transform + pos: -1.5,-22.5 + parent: 1 + - uid: 786 + components: + - type: Transform + pos: 0.5,-22.5 + parent: 1 + - uid: 787 + components: + - type: Transform + pos: 1.5,-24.5 + parent: 1 + - uid: 788 + components: + - type: Transform + pos: 1.5,-23.5 + parent: 1 + - uid: 789 + components: + - type: Transform + pos: 1.5,-22.5 + parent: 1 + - uid: 790 + components: + - type: Transform + pos: -2.5,-24.5 + parent: 1 + - uid: 791 + components: + - type: Transform + pos: -2.5,-23.5 + parent: 1 + - uid: 792 + components: + - type: Transform + pos: -2.5,-22.5 + parent: 1 + - uid: 793 + components: + - type: Transform + pos: 5.5,-22.5 + parent: 1 + - uid: 794 + components: + - type: Transform + pos: -7.5,-14.5 + parent: 1 + - uid: 795 + components: + - type: Transform + pos: -7.5,-18.5 + parent: 1 + - uid: 796 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -4.5,-28.5 + parent: 1 + - uid: 797 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -5.5,-28.5 + parent: 1 + - uid: 798 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -6.5,-28.5 + parent: 1 + - uid: 799 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 3.5,-28.5 + parent: 1 + - uid: 800 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 4.5,-28.5 + parent: 1 + - uid: 801 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 5.5,-28.5 + parent: 1 + - uid: 802 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: 2.5,-6.5 + parent: 1 + - uid: 803 + components: + - type: Transform + pos: 4.5,-27.5 + parent: 1 + - uid: 804 + components: + - type: Transform + pos: 4.5,-26.5 + parent: 1 + - uid: 805 + components: + - type: Transform + pos: 4.5,-25.5 + parent: 1 + - uid: 806 + components: + - type: Transform + pos: 2.5,-4.5 + parent: 1 + - uid: 807 + components: + - type: Transform + pos: 3.5,-6.5 + parent: 1 + - uid: 808 + components: + - type: Transform + pos: 2.5,-30.5 + parent: 1 + - uid: 809 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -5.5,-17.5 + parent: 1 + - uid: 810 + components: + - type: Transform + pos: 3.5,-29.5 + parent: 1 + - uid: 811 + components: + - type: Transform + pos: -5.5,-29.5 + parent: 1 + - uid: 812 + components: + - type: Transform + pos: 4.5,-29.5 + parent: 1 + - uid: 813 + components: + - type: Transform + pos: -4.5,-29.5 + parent: 1 + - uid: 814 + components: + - type: Transform + pos: 5.5,-29.5 + parent: 1 + - uid: 815 + components: + - type: Transform + pos: -3.5,-29.5 + parent: 1 + - uid: 816 + components: + - type: Transform + pos: 2.5,-29.5 + parent: 1 + - uid: 817 + components: + - type: Transform + pos: 6.5,-14.5 + parent: 1 + - uid: 818 + components: + - type: Transform + pos: 6.5,-18.5 + parent: 1 + - uid: 819 + components: + - type: Transform + pos: 5.5,-3.5 + parent: 1 + - uid: 820 + components: + - type: Transform + pos: -6.5,-29.5 + parent: 1 + - uid: 821 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -6.5,-30.5 + parent: 1 + - uid: 822 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: 5.5,-30.5 + parent: 1 + - uid: 823 + components: + - type: Transform + pos: 8.5,-15.5 + parent: 1 + - uid: 824 + components: + - type: Transform + pos: 8.5,-17.5 + parent: 1 + - uid: 825 + components: + - type: Transform + pos: -9.5,-17.5 + parent: 1 + - uid: 826 + components: + - type: Transform + pos: -9.5,-15.5 + parent: 1 +- proto: WarningN2 + entities: + - uid: 827 + components: + - type: Transform + pos: 4.5,-25.5 + parent: 1 +- proto: WarningO2 + entities: + - uid: 828 + components: + - type: Transform + pos: 2.5,-25.5 + parent: 1 +- proto: WarningWaste + entities: + - uid: 829 + components: + - type: Transform + pos: 4.5,-18.5 + parent: 1 +- proto: WaterCooler + entities: + - uid: 830 + components: + - type: Transform + pos: 0.5,-17.5 + parent: 1 +- proto: WeaponTurretSyndicate + entities: + - uid: 24 + components: + - type: Transform + pos: -2.5,-6.5 + parent: 1 + - uid: 610 + components: + - type: Transform + pos: -1.5,-19.5 + parent: 1 +- proto: WindoorSecure + entities: + - uid: 831 + components: + - type: Transform + rot: -1.5707963267948966 rad + pos: -2.5,-5.5 + parent: 1 +- proto: Wrench + entities: + - uid: 832 + components: + - type: Transform + pos: 5.4749,-23.512577 + parent: 1 + - uid: 833 + components: + - type: Transform + pos: 5.63115,-23.481327 + parent: 1 + - uid: 834 + components: + - type: Transform + pos: 1.6061028,-13.284962 + parent: 1 +... diff --git a/Resources/Migrations/eeMigration.yml b/Resources/Migrations/eeMigration.yml index ea2ac962144..a84314d3277 100644 --- a/Resources/Migrations/eeMigration.yml +++ b/Resources/Migrations/eeMigration.yml @@ -28,7 +28,7 @@ HyperlinkBookAtmos: BookAtmosDistro HyperlinkBookBartending: BookBartendersManual HyperlinkBookBotany: BookLeafLoversSecret HyperlinkBookChemistry: BookChemicalCompendium -HyperlinkBookCooking: BookChefGaming +HyperlinkBookCooking: BookHowToCookForFortySpaceman HyperlinkBookGlimmer: BookScientistsGuidebook HyperlinkBookHacking: BookEngineersHandbook HyperlinkBookMedical: BookMedicalReferenceBook @@ -117,3 +117,6 @@ SophicScribe: SophicScribeSpawner # 2024-11-16 GlassBoxLaser: null #Captain's Laser was moved to Loadouts. + +# 2024-12-22 +VendingMachineRestockSalvageEquipment: null diff --git a/Resources/Migrations/migration.yml b/Resources/Migrations/migration.yml index 3ac6b353a9b..7220520dab3 100644 --- a/Resources/Migrations/migration.yml +++ b/Resources/Migrations/migration.yml @@ -244,3 +244,70 @@ ReinforcementRadioSyndicateMonkeyNukeops: ReinforcementRadioSyndicateAncestorNuk # 2024-05-01 DrinkBottleGoldschlager: DrinkBottleGildlager + +# 2024-05-14 +soda_dispenser: SodaDispenser +chem_master: ChemMaster + +# 2024-05-21 +CrateJanitorExplosive: ClosetJanitorBombFilled + +# 2024-05-27 +DoorRemoteFirefight: null + +# 2024-06-03 +AirlockServiceCaptainLocked: AirlockCaptainLocked + +# 2024-06-15 +ClothingOuterCoatInspector: ClothingOuterCoatDetectiveLoadout + +# 2024-06-23 +FloorTileItemReinforced: PartRodMetal1 + + +#2024-06-25 +BookChefGaming: BookHowToCookForFortySpaceman + +#2024-06-29 +IntercomAssesmbly: IntercomAssembly + +# 2024-07-7 +SignScience1: SignScience +SignScience2: SignScience +SignXenobio2: SignXenobio +SignXenolab: SignXenobio +SignToxins2: SignToxins +SignMinerDock: SignShipDock +SignChemistry1: SignChem +SignChemistry2: SignChem +SignCourt: SignLawyer +SignAtmosMinsky: SignAtmos +SignDrones: SignMaterials +SignShield: null # what was this even for? +SignHydro2: SignHydro1 +SignHydro3: SignHydro1 + +# 2024-07-27 +LogicGate: LogicGateOr + +# 2024-08-11 +FoodTacoBeef: FoodTacoShell +FoodTacoChicken: FoodTacoShell +FoodTacoFish: FoodTacoShell +FoodTacoBeefSupreme: FoodTacoShell +FoodTacoChickenSupreme: FoodTacoShell +FoodTacoRat: FoodTacoShell + +FoodMeatHumanKebab: FoodKebabSkewer +FoodMeatLizardtailKebab: FoodKebabSkewer +FoodMeatRatKebab: FoodKebabSkewer +FoodMeatRatdoubleKebab: FoodKebabSkewer +FoodMeatSnakeKebab: FoodKebabSkewer +FoodMeatHawaiianKebab: FoodKebabSkewer +FoodMeatKebab: FoodKebabSkewer +FoodMeatFiestaKebab: FoodKebabSkewer + +# 2024-03-11 +ImprovisedExplosive: FireBomb +ImprovisedExplosiveEmpty: FireBombEmpty +ImprovisedExplosiveFuel: FireBombFuel diff --git a/Resources/Prototypes/Actions/ninja.yml b/Resources/Prototypes/Actions/ninja.yml index 51bfc33c494..3ad67844d66 100644 --- a/Resources/Prototypes/Actions/ninja.yml +++ b/Resources/Prototypes/Actions/ninja.yml @@ -2,7 +2,7 @@ - type: entity id: ActionToggleNinjaGloves name: Toggle ninja gloves - description: Toggles all glove actions on left click. Includes your doorjack, draining power, stunning enemies, downloading research and calling in a threat. + description: Toggles all glove actions on left click. Includes your doorjack, draining power, stunning enemies and hacking certain computers. categories: [ HideSpawnMenu ] components: - type: InstantAction @@ -23,7 +23,7 @@ state: icon itemIconStyle: NoItem priority: -10 - event: !type:CreateThrowingStarEvent {} + event: !type:CreateItemEvent {} - type: entity id: ActionRecallKatana @@ -64,7 +64,7 @@ # have to plan (un)cloaking ahead of time useDelay: 5 priority: -9 - event: !type:ToggleStealthEvent + event: !type:ToggleActionEvent # katana - type: entity @@ -78,6 +78,10 @@ sprite: Objects/Magic/magicactions.rsi state: blink itemIconStyle: NoItem + sound: + path: /Audio/Magic/blink.ogg + params: + volume: 5 priority: -12 event: !type:DashEvent checkCanAccess: false diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index f69d6a794a3..43aaef47364 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -327,6 +327,8 @@ checkCanInteract: false checkConsciousness: false event: !type:WakeActionEvent + startDelay: true + useDelay: 2 - type: entity id: ActionActivateHonkImplant diff --git a/Resources/Prototypes/Body/Organs/human.yml b/Resources/Prototypes/Body/Organs/human.yml index 606616038c1..788e582f714 100644 --- a/Resources/Prototypes/Body/Organs/human.yml +++ b/Resources/Prototypes/Body/Organs/human.yml @@ -69,7 +69,11 @@ - type: FlavorProfile flavors: - people - + - type: FoodSequenceElement + entries: + Burger: Brain + Taco: Brain + - type: entity id: OrganHumanEyes parent: BaseHumanOrgan diff --git a/Resources/Prototypes/Catalog/Bounties/bounties.yml b/Resources/Prototypes/Catalog/Bounties/bounties.yml index 3c1bd02fcfc..08bb2a14225 100644 --- a/Resources/Prototypes/Catalog/Bounties/bounties.yml +++ b/Resources/Prototypes/Catalog/Bounties/bounties.yml @@ -42,6 +42,9 @@ whitelist: tags: - Bread + blacklist: + tags: + - Slice - type: cargoBounty id: BountyCarrot @@ -309,6 +312,9 @@ whitelist: tags: - Pie + blacklist: + tags: + - Slice - type: cargoBounty id: BountyPrisonUniform @@ -530,6 +536,9 @@ whitelist: tags: - Meat + blacklist: + components: + - SliceableFood - type: cargoBounty id: BountyFruit @@ -541,6 +550,12 @@ whitelist: tags: - Fruit + blacklist: + tags: + - Slice + - Cake + - Pie + - Bread - type: cargoBounty id: BountyVegetable @@ -552,6 +567,12 @@ whitelist: tags: - Vegetable + blacklist: + tags: + - Slice + - Cake + - Pie + - Bread - type: cargoBounty id: BountyChili diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_botany.yml b/Resources/Prototypes/Catalog/Cargo/cargo_botany.yml index a6671ff0998..0a24240e7d0 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_botany.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_botany.yml @@ -34,7 +34,7 @@ sprite: Objects/Specific/Hydroponics/apple.rsi state: seed product: CrateHydroponicsSeeds - cost: 550 + cost: 600 category: cargoproduct-category-name-hydroponics group: market diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml b/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml index fed135cd6a6..38243337d34 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml @@ -66,7 +66,7 @@ sprite: Objects/Devices/flatpack.rsi state: solar-assembly-part product: CrateEngineeringSolar - cost: 500 + cost: 1250 category: cargoproduct-category-name-engineering group: market diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml b/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml index 8e2e1b4df58..937ee63b504 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml @@ -148,15 +148,15 @@ category: cargoproduct-category-name-service group: market -- type: cargoProduct - id: CrateVendingMachineRestockSalvageEquipment - icon: - sprite: Objects/Specific/Service/vending_machine_restock.rsi - state: base - product: CrateVendingMachineRestockSalvageEquipmentFilled - cost: 1000 - category: cargoproduct-category-name-engineering - group: market +#- type: cargoProduct # DeltaV: Salvage vendor doesn't have stock anymore +# id: CrateVendingMachineRestockSalvageEquipment +# icon: +# sprite: Objects/Specific/Service/vending_machine_restock.rsi +# state: base +# product: CrateVendingMachineRestockSalvageEquipmentFilled +# cost: 1000 +# category: cargoproduct-category-name-engineering +# group: market - type: cargoProduct id: CrateVendingMachineRestockSecTech diff --git a/Resources/Prototypes/Catalog/Fills/Books/bookshelf.yml b/Resources/Prototypes/Catalog/Fills/Books/bookshelf.yml index b52e8530085..09c7e165bfa 100644 --- a/Resources/Prototypes/Catalog/Fills/Books/bookshelf.yml +++ b/Resources/Prototypes/Catalog/Fills/Books/bookshelf.yml @@ -15,7 +15,7 @@ orGroup: BookPool - id: BookBartendersManual orGroup: BookPool - - id: BookChefGaming + - id: BookHowToCookForFortySpaceman orGroup: BookPool - id: BookLeafLoversSecret orGroup: BookPool diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml b/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml index fa7196f82fa..dcb5042edac 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml @@ -128,7 +128,54 @@ - BoxHug - type: entity - name: extended-capacity survival box + parent: BoxHug + id: BoxHugNitrogen + suffix: Emergency N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: EmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull + - type: Label + currentLabel: reagent-name-nitrogen + +- type: entity + parent: BoxSurvival + id: BoxMime + name: survival box + description: It's a box with basic internals inside. + suffix: Emergency + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: EmergencyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodBreadBaguette + - id: DrinkWaterBottleFull + +- type: entity + parent: BoxSurvival + id: BoxMimeNitrogen + suffix: Emergency N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: EmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodBreadBaguette + - id: DrinkWaterBottleFull + - type: Label + currentLabel: reagent-name-nitrogen + +- type: entity parent: BoxCardboard id: BoxSurvivalSyndicate description: It's a box with basic internals inside. This one is labelled to contain an extended-capacity tank. diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml b/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml index 7b5b05a49a5..6aab924c0ad 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml @@ -66,3 +66,21 @@ layers: - state: box_of_doom - state: throwing_knives + +- type: entity + parent: BoxCardboard + id: CombatBakeryKit + name: combat bakery kit + description: A kit of clandestine baked weapons. + components: + - type: Sprite + layers: + - state: box_of_doom + - state: france + - type: StorageFill + contents: + - id: WeaponCroissant + amount: 2 + - id: WeaponBaguette + - id: SyndicateMicrowaveMachineCircuitboard + - id: PaperWrittenCombatBakeryKit \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Crates/botany.yml b/Resources/Prototypes/Catalog/Fills/Crates/botany.yml index f10fd010f64..a708b02610e 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/botany.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/botany.yml @@ -95,3 +95,4 @@ - id: GrapeSeeds - id: WatermelonSeeds - id: PeaSeeds + - id: CherrySeeds diff --git a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml index 2ac099e7926..30eb29f68e0 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml @@ -120,12 +120,14 @@ id: CrateEngineeringSolar parent: CrateEngineering name: solar assembly crate - description: Parts for constructing solar panels and trackers. + description: A kit with solar flatpacks and glass to construct ten solar panels. components: - type: StorageFill contents: - id: SolarAssemblyFlatpack - amount: 6 + amount: 10 + - id: SheetGlass10 + amount: 2 - type: entity id: CrateEngineeringShuttle diff --git a/Resources/Prototypes/Catalog/Fills/Crates/service.yml b/Resources/Prototypes/Catalog/Fills/Crates/service.yml index 35e66ac4d35..3e38b6c4efc 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/service.yml @@ -162,7 +162,7 @@ - id: BookSpaceEncyclopedia - id: BookTheBookOfControl - id: BookBartendersManual - - id: BookChefGaming + - id: BookHowToCookForFortySpaceman - id: BookLeafLoversSecret - id: BookEngineersHandbook - id: BookScientistsGuidebook diff --git a/Resources/Prototypes/Catalog/Fills/Crates/syndicate.yml b/Resources/Prototypes/Catalog/Fills/Crates/syndicate.yml index 3f9e909c809..ba97af39250 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/syndicate.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/syndicate.yml @@ -1,6 +1,6 @@ - type: entity id: CrateSyndicateSurplusBundle - parent: CrateSyndicate + parent: [ CrateSyndicate, StorePresetUplink ] name: Syndicate surplus crate description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good. components: @@ -24,7 +24,7 @@ - type: entity id: CrateSyndicateSuperSurplusBundle - parent: CrateSyndicate + parent: [ CrateSyndicate, StorePresetUplink ] name: Syndicate super surplus crate description: Contains 125 telecrystals worth of completely random Syndicate items. components: diff --git a/Resources/Prototypes/Catalog/Fills/Crates/vending.yml b/Resources/Prototypes/Catalog/Fills/Crates/vending.yml index d54e103a70f..fd4e4fd2b43 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/vending.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/vending.yml @@ -140,15 +140,15 @@ - id: VendingMachineRestockRobustSoftdrinks amount: 2 -- type: entity - id: CrateVendingMachineRestockSalvageEquipmentFilled - parent: CrateGenericSteel - name: Salvage restock crate - description: Contains a restock box for the salvage vendor. - components: - - type: StorageFill - contents: - - id: VendingMachineRestockSalvageEquipment +#- type: entity # DeltaV: Salvage vendor doesn't have stock anymore +# id: CrateVendingMachineRestockSalvageEquipmentFilled +# parent: CrateGenericSteel +# name: Salvage restock crate +# description: Contains a restock box for the salvage vendor. +# components: +# - type: StorageFill +# contents: +# - id: VendingMachineRestockSalvageEquipment - type: entity id: CrateVendingMachineRestockSecTechFilled diff --git a/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml b/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml index f1b259f829c..6b6929c038a 100644 --- a/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml +++ b/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml @@ -71,7 +71,7 @@ contents: - id: SyringePhalanximine - id: RadAutoInjector - - id: EmergencyMedipen + - id: PillCanisterPotassiumIodide - id: PillCanisterHyronalin - type: entity diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml b/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml index 4ca95adff84..02eee486913 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml @@ -36,12 +36,13 @@ components: - type: StorageFill contents: -# Currently do not function as 'true' mesons, so they're useless for salvagers. -# - id: ClothingEyesGlassesMeson + - id: ClothingEyesGlassesGarMeson - id: ClothingBeltUtilityFilled - id: SurvivalKnife - id: HandheldGPSBasic - id: RadioHandheld + - id: FlashlightSeclite + - id: Pickaxe - id: SeismicCharge amount: 2 - id: ClothingShoesBootsWinterMiner #Delta V: Add departmental winter boots diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index 4ba27a4bc0a..fec4f3ffcf0 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -22,6 +22,7 @@ - id: SpaceCashLuckyBill # DeltaV - LO steal objective, see Resources/Prototypes/DeltaV/Entities/Objects/Misc/first_bill.yml - id: BoxPDACargo # Delta-V - id: QuartermasterIDCard # Delta-V + - id: AstroNavCartridge - id: LunchboxCommandFilledRandom # Delta-V Lunchboxes! prob: 0.3 @@ -210,6 +211,7 @@ - id: BoxPDAMedical # Delta-V - id: ClothingBeltMilitaryWebbingCMO # DeltaV - add webbing for CMO. ON THIS STATION, IT'S DRIP OR [die], CAPTAIN! - id: CMOIDCard # Delta-V + - id: MedTekCartridge # Delta-v - id: LunchboxCommandFilledRandom # Delta-V Lunchboxes! prob: 0.3 - id: MedicalBiofabMachineBoard # Shitmed Change @@ -283,7 +285,6 @@ - id: JetpackSecurityFilled - id: BoxEncryptionKeySecurity - id: HoloprojectorSecurity - - id: BookSecretDocuments - id: BoxPDASecurity # Delta-V - id: HoSIDCard # Delta-V - id: LunchboxCommandFilledRandom # Delta-V Lunchboxes! @@ -311,7 +312,6 @@ - id: SecurityTechFabCircuitboard - id: BoxEncryptionKeySecurity - id: HoloprojectorSecurity - - id: BookSecretDocuments - id: BoxPDASecurity # Delta-V - id: HoSIDCard # Delta-V - id: LunchboxCommandFilledRandom # Delta-V Lunchboxes! @@ -331,4 +331,4 @@ - id: JetpackBlue - id: SpaceCash1000 - id: BeachBall - - id: BikeHorn + - id: BikeHorn \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml index 052ce95be2f..756f295d129 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml @@ -44,7 +44,7 @@ prob: 0.2 - !type:EntSelector id: WeaponFlareGun - prob: 0.05 + prob: 0.1 - !type:EntSelector id: BoxMRE prob: 0.1 diff --git a/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml b/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml index 4893fa2557f..7f2268953bf 100644 --- a/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml +++ b/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml @@ -23,3 +23,12 @@ type: PaperBoundUserInterface - type: Paper content: book-text-holoparasite-info + +- type: entity + id: PaperWrittenCombatBakeryKit + name: "combat bakery kit instructions" + description: "Eat note after reading." + parent: Paper + components: + - type: Paper + content: book-text-combat-bakery-kit \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/chefvend.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/chefvend.yml index bc6ddc96fb8..7343233b6b9 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/chefvend.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/chefvend.yml @@ -17,7 +17,7 @@ FoodContainerEgg: 1 DrinkMilkCarton: 2 DrinkSoyMilkCarton: 1 - FoodButter: 4 + FoodButter: 3 FoodCheese: 1 FoodMeat: 6 LunchboxGenericFilledRandom: 5 # Delta-V Adds Lunchbox diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml index b5665df37ba..a1184748f1b 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml @@ -9,6 +9,7 @@ Floodlight: 2 HandheldGPSBasic: 2 RadioHandheld: 2 + WeaponGrapplingGun: 4 WeaponProtoKineticAccelerator: 4 SeismicCharge: 2 FultonBeacon: 1 diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/seeds.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/seeds.yml index 05e3ae0eb0e..90379550d8a 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/seeds.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/seeds.yml @@ -8,6 +8,7 @@ CarrotSeeds: 5 CabbageSeeds: 5 ChanterelleSeeds: 5 + CherrySeeds: 5 ChiliSeeds: 5 CornSeeds: 5 EggplantSeeds: 5 diff --git a/Resources/Prototypes/Catalog/discount_categories.yml b/Resources/Prototypes/Catalog/discount_categories.yml new file mode 100644 index 00000000000..5c512e9009b --- /dev/null +++ b/Resources/Prototypes/Catalog/discount_categories.yml @@ -0,0 +1,13 @@ +- type: discountCategory + id: rareDiscounts # Dirty-cheap items that are rarely used and can be discounted to 0-ish cost to encourage usage. + weight: 18 + maxItems: 2 + +- type: discountCategory + id: usualDiscounts # Cheap items that are used not very often. + weight: 80 + +- type: discountCategory + id: veryRareDiscounts # Casually used items that are widely used but can be (rarely) discounted for epic lulz. + weight: 2 + maxItems: 1 diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index fdfb4ed33a5..0bc47d74eb7 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -6,22 +6,26 @@ name: uplink-pistol-viper-name description: uplink-pistol-viper-desc productEntity: WeaponPistolViper + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 3 categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry - type: listing id: UplinkRevolverPython name: uplink-revolver-python-name description: uplink-revolver-python-desc productEntity: WeaponRevolverPythonAP + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 8 # Originally was 13 TC but was not used due to high cost categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry # Inbuilt suppressor so it's sneaky + more expensive. - type: listing @@ -29,11 +33,13 @@ name: uplink-pistol-cobra-name description: uplink-pistol-cobra-desc productEntity: WeaponPistolCobra + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry # Poor accuracy, slow to fire, cheap option - type: listing @@ -44,20 +50,21 @@ cost: Telecrystal: 1 categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry - type: listing id: UplinkEsword name: uplink-esword-name description: uplink-esword-desc icon: { sprite: /Textures/Objects/Weapons/Melee/e_sword.rsi, state: icon } + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 4 productEntity: EnergySword cost: Telecrystal: 8 categories: - - UplinkWeapons - saleLimit: 2 + - UplinkWeaponry - type: listing id: UplinkEnergyDagger @@ -65,11 +72,13 @@ description: uplink-edagger-desc icon: { sprite: /Textures/Objects/Weapons/Melee/e_dagger.rsi, state: icon } productEntity: EnergyDaggerBox + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry - type: listing id: UplinkThrowingKnivesKit @@ -77,50 +86,134 @@ description: uplink-knives-kit-desc icon: { sprite: /Textures/Objects/Storage/boxicons.rsi, state: throwing_knives } productEntity: ThrowingKnivesKit + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: - - UplinkWeapons + - UplinkWeaponry + - type: listing id: UplinkGlovesNorthStar name: uplink-gloves-north-star-name description: uplink-gloves-north-star-desc productEntity: ClothingHandsGlovesNorthStar + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 8 categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry - type: listing id: UplinkDisposableTurret name: uplink-disposable-turret-name description: uplink-disposable-turret-desc productEntity: ToolboxElectricalTurretFilled + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: - - UplinkWeapons + - UplinkWeaponry conditions: - !type:StoreWhitelistCondition blacklist: tags: - NukeOpsUplink - saleLimit: 2 - type: listing - id: BaseBallBatHomeRun - name: uplink-home-run-bat-name - description: uplink-home-run-bat-desc - productEntity: BaseBallBatHomeRun - icon: - entity: BaseBallBatHomeRun + id: UplinkEshield + name: uplink-eshield-name + description: uplink-eshield-desc + icon: { sprite: /Textures/Objects/Weapons/Melee/e_shield.rsi, state: eshield-on } + productEntity: EnergyShield + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 16 categories: - - UplinkWeapons - saleLimit: 1 + - UplinkWeaponry + conditions: + - !type:StoreWhitelistCondition + whitelist: + tags: + - NukeOpsUplink + +- type: listing + id: UplinkSniperBundle + name: uplink-sniper-bundle-name + description: uplink-sniper-bundle-desc + icon: { sprite: /Textures/Objects/Weapons/Guns/Snipers/heavy_sniper.rsi, state: base } + productEntity: BriefcaseSyndieSniperBundleFilled + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 6 + cost: + Telecrystal: 12 + categories: + - UplinkWeaponry + +- type: listing + id: UplinkC20RBundle + name: uplink-c20r-bundle-name + description: uplink-c20r-bundle-desc + icon: { sprite: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateFilledSMG + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 10 + cost: + Telecrystal: 17 + categories: + - UplinkWeaponry + +- type: listing + id: UplinkBulldogBundle + name: uplink-buldog-bundle-name + description: uplink-buldog-bundle-desc + icon: { sprite: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateFilledShotgun + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 12 + cost: + Telecrystal: 20 + categories: + - UplinkWeaponry + +- type: listing + id: UplinkGrenadeLauncherBundle + name: uplink-grenade-launcher-bundle-name + description: uplink-grenade-launcher-bundle-desc + icon: { sprite: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 20 + cost: + Telecrystal: 25 + categories: + - UplinkWeaponry + +- type: listing + id: UplinkL6SawBundle + name: uplink-l6-saw-bundle-name + description: uplink-l6-saw-bundle-desc + icon: { sprite: /Textures/Objects/Weapons/Guns/LMGs/l6.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateFilledLMG + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 24 + cost: + Telecrystal: 30 + categories: + - UplinkWeaponry # Explosives @@ -129,6 +222,9 @@ name: uplink-explosive-grenade-name description: uplink-explosive-grenade-desc productEntity: ExGrenade + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: @@ -159,6 +255,9 @@ name: uplink-mini-bomb-name description: uplink-mini-bomb-desc productEntity: SyndieMiniBomb + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: @@ -169,6 +268,9 @@ name: uplink-supermatter-grenade-name description: uplink-supermatter-grenade-desc productEntity: SupermatterGrenade + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: @@ -179,6 +281,9 @@ name: uplink-whitehole-grenade-name description: uplink-whitehole-grenade-desc productEntity: WhiteholeGrenade + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 3 categories: @@ -189,6 +294,9 @@ name: uplink-penguin-grenade-name description: uplink-penguin-grenade-desc productEntity: MobGrenadePenguin + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 5 categories: @@ -204,6 +312,9 @@ name: uplink-c4-name description: uplink-c4-desc productEntity: C4 + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: @@ -214,6 +325,9 @@ name: uplink-grenadier-rig-name description: uplink-grenadier-rig-desc productEntity: ClothingBeltMilitaryWebbingGrenadeFilled + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 6 cost: Telecrystal: 12 categories: @@ -223,24 +337,28 @@ whitelist: tags: - NukeOpsUplink - saleLimit: 1 - type: listing id: UplinkC4Bundle name: uplink-c4-bundle-name description: uplink-c4-bundle-desc productEntity: ClothingBackpackDuffelSyndicateC4tBundle + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 8 cost: - Telecrystal: 12 #you're buying bulk so its a 25% discount + Telecrystal: 12 #you're buying bulk so its a 25% discount, so no additional random discount over it categories: - UplinkExplosives - saleLimit: 1 - type: listing id: UplinkEmpGrenade name: uplink-emp-grenade-name description: uplink-emp-grenade-desc productEntity: EmpGrenade + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: @@ -252,6 +370,9 @@ description: uplink-exploding-pen-desc icon: { sprite: /Textures/Objects/Misc/bureaucracy.rsi, state: pen } productEntity: PenExplodingBox + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: @@ -272,7 +393,6 @@ blacklist: tags: - NukeOpsUplink - saleLimit: 1 - type: listing id: UplinkSyndicateBombNukie @@ -288,13 +408,15 @@ whitelist: tags: - NukeOpsUplink - saleLimit: 1 - type: listing id: UplinkClusterGrenade name: uplink-cluster-grenade-name description: uplink-cluster-grenade-desc productEntity: ClusterGrenade + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 5 cost: Telecrystal: 8 categories: @@ -305,6 +427,9 @@ name: uplink-shrapnel-grenade-name description: uplink-shrapnel-grenade-desc productEntity: GrenadeShrapnel + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: @@ -315,11 +440,27 @@ name: uplink-incendiary-grenade-name description: uplink-incendiary-grenade-desc productEntity: GrenadeIncendiary + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - UplinkExplosives +- type: listing + id: UplinkEmpKit + name: uplink-emp-kit-name + description: uplink-emp-kit-desc + productEntity: ElectricalDisruptionKit + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 4 + cost: + Telecrystal: 6 + categories: + - UplinkExplosives + # Ammo - type: listing @@ -391,7 +532,68 @@ categories: - UplinkAmmo -#Utility +- type: listing + id: UplinkAmmoBundle + name: uplink-ammo-bundle-name + description: uplink-ammo-bundle-desc + productEntity: ClothingBackpackDuffelSyndicateAmmoFilled + cost: + Telecrystal: 15 + categories: + - UplinkAmmo + conditions: + - !type:StoreWhitelistCondition + whitelist: + tags: + - NukeOpsUplink + - !type:BuyerWhitelistCondition + blacklist: + components: + - SurplusBundle + +#Chemicals + +- type: listing + id: UplinkHypopen + name: uplink-hypopen-name + description: uplink-hypopen-desc + icon: { sprite: /Textures/Objects/Misc/bureaucracy.rsi, state: pen } + productEntity: HypopenBox + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 4 + cost: + Telecrystal: 6 + categories: + - UplinkChemicals + +- type: listing + id: UplinkHypoDart + name: uplink-hypodart-name + description: uplink-hypodart-desc + icon: { sprite: /Textures/Objects/Fun/Darts/dart_red.rsi, state: icon } + productEntity: HypoDartBox + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 1 + cost: + Telecrystal: 2 + categories: + - UplinkChemicals + +- type: listing + id: UplinkChemistryKitBundle + name: uplink-chemistry-kit-name + description: uplink-chemistry-kit-desc + icon: { sprite: /Textures/Objects/Storage/boxicons.rsi, state: vials } + productEntity: ChemicalSynthesisKit + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 + cost: + Telecrystal: 4 + categories: + - UplinkChemicals - type: listing id: UplinkZombieBundle @@ -412,68 +614,80 @@ blacklist: components: - SurplusBundle - saleLimit: 1 - type: listing id: UplinkNocturineChemistryBottle name: uplink-nocturine-chemistry-bottle-name description: uplink-nocturine-chemistry-bottle-desc productEntity: NocturineChemistryBottle + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: - UplinkChemicals - saleLimit: 1 - type: listing id: UplinkCombatMedkit name: uplink-combat-medkit-name description: uplink-combat-medkit-desc productEntity: MedkitCombatFilled + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 5 categories: - UplinkChemicals - saleLimit: 1 - type: listing id: UplinkCombatMedipen name: uplink-combat-medipen-name description: uplink-combat-medipen-desc productEntity: CombatMedipen + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - UplinkChemicals - saleLimit: 1 - type: listing id: UplinkStimpack name: uplink-stimpack-name description: uplink-stimpack-desc productEntity: Stimpack + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - UplinkChemicals - saleLimit: 1 - type: listing id: UplinkStimkit name: uplink-stimkit-name description: uplink-stimkit-desc productEntity: StimkitFilled + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 8 cost: Telecrystal: 12 categories: - UplinkChemicals - saleLimit: 1 - type: listing id: UplinkCigarettes name: uplink-cigarettes-name description: uplink-cigarettes-desc productEntity: CigPackSyndicate + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: @@ -484,6 +698,9 @@ name: uplink-meds-bundle-name description: uplink-meds-bundle-desc productEntity: ClothingBackpackDuffelSyndicateMedicalBundleFilled + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 12 cost: Telecrystal: 20 categories: @@ -497,7 +714,6 @@ blacklist: components: - SurplusBundle - saleLimit: 1 # Deception @@ -506,30 +722,53 @@ name: uplink-agent-id-card-name description: uplink-agent-id-card-desc productEntity: AgentIDCard + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 3 categories: - - UplinkUtility + - UplinkDeception - type: listing id: UplinkStealthBox name: uplink-stealth-box-name description: uplink-stealth-box-desc productEntity: StealthBox + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 5 categories: - - UplinkUtility + - UplinkDeception - type: listing id: UplinkChameleonProjector name: uplink-chameleon-projector-name description: uplink-chameleon-projector-desc productEntity: ChameleonProjector + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 7 categories: - - UplinkUtility + - UplinkDeception + +- type: listing + id: UplinkHeadsetEncryptionKey + name: uplink-encryption-key-name + description: uplink-encryption-key-desc + icon: { sprite: /Textures/Objects/Devices/encryption_keys.rsi, state: synd_label } + productEntity: BoxEncryptionKeySyndie # Two for the price of one + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 + cost: + Telecrystal: 2 + categories: + - UplinkDeception - type: listing id: UplinkCyberpen @@ -539,7 +778,7 @@ cost: Telecrystal: 1 categories: - - UplinkUtility + - UplinkDeception - type: listing id: UplinkDecoyDisk @@ -549,27 +788,33 @@ cost: Telecrystal: 1 categories: - - UplinkUtility + - UplinkDeception - type: listing id: UplinkUltrabrightLantern name: uplink-ultrabright-lantern-name description: uplink-ultrabright-lantern-desc productEntity: LanternFlash + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: - - UplinkUtility + - UplinkDeception - type: listing id: UplinkBribe name: uplink-bribe-name description: uplink-bribe-desc productEntity: BriefcaseSyndieLobbyingBundleFilled + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - - UplinkUtility + - UplinkDeception # - type: listing # id: UplinkGigacancerScanner @@ -582,24 +827,72 @@ # - UplinkUtility - type: listing - id: UplinkHolster - name: uplink-holster-name - description: uplink-holster-desc - productEntity: ClothingBeltSyndieHolster + id: UplinkDecoyKit + name: uplink-decoy-kit-name + description: uplink-decoy-kit-desc + icon: { sprite: /Textures/Objects/Tools/Decoys/operative_decoy.rsi, state: folded } + productEntity: ClothingBackpackDuffelSyndicateDecoyKitFilled + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 1 categories: - - UplinkUtility + - UplinkDeception + +- type: listing + id: UplinkSyndicateBombFake + name: uplink-exploding-syndicate-bomb-fake-name + description: uplink-exploding-syndicate-bomb-fake-desc + productEntity: SyndicateBombFake + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 + cost: + Telecrystal: 4 + categories: + - UplinkDeception + +# Disruption - type: listing id: UplinkEmag name: uplink-emag-name description: uplink-emag-desc productEntity: Emag + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 5 cost: Telecrystal: 8 categories: - - UplinkUtility + - UplinkDisruption + +- type: listing + id: UplinkRadioJammer + name: uplink-radio-jammer-name + description: uplink-radio-jammer-desc + productEntity: RadioJammer + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 + cost: + Telecrystal: 4 + categories: + - UplinkDisruption + +- type: listing + id: UplinkSyndicateWeaponModule + name: uplink-syndicate-weapon-module-name + description: uplink-syndicate-weapon-module-desc + productEntity: BorgModuleSyndicateWeapon + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 + cost: + Telecrystal: 5 + categories: + - UplinkDisruption - type: listing id: UplinkSyndicateMartyrModule @@ -607,82 +900,211 @@ description: uplink-syndicate-martyr-module-desc productEntity: BorgModuleMartyr icon: { sprite: /Textures/Objects/Specific/Robotics/borgmodule.rsi, state: syndicateborgbomb } + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - UplinkDisruption - type: listing - id: UplinkJetpack - name: uplink-black-jetpack-name - description: uplink-black-jetpack-desc - productEntity: JetpackBlackFilled + id: UplinkSoapSyndie + name: uplink-soap-name + description: uplink-soap-desc + productEntity: SoapSyndie cost: - Telecrystal: 2 + Telecrystal: 1 categories: - - UplinkUtility + - UplinkDisruption - type: listing - id: UplinkReinforcementRadioSyndicate - name: uplink-reinforcement-radio-name - description: uplink-reinforcement-radio-desc - productEntity: ReinforcementRadioSyndicate - icon: { sprite: Objects/Devices/communication.rsi, state: radio } + id: UplinkSlipocalypseClusterSoap + name: uplink-slipocalypse-clustersoap-name + description: uplink-slipocalypse-clustersoap-desc + productEntity: SlipocalypseClusterSoap + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: - Telecrystal: 16 + Telecrystal: 2 categories: - - UplinkUtility - conditions: - - !type:StoreWhitelistCondition - blacklist: - tags: - - NukeOpsUplink - saleLimit: 1 + - UplinkDisruption - type: listing - id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns an agent with the NukeOperative component. - name: uplink-reinforcement-radio-name - description: uplink-reinforcement-radio-desc - productEntity: ReinforcementRadioSyndicateNukeops - icon: { sprite: Objects/Devices/communication.rsi, state: radio } + id: UplinkToolbox + name: uplink-toolbox-name + description: uplink-toolbox-desc + productEntity: ToolboxSyndicateFilled + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: - Telecrystal: 16 + Telecrystal: 2 categories: - - UplinkUtility - conditions: - - !type:StoreWhitelistCondition - whitelist: + - UplinkDisruption + +- type: listing + id: UplinkSyndicateJawsOfLife + name: uplink-syndicate-jaws-of-life-name + description: uplink-syndicate-jaws-of-life-desc + productEntity: SyndicateJawsOfLife + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 + cost: + Telecrystal: 2 + categories: + - UplinkDisruption + +- type: listing + id: UplinkDuffelSurgery + name: uplink-duffel-surgery-name + description: uplink-duffel-surgery-desc + productEntity: ClothingBackpackDuffelSyndicateFilledMedical + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 + cost: + Telecrystal: 4 + categories: + - UplinkDisruption + +- type: listing + id: UplinkPowerSink + name: uplink-power-sink-name + description: uplink-power-sink-desc + productEntity: PowerSink + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 4 + cost: + Telecrystal: 8 + categories: + - UplinkDisruption + conditions: + - !type:BuyerWhitelistCondition + blacklist: + components: + - SurplusBundle + +- type: listing + id: UplinkSuperSurplusBundle + name: uplink-super-surplus-bundle-name + description: uplink-super-surplus-bundle-desc + productEntity: CrateSyndicateSuperSurplusBundle + cost: + Telecrystal: 40 + categories: + - UplinkDisruption + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - NukeOpsUplink + - !type:BuyerWhitelistCondition + blacklist: + components: + - SurplusBundle + +- type: listing + id: UplinkSingarityBeacon + name: uplink-singularity-beacon-name + description: uplink-singularity-beacon-desc + productEntity: SingularityBeacon + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 4 + cost: + Telecrystal: 12 + categories: + - UplinkDisruption + +# Allies + +- type: listing + id: UplinkHoloparaKit + name: uplink-holopara-kit-name + description: uplink-holopara-kit-desc + icon: { sprite: /Textures/Objects/Misc/guardian_info.rsi, state: icon } + productEntity: BoxHoloparasite + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 8 + cost: + Telecrystal: 14 + categories: + - UplinkAllies + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - NukeOpsUplink + +- type: listing + id: UplinkReinforcementRadioSyndicate + name: uplink-reinforcement-radio-name + description: uplink-reinforcement-radio-desc + productEntity: ReinforcementRadioSyndicate + icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-urist } + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 7 + cost: + Telecrystal: 16 + categories: + - UplinkAllies + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - NukeOpsUplink + +- type: listing + id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns an agent with the NukeOperative component. + name: uplink-reinforcement-radio-name + description: uplink-reinforcement-radio-desc + productEntity: ReinforcementRadioSyndicateNukeops + icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-urist } + cost: + Telecrystal: 16 + categories: + - UplinkAllies + conditions: + - !type:StoreWhitelistCondition + whitelist: tags: - NukeOpsUplink - saleLimit: 1 - type: listing id: UplinkReinforcementRadioSyndicateCyborgAssault name: uplink-reinforcement-radio-cyborg-assault-name description: uplink-reinforcement-radio-cyborg-assault-desc productEntity: ReinforcementRadioSyndicateCyborgAssault - icon: { sprite: Mobs/Silicon/chassis.rsi, state: synd_sec } + icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-borg-assault } cost: Telecrystal: 65 categories: - - UplinkUtility + - UplinkAllies conditions: - !type:StoreWhitelistCondition whitelist: tags: - NukeOpsUplink - saleLimit: 1 - type: listing id: UplinkReinforcementRadioSyndicateAncestor name: uplink-reinforcement-radio-ancestor-name description: uplink-reinforcement-radio-ancestor-desc productEntity: ReinforcementRadioSyndicateAncestor - icon: { sprite: Objects/Devices/communication.rsi, state: radio } + icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-ancestor } + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 8 categories: - - UplinkUtility + - UplinkAllies conditions: - !type:StoreWhitelistCondition blacklist: @@ -694,101 +1116,47 @@ name: uplink-reinforcement-radio-ancestor-name description: uplink-reinforcement-radio-ancestor-desc productEntity: ReinforcementRadioSyndicateAncestorNukeops - icon: { sprite: Objects/Devices/communication.rsi, state: radio } + icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-ancestor } + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 6 categories: - - UplinkUtility + - UplinkAllies conditions: - !type:StoreWhitelistCondition whitelist: tags: - NukeOpsUplink - saleLimit: 1 - -- type: listing - id: UplinkHeadsetEncryptionKey - name: uplink-encryption-key-name - description: uplink-encryption-key-desc - icon: { sprite: /Textures/Objects/Devices/encryption_keys.rsi, state: synd_label } - productEntity: BoxEncryptionKeySyndie # Two for the price of one - cost: - Telecrystal: 2 - categories: - - UplinkUtility - type: listing - id: UplinkBinaryTranslatorKey - name: uplink-binary-translator-key-name - description: uplink-binary-translator-key-desc - icon: { sprite: /Textures/Objects/Devices/encryption_keys.rsi, state: rd_label } - productEntity: EncryptionKeyBinary - cost: + id: UplinkCarpDehydrated + name: uplink-carp-dehydrated-name + description: uplink-carp-dehydrated-desc + productEntity: DehydratedSpaceCarp + discountCategory: rareDiscounts + discountDownTo: Telecrystal: 1 - categories: - - UplinkUtility - -- type: listing - id: UplinkHypopen - name: uplink-hypopen-name - description: uplink-hypopen-desc - icon: { sprite: /Textures/Objects/Misc/bureaucracy.rsi, state: pen } - productEntity: HypopenBox - cost: - Telecrystal: 6 - categories: - - UplinkUtility - -- type: listing - id: UplinkVoiceMask - name: uplink-voice-mask-name - description: uplink-voice-mask-desc - productEntity: ClothingMaskGasVoiceChameleon - cost: - Telecrystal: 2 - categories: - - UplinkUtility - -- type: listing - id: UplinkClothingEyesHudSyndicate - name: uplink-clothing-eyes-hud-syndicate-name - description: uplink-clothing-eyes-hud-syndicate-desc - productEntity: ClothingEyesHudSyndicate cost: Telecrystal: 2 categories: - - UplinkUtility - -- type: listing - id: UplinkRadioJammer - name: uplink-radio-jammer-name - description: uplink-radio-jammer-desc - productEntity: RadioJammer - cost: - Telecrystal: 4 - categories: - - UplinkUtility + - UplinkAllies - type: listing - id: UplinkHypoDart - name: uplink-hypodart-name - description: uplink-hypodart-desc - icon: { sprite: /Textures/Objects/Fun/Darts/dart_red.rsi, state: icon } - productEntity: HypoDartBox + id: UplinkMobCatMicrobomb + name: uplink-mobcat-microbomb-name + description: uplink-mobcat-microbomb-desc + icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-syndicat } + productEntity: ReinforcementRadioSyndicateSyndiCat + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: - Telecrystal: 2 + Telecrystal: 6 categories: - - UplinkUtility + - UplinkAllies -- type: listing - id: UplinkSyndicateWeaponModule - name: uplink-syndicate-weapon-module-name - description: uplink-syndicate-weapon-module-desc - productEntity: BorgModuleSyndicateWeapon - cost: - Telecrystal: 5 - categories: - - UplinkUtility # Implants - type: listing @@ -797,6 +1165,9 @@ description: uplink-storage-implanter-desc icon: { sprite: /Textures/Clothing/Back/Backpacks/backpack.rsi, state: icon } productEntity: StorageImplanter + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 8 categories: @@ -813,6 +1184,9 @@ description: uplink-freedom-implanter-desc icon: { sprite: /Textures/Actions/Implants/implants.rsi, state: freedom } productEntity: FreedomImplanter + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 5 categories: @@ -824,6 +1198,9 @@ description: uplink-scram-implanter-desc icon: { sprite: /Textures/Structures/Specific/anomaly.rsi, state: anom4 } productEntity: ScramImplanter + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 4 cost: Telecrystal: 6 # it's a gamble that may kill you easily so 6 TC per 2 uses, second one more of a backup categories: @@ -835,6 +1212,9 @@ description: uplink-dna-scrambler-implanter-desc icon: { sprite: /Textures/Mobs/Species/Human/parts.rsi, state: full } productEntity: DnaScramblerImplanter + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 5 categories: @@ -846,6 +1226,9 @@ description: uplink-emp-implanter-desc icon: { sprite: /Textures/Objects/Magic/magicactions.rsi, state: shield } productEntity: EmpImplanter + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: @@ -913,6 +1296,9 @@ description: uplink-uplink-implanter-desc icon: { sprite: /Textures/Objects/Devices/communication.rsi, state: radio } productEntity: UplinkImplanter + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: @@ -922,7 +1308,6 @@ blacklist: tags: - NukeOpsUplink - saleBlacklist: true - type: listing id: UplinkDeathRattle @@ -943,153 +1328,153 @@ components: - SurplusBundle -# Bundles +# Wearables - type: listing - id: Observationkit - name: uplink-observation-kit-name - description: uplink-observation-kit-desc - icon: { sprite: /Textures/Objects/Storage/boxicons.rsi, state: tracks } - productEntity: Observationskit + id: UplinkJetpack + name: uplink-black-jetpack-name + description: uplink-black-jetpack-desc + productEntity: JetpackBlackFilled + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 4 categories: - - UplinkBundles + - UplinkWearables - type: listing - id: UplinkEmpKit - name: uplink-emp-kit-name - description: uplink-emp-kit-desc - productEntity: ElectricalDisruptionKit + id: UplinkVoiceMask + name: uplink-voice-mask-name + description: uplink-voice-mask-desc + productEntity: ClothingMaskGasVoiceChameleon + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 6 categories: - - UplinkBundles + - UplinkWearables + +# Disruption - type: listing - id: UplinkSyndicateBombFake - name: uplink-exploding-syndicate-bomb-fake-name - description: uplink-exploding-syndicate-bomb-fake-desc - productEntity: SyndicateBombFake + id: UplinkChameleon + name: uplink-chameleon-name + description: uplink-chameleon-desc + productEntity: ClothingBackpackChameleonFill + icon: { sprite: /Textures/Clothing/Uniforms/Jumpsuit/rainbow.rsi, state: icon } + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - - UplinkUtility - -# Disruption + - UplinkWearables - type: listing - id: UplinkAmmoBundle - name: uplink-ammo-bundle-name - description: uplink-ammo-bundle-desc - productEntity: ClothingBackpackDuffelSyndicateAmmoFilled + id: UplinkClothingNoSlipsShoes + name: uplink-clothing-no-slips-shoes-name + description: uplink-clothing-no-slips-shoes-desc + productEntity: ClothingShoesChameleonNoSlips + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 1 cost: - Telecrystal: 15 + Telecrystal: 2 categories: - - UplinkBundles - conditions: - - !type:StoreWhitelistCondition - whitelist: - tags: - - NukeOpsUplink - - !type:BuyerWhitelistCondition - blacklist: - components: - - SurplusBundle - saleLimit: 1 + - UplinkWearables - type: listing - id: UplinkChemistryKitBundle - name: uplink-chemistry-kit-name - description: uplink-chemistry-kit-desc - icon: { sprite: /Textures/Objects/Storage/boxicons.rsi, state: vials } - productEntity: ChemicalSynthesisKit + id: UplinkgClothingThievingGloves + name: uplink-clothing-thieving-gloves-name + description: uplink-clothing-thieving-gloves-desc + productEntity: ThievingGloves + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: - - UplinkBundles + - UplinkWearables - type: listing - id: UplinkDecoyKit - name: uplink-decoy-kit-name - description: uplink-decoy-kit-desc - icon: { sprite: /Textures/Objects/Tools/Decoys/operative_decoy.rsi, state: folded } - productEntity: ClothingBackpackDuffelSyndicateDecoyKitFilled + id: UplinkClothingOuterVestWeb + name: uplink-clothing-outer-vest-web-name + description: uplink-clothing-outer-vest-web-desc + productEntity: ClothingOuterVestWeb + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 cost: - Telecrystal: 6 + Telecrystal: 3 categories: - - UplinkBundles - + - UplinkWearables - type: listing - id: UplinkSniperBundle - name: uplink-sniper-bundle-name - description: uplink-sniper-bundle-desc - icon: { sprite: /Textures/Objects/Weapons/Guns/Snipers/heavy_sniper.rsi, state: base } - productEntity: BriefcaseSyndieSniperBundleFilled - cost: - Telecrystal: 12 - categories: - - UplinkBundles - saleLimit: 1 - -- type: listing - id: UplinkC20RBundle - name: uplink-c20r-bundle-name - description: uplink-c20r-bundle-desc - icon: { sprite: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateFilledSMG + id: UplinkClothingShoesBootsMagSyndie + name: uplink-clothing-shoes-boots-mag-syndie-name + description: uplink-clothing-shoes-boots-mag-syndie-desc + productEntity: ClothingShoesBootsMagSyndie + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: - Telecrystal: 17 + Telecrystal: 4 categories: - - UplinkBundles - saleLimit: 1 + - UplinkWearables - type: listing - id: UplinkBulldogBundle - name: uplink-buldog-bundle-name - description: uplink-buldog-bundle-desc - icon: { sprite: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateFilledShotgun + id: UplinkEVASyndie + name: uplink-eva-syndie-name + description: uplink-eva-syndie-desc + icon: { sprite: /Textures/Clothing/OuterClothing/Suits/eva_syndicate.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateEVABundle + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: - Telecrystal: 20 + Telecrystal: 2 categories: - - UplinkBundles - saleLimit: 1 + - UplinkWearables - type: listing - id: UplinkGrenadeLauncherBundle - name: uplink-grenade-launcher-bundle-name - description: uplink-grenade-launcher-bundle-desc - icon: { sprite: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher + id: UplinkHardsuitCarp + name: uplink-hardsuit-carp-name + description: uplink-hardsuit-carp-desc + icon: { sprite: /Textures/Clothing/OuterClothing/Suits/carpsuit.rsi, state: icon } + productEntity: ClothingOuterHardsuitCarp + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: - Telecrystal: 25 + Telecrystal: 8 categories: - - UplinkBundles - saleLimit: 1 + - UplinkWearables - type: listing - id: UplinkL6SawBundle - name: uplink-l6-saw-bundle-name - description: uplink-l6-saw-bundle-desc - icon: { sprite: /Textures/Objects/Weapons/Guns/LMGs/l6.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateFilledLMG + id: UplinkHardsuitSyndie + name: uplink-hardsuit-syndie-name + description: uplink-hardsuit-syndie-desc + icon: { sprite: /Textures/Clothing/OuterClothing/Hardsuits/syndicate.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateHardsuitBundle + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 4 cost: - Telecrystal: 30 + Telecrystal: 10 categories: - - UplinkBundles - saleLimit: 1 + - UplinkWearables - type: listing - id: UplinkSurplusBundle - name: uplink-surplus-bundle-name - description: uplink-surplus-bundle-desc - productEntity: CrateSyndicateSurplusBundle + id: UplinkClothingConductingGloves + name: uplink-clothing-conducting-gloves-name + description: uplink-clothing-conducting-gloves-desc + productEntity: ClothingHandsGlovesConducting cost: - Telecrystal: 20 + Telecrystal: 2 categories: - - UplinkBundles + - UplinkWearables conditions: - !type:StoreWhitelistCondition blacklist: @@ -1099,17 +1484,44 @@ blacklist: components: - SurplusBundle - saleBlacklist: true - type: listing - id: UplinkSuperSurplusBundle - name: uplink-super-surplus-bundle-name - description: uplink-super-surplus-bundle-desc - productEntity: CrateSyndicateSuperSurplusBundle + id: UplinkHardsuitSyndieElite + name: uplink-hardsuit-syndieelite-name + description: uplink-hardsuit-syndieelite-desc + icon: { sprite: /Textures/Clothing/OuterClothing/Hardsuits/syndieelite.rsi, state: icon } + productEntity: ClothingBackpackDuffelSyndicateEliteHardsuitBundle + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 7 cost: - Telecrystal: 40 + Telecrystal: 1 categories: - - UplinkBundles + - UplinkWearables + +- type: listing + id: UplinkClothingOuterHardsuitJuggernaut + name: uplink-clothing-outer-hardsuit-juggernaut-name + description: uplink-clothing-outer-hardsuit-juggernaut-desc + icon: { sprite: /Textures/Structures/Storage/Crates/syndicate.rsi, state: icon } + productEntity: CrateCybersunJuggernautBundle + discountCategory: veryRareDiscounts + discountDownTo: + Telecrystal: 8 + cost: + Telecrystal: 12 + categories: + - UplinkWearables + +- type: listing + id: UplinkClothingEyesHudSyndicate + name: uplink-clothing-eyes-hud-syndicate-name + description: uplink-clothing-eyes-hud-syndicate-desc + productEntity: ClothingEyesHudSyndicate + cost: + Telecrystal: 2 + categories: + - UplinkWearables conditions: - !type:StoreWhitelistCondition blacklist: @@ -1119,69 +1531,21 @@ blacklist: components: - SurplusBundle - saleBlacklist: true # Tools - type: listing - id: UplinkToolbox - name: uplink-toolbox-name - description: uplink-toolbox-desc - productEntity: ToolboxSyndicateFilled - cost: - Telecrystal: 2 - categories: - - UplinkTools - -- type: listing - id: UplinkSyndicateJawsOfLife - name: uplink-syndicate-jaws-of-life-name - description: uplink-syndicate-jaws-of-life-desc - productEntity: SyndicateJawsOfLife - cost: - Telecrystal: 2 - categories: - - UplinkTools - -- type: listing - id: UplinkDuffelSurgery - name: uplink-duffel-surgery-name - description: uplink-duffel-surgery-desc - productEntity: ClothingBackpackDuffelSyndicateFilledMedical - cost: - Telecrystal: 4 - categories: - - UplinkTools - -- type: listing - id: UplinkPowerSink - name: uplink-power-sink-name - description: uplink-power-sink-desc - productEntity: PowerSink - cost: - Telecrystal: 8 - categories: - - UplinkTools - conditions: - - !type:BuyerWhitelistCondition - blacklist: - components: - - SurplusBundle - -- type: listing - id: UplinkCarpDehydrated - name: uplink-carp-dehydrated-name - description: uplink-carp-dehydrated-desc - productEntity: DehydratedSpaceCarp + id: UplinkSyndicateStamp + name: uplink-syndicate-stamp-name + description: uplink-syndicate-stamp-desc + productEntity: RubberStampSyndicate + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: - Telecrystal: 2 + Telecrystal: 1 categories: - - UplinkTools - conditions: - - !type:StoreWhitelistCondition - blacklist: - tags: - - NukeOpsUplink + - UplinkPointless # Job Specific @@ -1190,6 +1554,9 @@ name: uplink-gatfruit-seeds-name description: uplink-gatfruit-seeds-desc productEntity: GatfruitSeeds + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: @@ -1204,6 +1571,9 @@ name: uplink-rigged-boxing-gloves-name description: uplink-rigged-boxing-gloves-desc productEntity: ClothingHandsGlovesBoxingRigged + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: @@ -1218,6 +1588,9 @@ name: uplink-rigged-boxing-gloves-name description: uplink-rigged-boxing-gloves-desc productEntity: ClothingHandsGlovesBoxingRigged + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: @@ -1232,6 +1605,9 @@ name: uplink-necronomicon-name description: uplink-necronomicon-desc productEntity: BibleNecronomicon + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: @@ -1250,6 +1626,9 @@ name: uplink-holy-hand-grenade-name description: uplink-holy-hand-grenade-desc productEntity: HolyHandGrenade + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 14 cost: Telecrystal: 20 categories: @@ -1258,13 +1637,15 @@ - !type:BuyerJobCondition whitelist: - Chaplain - saleLimit: 1 - type: listing id: uplinkRevolverCapGunFake name: uplink-revolver-cap-gun-fake-name description: uplink-revolver-cap-gun-fake-desc productEntity: RevolverCapGunFake + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 5 cost: Telecrystal: 9 categories: @@ -1274,7 +1655,6 @@ whitelist: - Mime - Clown - saleLimit: 1 - type: listing id: uplinkBananaPeelExplosive @@ -1282,6 +1662,9 @@ description: uplink-banana-peel-explosive-desc icon: { sprite: Objects/Specific/Hydroponics/banana.rsi, state: peel } productEntity: TrashBananaPeelExplosiveUnarmed + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 1 cost: Telecrystal: 2 categories: @@ -1296,6 +1679,9 @@ name: uplink-cluster-banana-peel-name description: uplink-cluster-banana-peel-desc productEntity: ClusterBananaPeel + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 3 cost: Telecrystal: 6 categories: @@ -1311,6 +1697,9 @@ description: uplink-holoclown-kit-desc icon: { sprite: /Textures/Objects/Fun/figurines.rsi, state: holoclown } productEntity: BoxHoloclown + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 6 cost: Telecrystal: 12 categories: @@ -1319,12 +1708,14 @@ - !type:BuyerJobCondition whitelist: - Clown - saleLimit: 1 - type: listing id: uplinkHotPotato name: uplink-hot-potato-name description: uplink-hot-potato-desc + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 productEntity: HotPotato cost: Telecrystal: 4 @@ -1343,6 +1734,9 @@ name: uplink-chimp-upgrade-kit-name description: uplink-chimp-upgrade-kit-desc productEntity: WeaponPistolCHIMPUpgradeKit + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 4 categories: @@ -1357,6 +1751,9 @@ name: uplink-proximity-mine-name description: uplink-proximity-mine-desc productEntity: WetFloorSignMineExplosive + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 5 # was 4, with my buff made it 5 to be closer to minibomb -panzer categories: @@ -1375,6 +1772,9 @@ name: uplink-syndicate-sponge-box-name description: uplink-syndicate-sponge-box-desc icon: { sprite: Objects/Misc/monkeycube.rsi, state: box} + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 4 productEntity: SyndicateSpongeBox cost: Telecrystal: 7 @@ -1395,6 +1795,9 @@ description: uplink-cane-blade-desc icon: { sprite: Objects/Weapons/Melee/cane.rsi, state: cane} productEntity: CaneSheathFilled + discountCategory: rareDiscounts + discountDownTo: + Telecrystal: 2 cost: Telecrystal: 5 categories: @@ -1407,15 +1810,25 @@ blacklist: components: - SurplusBundle + - type: listing - id: UplinkSingarityBeacon - name: uplink-singularity-beacon-name - description: uplink-singularity-beacon-desc - productEntity: SingularityBeacon + id: UplinkCombatBakery + name: uplink-combat-bakery-name + description: uplink-combat-bakery-desc + icon: { sprite: Objects/Consumable/Food/Baked/bread.rsi, state: baguette} + productEntity: CombatBakeryKit + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 2 cost: - Telecrystal: 12 + Telecrystal: 6 categories: - - UplinkUtility + - UplinkJob + conditions: + - !type:BuyerJobCondition + whitelist: + - Chef + - Mime - type: listing id: UplinkEmpFlashlight @@ -1425,7 +1838,7 @@ cost: Telecrystal: 3 categories: - - UplinkUtility + - UplinkAllies # Armor @@ -1437,355 +1850,4 @@ cost: Telecrystal: 1 categories: - - UplinkArmor - -- type: listing - id: UplinkChameleon - name: uplink-chameleon-name - description: uplink-chameleon-desc - productEntity: ClothingBackpackChameleonFill - icon: { sprite: /Textures/Clothing/Uniforms/Jumpsuit/rainbow.rsi, state: icon } - cost: - Telecrystal: 4 - categories: - - UplinkArmor - -- type: listing - id: UplinkClothingNoSlipsShoes - name: uplink-clothing-no-slips-shoes-name - description: uplink-clothing-no-slips-shoes-desc - productEntity: ClothingShoesChameleonNoSlips - cost: - Telecrystal: 2 - categories: - - UplinkArmor - -- type: listing - id: UplinkgClothingThievingGloves - name: uplink-clothing-thieving-gloves-name - description: uplink-clothing-thieving-gloves-desc - productEntity: ThievingGloves - cost: - Telecrystal: 4 - categories: - - UplinkArmor - -- type: listing - id: UplinkClothingOuterVestWeb - name: uplink-clothing-outer-vest-web-name - description: uplink-clothing-outer-vest-web-desc - productEntity: ClothingOuterVestWeb - cost: - Telecrystal: 3 - categories: - - UplinkArmor - -- type: listing - id: UplinkClothingShoesBootsMagSyndie - name: uplink-clothing-shoes-boots-mag-syndie-name - description: uplink-clothing-shoes-boots-mag-syndie-desc - productEntity: ClothingShoesBootsMagSyndie - cost: - Telecrystal: 4 - categories: - - UplinkArmor - -- type: listing - id: UplinkEVASyndie - name: uplink-eva-syndie-name - description: uplink-eva-syndie-desc - icon: { sprite: /Textures/Clothing/OuterClothing/Suits/eva_syndicate.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateEVABundle - cost: - Telecrystal: 2 - categories: - - UplinkArmor - -- type: listing - id: UplinkHardsuitSyndie - name: uplink-hardsuit-syndie-name - description: uplink-hardsuit-syndie-desc - icon: { sprite: /Textures/Clothing/OuterClothing/Hardsuits/syndicate.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateHardsuitBundle - cost: - Telecrystal: 8 - categories: - - UplinkArmor - saleLimit: 1 - -- type: listing - id: UplinkHardsuitSyndieElite - name: uplink-hardsuit-syndieelite-name - description: uplink-hardsuit-syndieelite-desc - icon: { sprite: /Textures/Clothing/OuterClothing/Hardsuits/syndieelite.rsi, state: icon } - productEntity: ClothingBackpackDuffelSyndicateEliteHardsuitBundle - cost: - Telecrystal: 10 - categories: - - UplinkArmor - saleLimit: 1 - -- type: listing - id: UplinkClothingOuterHardsuitJuggernaut - name: uplink-clothing-outer-hardsuit-juggernaut-name - description: uplink-clothing-outer-hardsuit-juggernaut-desc - icon: { sprite: /Textures/Structures/Storage/Crates/syndicate.rsi, state: icon } - productEntity: CrateCybersunJuggernautBundle - cost: - Telecrystal: 12 - categories: - - UplinkArmor - saleLimit: 1 - -# Misc - -- type: listing - id: UplinkClothingConductingGloves - name: uplink-clothing-conducting-gloves-name - description: uplink-clothing-conducting-gloves-desc - productEntity: ClothingHandsGlovesConducting - cost: - Telecrystal: 2 - categories: - - UplinkMisc - -- type: listing - id: UplinkSnackBox - name: uplink-snack-box-name - description: uplink-snack-box-desc - productEntity: HappyHonkNukieSnacks - cost: - Telecrystal: 1 - categories: - - UplinkMisc - -- type: listing - id: UplinkEshield - name: uplink-eshield-name - description: uplink-eshield-desc - icon: { sprite: /Textures/Objects/Weapons/Melee/e_shield.rsi, state: eshield-on } - productEntity: EnergyShield - cost: - Telecrystal: 8 - categories: - - UplinkMisc - conditions: - - !type:StoreWhitelistCondition - whitelist: - tags: - - NukeOpsUplink - saleLimit: 1 - -- type: listing - id: UplinkSoapSyndie - name: uplink-soap-name - description: uplink-soap-desc - productEntity: SoapSyndie - cost: - Telecrystal: 1 - categories: - - UplinkMisc - -- type: listing - id: UplinkSlipocalypseClusterSoap - name: uplink-slipocalypse-clustersoap-name - description: uplink-slipocalypse-clustersoap-desc - productEntity: SlipocalypseClusterSoap - cost: - Telecrystal: 3 - categories: - - UplinkMisc - -# - type: listing -# id: UplinkGigacancerScanner -# name: Ultragigacancer Health Analyzer -# description: Works like a normal health analyzer, other than giving everyone it scans ultragigacancer. -# productEntity: HandheldHealthAnalyzerGigacancer -# cost: -# Telecrystal: 5 -# categories: -# - UplinkMisc - -- type: listing - id: UplinkMobCatMicrobomb - name: uplink-mobcat-microbomb-name - description: uplink-mobcat-microbomb-desc - icon: { sprite: /Textures/Mobs/Pets/cat.rsi, state: syndicat } - productEntity: MobCatSyndy - cost: - Telecrystal: 10 - categories: - - UplinkMisc - conditions: - - !type:StoreWhitelistCondition - whitelist: - tags: - - NukeOpsUplink - saleLimit: 1 - -- type: listing - id: UplinkBackpackSyndicate - name: uplink-backpack-syndicate-name - description: uplink-backpack-syndicate-desc - productEntity: ClothingBackpackSyndicate - cost: - Telecrystal: 2 - categories: - - UplinkPointless - - # Pointless - -- type: listing - id: UplinkBarberScissors - name: uplink-barber-scissors-name - description: uplink-barber-scissors-desc - productEntity: BarberScissors - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkRevolverCapGun - name: uplink-revolver-cap-gun-name - description: uplink-revolver-cap-gun-desc - productEntity: RevolverCapGun - cost: - Telecrystal: 4 - categories: - - UplinkPointless - -- type: listing - id: UplinkSyndicateStamp - name: uplink-syndicate-stamp-name - description: uplink-syndicate-stamp-desc - productEntity: RubberStampSyndicate - cost: - Telecrystal: 2 - categories: - - UplinkPointless - -# - type: listing # DeltaV - Move cat ears out of syndicate uplink into the AutoDrobe -# id: UplinkCatEars -# name: uplink-cat-ears-name -# description: uplink-cat-ears-desc -# productEntity: ClothingHeadHatCatEars -# cost: -# Telecrystal: 4 # Nyanotrasen - Make Cat Ears 4TC instead of 26TC -# categories: -# - UplinkPointless - -- type: listing - id: UplinkOutlawHat - name: uplink-outlaw-hat-name - description: uplink-outlaw-hat-desc - productEntity: ClothingHeadHatOutlawHat - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkOutlawGlasses - name: uplink-outlaw-glasses-name - description: uplink-outlaw-glasses-desc - productEntity: ClothingEyesGlassesOutlawGlasses - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkCostumePyjama - name: uplink-costume-pyjama-name - description: uplink-costume-pyjama-desc - productEntity: ClothingBackpackDuffelSyndicatePyjamaBundle - cost: - Telecrystal: 4 - categories: - - UplinkPointless - -- type: listing - id: UplinkCostumeClown - name: uplink-costume-clown-name - description: uplink-costume-clown-desc - productEntity: ClothingBackpackDuffelSyndicateCostumeClown - cost: - Telecrystal: 2 - categories: - - UplinkPointless - -# - type: listing # DeltaV - Disable carp suit being buyable -# id: UplinkCarpSuitBundle -# name: uplink-carp-suit-bundle-name -# description: uplink-carp-suit-bundle-desc -# productEntity: ClothingBackpackDuffelSyndicateCarpSuit -# cost: -# Telecrystal: 4 -# categories: -# - UplinkPointless - -- type: listing - id: UplinkOperativeSuit - name: uplink-operative-suit-name - description: uplink-operative-suit-desc - productEntity: ClothingUniformJumpsuitOperative - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkOperativeSkirt - name: uplink-operative-skirt-name - description: uplink-operative-skirt-desc - productEntity: ClothingUniformJumpskirtOperative - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkBalloon - name: uplink-balloon-name - description: uplink-balloon-desc - productEntity: BalloonSyn - cost: - Telecrystal: 20 - categories: - - UplinkPointless - -- type: listing - id: UplinkScarfSyndieRed - name: uplink-scarf-syndie-red-name - description: uplink-scarf-syndie-red-desc - productEntity: ClothingNeckScarfStripedSyndieRed - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkScarfSyndieGreen - name: uplink-scarf-syndie-green-name - description: uplink-scarf-syndie-green-desc - productEntity: ClothingNeckScarfStripedSyndieGreen - cost: - Telecrystal: 1 - categories: - - UplinkPointless - -- type: listing - id: UplinkSyndicatePersonalAI - name: uplink-syndicate-pai-name - description: uplink-syndicate-pai-desc - icon: { sprite: /Textures/Objects/Fun/pai.rsi, state: syndicate-icon-pai-off } - productEntity: SyndicatePersonalAI - cost: - Telecrystal: 1 - categories: - - UplinkPointless - conditions: - - !type:ListingLimitedStockCondition - stock: 1 - + - UplinkWearables \ No newline at end of file diff --git a/Resources/Prototypes/CharacterItemGroups/Jobs/Command/adminAssistant.yml b/Resources/Prototypes/CharacterItemGroups/Jobs/Command/adminAssistant.yml new file mode 100644 index 00000000000..d87cf366553 --- /dev/null +++ b/Resources/Prototypes/CharacterItemGroups/Jobs/Command/adminAssistant.yml @@ -0,0 +1,17 @@ +- type: characterItemGroup + id: LoadoutAdminAssistantUniforms + maxItems: 1 + items: + - type: loadout + id: LoadoutAdminAssistantUniformJumpsuit + - type: loadout + id: LoadoutAdminAssistantUniformJumpskirt + +- type: characterItemGroup + id: LoadoutAdminAssistantGloves + maxItems: 1 + items: + - type: loadout + id: LoadoutHeadOfPersonnelGlovesHoP + - type: loadout + id: LoadoutHeadOfPersonnelGlovesInspection \ No newline at end of file diff --git a/Resources/Prototypes/Damage/modifier_sets.yml b/Resources/Prototypes/Damage/modifier_sets.yml index ff770e2cc25..694ec4fbea1 100644 --- a/Resources/Prototypes/Damage/modifier_sets.yml +++ b/Resources/Prototypes/Damage/modifier_sets.yml @@ -377,4 +377,4 @@ coefficients: Blunt: 0.9 Slash: 0.9 - Piercing: 0.9 \ No newline at end of file + Piercing: 0.9 diff --git a/Resources/Prototypes/Datasets/adjectives.yml b/Resources/Prototypes/Datasets/adjectives.yml index 86fb2909521..fd243a88696 100644 --- a/Resources/Prototypes/Datasets/adjectives.yml +++ b/Resources/Prototypes/Datasets/adjectives.yml @@ -314,7 +314,6 @@ - slow - swift - young - - Taste/Touch - bitter - delicious - fresh diff --git a/Resources/Prototypes/Datasets/verbs.yml b/Resources/Prototypes/Datasets/verbs.yml index f53c18a71bf..ce245370213 100644 --- a/Resources/Prototypes/Datasets/verbs.yml +++ b/Resources/Prototypes/Datasets/verbs.yml @@ -1,4 +1,4 @@ -- type: dataset +- type: dataset id: verbs values: - accept @@ -616,7 +616,6 @@ - whine - whip - whirl - - whisper - whistle - wink - wipe diff --git a/Resources/Prototypes/DeltaV/Catalog/Fills/Items/Backpacks/duffelbag.yml b/Resources/Prototypes/DeltaV/Catalog/Fills/Items/Backpacks/duffelbag.yml new file mode 100644 index 00000000000..f0efd4197a3 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Catalog/Fills/Items/Backpacks/duffelbag.yml @@ -0,0 +1,17 @@ +- type: entity + parent: ClothingBackpackDuffelSalvage + id: ClothingBackpackDuffelSalvageConscription + name: mining conscription kit + description: A duffel bag containing everything a crewmember needs to support a shaft miner in the field. + components: + - type: StorageFill + contents: + - id: ClothingEyesGlassesMeson + - id: OreBag + - id: ClothingUniformJumpsuitSalvageSpecialist + - id: EncryptionKeyCargo + - id: ClothingMaskGasExplorer + - id: SalvageIDCard + - id: WeaponProtoKineticAccelerator + - id: SurvivalKnife + - id: FlashlightSeclite diff --git a/Resources/Prototypes/DeltaV/Catalog/Shipyard/civilian.yml b/Resources/Prototypes/DeltaV/Catalog/Shipyard/civilian.yml index c3a0935e369..4e1a127bdd8 100644 --- a/Resources/Prototypes/DeltaV/Catalog/Shipyard/civilian.yml +++ b/Resources/Prototypes/DeltaV/Catalog/Shipyard/civilian.yml @@ -36,7 +36,7 @@ id: Prospector name: NT-7 Prospector description: A small mining vessel designed to assist salvage operations. - price: 20800 + price: 21000 path: /Maps/Shuttles/DeltaV/prospector.yml categories: - Civilian diff --git a/Resources/Prototypes/DeltaV/Catalog/VendingMachines/Inventories/salvage_points.yml b/Resources/Prototypes/DeltaV/Catalog/VendingMachines/Inventories/salvage_points.yml new file mode 100644 index 00000000000..8b75b147fab --- /dev/null +++ b/Resources/Prototypes/DeltaV/Catalog/VendingMachines/Inventories/salvage_points.yml @@ -0,0 +1,51 @@ +- type: shopInventory + id: SalvageVendorInventory + listings: + # TODO: marker beacons 1/10/30 for 10 each + - id: DrinkWhiskeyBottleFull + cost: 100 + - id: DrinkAbsintheBottleFull + cost: 100 + - id: CigarGold + cost: 150 + - id: Soap + cost: 200 + - id: SeismicCharge + cost: 250 + - id: WeaponGrapplingGun + cost: 300 + # TODO: laser pointer 300, toy facehugger 300 + # TODO: stabilizing serum for 400 + - id: FultonBeacon + cost: 400 + # TODO: bluespace shelter capsule for 400 + - id: ClothingEyesGlassesGarMeson + cost: 500 + - id: ClothingBeltSalvageWebbing + cost: 500 + - id: MedkitBruteFilled + cost: 600 + - id: MedkitBurnFilled + cost: 600 + # TODO: salvage 5g, 3 implants and a locator for 600 + # TODO: wormhole jaunter for 750 + - id: WeaponCrusher + cost: 750 + - id: WeaponProtoKineticAccelerator + cost: 750 + # TODO: resonator for 800 + - id: Fulton + cost: 1000 + # TODO: lazarus injector for 1k + - id: ClothingBackpackDuffelSalvageConscription + cost: 1500 + - id: SpaceCash1000 + cost: 2000 + # TODO: super resonator for 2500 + # TODO: jump boots for 2500 + - id: ClothingOuterHardsuitSalvage + cost: 3000 + # TODO: luxury shelter capsule for 3k + # TODO: luxury elite bar capsule for 10k + # TODO: pka mods + # TODO: mining drone stuff diff --git a/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml b/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml index ca0e83d45a3..62525a9c077 100644 --- a/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml @@ -7,7 +7,7 @@ cost: Telecrystal: 3 categories: - - UplinkUtility + - UplinkAllies - type: listing id: UplinkBionicSyrinxImplanter diff --git a/Resources/Prototypes/DeltaV/Entities/Clothing/Ears/headsets.yml b/Resources/Prototypes/DeltaV/Entities/Clothing/Ears/headsets.yml index b32cab7b21a..f2f63ac949c 100644 --- a/Resources/Prototypes/DeltaV/Entities/Clothing/Ears/headsets.yml +++ b/Resources/Prototypes/DeltaV/Entities/Clothing/Ears/headsets.yml @@ -115,3 +115,28 @@ - type: Clothing sprite: Clothing/Ears/Headsets/security.rsi +- type: entity + parent: ClothingHeadset + id: ClothingHeadsetAdminAssist + name: adminstrative assistant headset + description: A headset used by the administrative assistant. + components: + - type: ContainerFill + containers: + key_slots: + - EncryptionKeyCommand + - EncryptionKeyCommon + - type: Sprite + sprite: DeltaV/Clothing/Ears/Headsets/adminassistant.rsi + - type: Clothing + sprite: DeltaV/Clothing/Ears/Headsets/adminassistant.rsi + +- type: entity + parent: ClothingHeadsetAdminAssist + id: ClothingHeadsetAltAdminAssist + name: adminstrative assistant over-ear headset + components: + - type: Sprite + state: icon_alt + - type: Clothing + equippedPrefix: alt diff --git a/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml index 8642867effd..4a1d35ffea9 100644 --- a/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml @@ -12,3 +12,11 @@ modifiers: coefficients: Caustic: 0.85 + +- type: entity + parent: ClothingEyesGlassesGar + id: ClothingEyesGlassesGarMeson + name: gar mesons + description: Do the impossible, see the invisible! + components: + - type: EyeProtection \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpskirts.yml b/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpskirts.yml index 9e80bc4ff2f..79c8f1646b1 100644 --- a/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpskirts.yml +++ b/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpskirts.yml @@ -127,3 +127,13 @@ - type: Clothing sprite: DeltaV/Clothing/Uniforms/Jumpskirt/prosecutorred.rsi +- type: entity + parent: ClothingUniformBase + id: ClothingUniformJumpskirtAdminAssistant + name: administrative assistant's jumpskirt + description: A suit worn by the Administrative Assistant. Smells of burnt coffee. + components: + - type: Sprite + sprite: DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi + - type: Clothing + sprite: DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi diff --git a/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpsuits.yml b/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpsuits.yml index 9382502e740..f64ad3041a5 100644 --- a/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpsuits.yml +++ b/Resources/Prototypes/DeltaV/Entities/Clothing/Uniforms/jumpsuits.yml @@ -310,3 +310,13 @@ - type: Clothing sprite: DeltaV/Clothing/Uniforms/Jumpsuit/prosecutorred.rsi +- type: entity + parent: ClothingUniformBase + id: ClothingUniformJumpsuitAdminAssistant + name: administrative assistant's jumpsuit + description: A suit worn by the Administrative Assistant. Smells of burnt coffee. + components: + - type: Sprite + sprite: DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi + - type: Clothing + sprite: DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml index f313d30f8f0..4bcca6ca675 100644 --- a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml @@ -47,6 +47,7 @@ - state: green - sprite: Structures/Wallmounts/signs.rsi state: radiation + - type: GhostRoleAntagSpawner - type: entity parent: MarkerBase @@ -58,6 +59,7 @@ description: ghost-role-information-paradox-anomaly-description rules: ghost-role-information-paradox-anomaly-rules - type: ParadoxAnomalySpawner + - type: GhostRoleAntagSpawner - type: Sprite sprite: Markers/jobs.rsi layers: diff --git a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/jobs.yml b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/jobs.yml index 12747abbcfb..65d3d8369c1 100644 --- a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/jobs.yml +++ b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/jobs.yml @@ -65,3 +65,16 @@ - state: green - sprite: DeltaV/Markers/jobs.rsi state: courier + +- type: entity + id: SpawnPointAdminAssistant + parent: SpawnPointJobBase + name: administrative assistant + components: + - type: SpawnPoint + job_id: AdministrativeAssistant + - type: Sprite + layers: + - state: green + - sprite: DeltaV/Markers/jobs.rsi + state: adminassistant \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml index 09cafa29575..b9e4fedd5b2 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml @@ -159,8 +159,7 @@ - Syndicate # make the player a traitor once its taken - type: AutoTraitor - giveUplink: false - giveObjectives: false + profile: TraitorReinforcement - type: InteractionPopup successChance: 0.6 interactSuccessString: petting-success-nukie-mouse diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml index 5bc30734426..5c61c9d359e 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml @@ -9,9 +9,6 @@ - type: NpcFactionMember factions: - Syndicate - - type: AutoTraitor - giveUplink: false - giveObjectives: false - type: AutoImplant implants: - DeathAcidifierImplant diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Consumable/Food/Baked/pie.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Consumable/Food/Baked/pie.yml index d86b5142dbf..b83cf87fb26 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Consumable/Food/Baked/pie.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Consumable/Food/Baked/pie.yml @@ -36,4 +36,5 @@ tags: - Fruit - Pie + - Slice # Tastes like pie, pumpkin. diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/Medical/portafib.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/Medical/portafib.yml index 0f3095663d3..31ac4e0c179 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/Medical/portafib.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/Medical/portafib.yml @@ -1,55 +1,17 @@ - type: entity id: Portafib - parent: [ BaseItem, PowerCellSlotSmallItem ] + parent: Defibrillator name: portafib description: Less weight, same great ZZZAP! components: - type: Sprite sprite: DeltaV/Objects/Medical/portafib.rsi - layers: - - state: icon - - state: screen - map: [ "enum.ToggleVisuals.Layer" ] - visible: false - shader: unshaded - - state: ready - map: ["enum.PowerDeviceVisualLayers.Powered"] - shader: unshaded - - type: GenericVisualizer - visuals: - enum.ToggleVisuals.Toggled: - enum.ToggleVisuals.Layer: - True: { visible: true } - False: { visible: false } - enum.DefibrillatorVisuals.Ready: - enum.PowerDeviceVisualLayers.Powered: - True: { visible: true } - False: { visible: false } - type: Item size: Normal - - type: MultiHandedItem - - type: Speech - - type: Defibrillator - zapHeal: - types: - Asphyxiation: -40 - type: PowerCellDraw useRate: 140 - - type: Appearance - - type: DoAfter - - type: UseDelay - type: StaticPrice price: 100 - - type: GuideHelp - guides: - - Medical Doctor - - type: DamageOtherOnHit - damage: - types: - Blunt: 6.5 - staminaCost: 8 - soundHit: - path: /Audio/Weapons/smash.ogg - type: entity id: PortafibEmpty diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml index 81e11d9d084..9c7e221b86d 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml @@ -59,3 +59,48 @@ icon: sprite: Objects/Specific/Mail/mail.rsi state: icon + +- type: entity + parent: BaseItem + id: StockTradingCartridge + name: StockTrading cartridge + description: A cartridge that tracks the intergalactic stock market. + components: + - type: Sprite + sprite: DeltaV/Objects/Devices/cartridge.rsi + state: cart-stonk + - type: Icon + sprite: DeltaV/Objects/Devices/cartridge.rsi + state: cart-mail + - type: UIFragment + ui: !type:StockTradingUi + - type: StockTradingCartridge + - type: Cartridge + programName: stock-trading-program-name + icon: + sprite: DeltaV/Misc/program_icons.rsi + state: stock_trading + - type: BankClient + - type: AccessReader # This is so that we can restrict who can buy stocks + access: [["Orders"]] + +- type: entity + parent: BaseItem + id: NanoChatCartridge + name: NanoChat cartridge + description: Lets you message other people! + components: + - type: Sprite + sprite: DeltaV/Objects/Devices/cartridge.rsi + state: cart-chat + - type: UIFragment + ui: !type:NanoChatUi + - type: NanoChatCartridge + - type: Cartridge + programName: nano-chat-program-name + icon: + sprite: DeltaV/Misc/program_icons.rsi + state: nanochat + - type: ActiveRadio + channels: + - Common diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml index b640206c912..fcb21e50de1 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml @@ -23,6 +23,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: Pda id: BrigmedicIDCard state: pda-corpsman @@ -75,6 +76,7 @@ - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -114,6 +116,7 @@ - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -153,6 +156,7 @@ - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - NanoChatCartridge - type: entity parent: SyndiPDA @@ -166,3 +170,40 @@ whitelist: tags: - Write + +- type: entity + parent: BasePDA + id: AdminAssistantPDA + name: administrative assistant PDA + description: Theres pen scribbles all over the edges, and a few sticky notes stuck on it. + components: + - type: Sprite + sprite: DeltaV/Objects/Devices/pda.rsi + layers: + - map: [ "enum.PdaVisualLayers.Base" ] + - state: "light_overlay" + map: [ "enum.PdaVisualLayers.Flashlight" ] + shader: "unshaded" + visible: false + - state: "id_overlay" + map: [ "enum.PdaVisualLayers.IdLight" ] + shader: "unshaded" + visible: false + - type: Pda + id: AdminAssistantIDCard + state: pda-admin-assistant + penSlot: + startingItem: LuxuryPen + priority: -1 + whitelist: + tags: + - Write + - type: Icon + sprite: DeltaV/Objects/Devices/pda.rsi + state: pda-admin-assistant + - type: CartridgeLoader + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - NanoChatCartridge \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Misc/rubber_stamp.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/rubber_stamp.yml index 764d053393e..791e142d173 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Misc/rubber_stamp.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/rubber_stamp.yml @@ -55,3 +55,16 @@ sprite: DeltaV/Objects/Misc/stamps.rsi state: stamp-cj +- type: entity + name: administrative assistant rubber stamp + parent: RubberStampBase + id: RubberStampAdminAssistant + suffix: DO NOT MAP + components: + - type: Stamp + stampedName: stamp-component-stamped-name-admin-assistant + stampedColor: "#4191f2" + stampState: "paper_stamp-admin-assistant" + - type: Sprite + sprite: DeltaV/Objects/Misc/stamps.rsi + state: stamp-admin-assistant \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Command/safe.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Command/safe.yml index 17321ef59e2..17880dd3e29 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Command/safe.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Command/safe.yml @@ -21,6 +21,7 @@ - IdCard - type: AccessReader access: [["DV-SpareSafe"]] + - type: SpareIDSafe - type: entity id: SpareIdCabinetFilled diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml index 43e26db6352..c4fd8bb1d2e 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml @@ -111,7 +111,7 @@ - id: BookBartendersManual prob: 0.2 orGroup: bookguide - - id: BookChefGaming + - id: BookHowToCookForFortySpaceman prob: 0.2 orGroup: bookguide - id: BookLeafLoversSecret diff --git a/Resources/Prototypes/DeltaV/Entities/Stations/base.yml b/Resources/Prototypes/DeltaV/Entities/Stations/base.yml new file mode 100644 index 00000000000..ffeb605adc2 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Entities/Stations/base.yml @@ -0,0 +1,30 @@ +- type: entity + id: BaseStationStockMarket + abstract: true + components: + - type: StationStockMarket + companies: + - displayName: stock-trading-company-nanotrasen + basePrice: 100 + currentPrice: 100 + - displayName: stock-trading-company-gorlex + basePrice: 75 + currentPrice: 75 + - displayName: stock-trading-company-interdyne + basePrice: 300 + currentPrice: 300 + - displayName: stock-trading-company-fishinc + basePrice: 25 + currentPrice: 25 + - displayName: stock-trading-company-donk + basePrice: 90 + currentPrice: 90 + - displayName: stock-trading-company-hydroco + basePrice: 30 + currentPrice: 30 + +- type: entity + id: BaseStationCaptainState + abstract: true + components: + - type: CaptainState diff --git a/Resources/Prototypes/DeltaV/Roles/Jobs/Command/administrative_assistant.yml b/Resources/Prototypes/DeltaV/Roles/Jobs/Command/administrative_assistant.yml new file mode 100644 index 00000000000..1483d646605 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Roles/Jobs/Command/administrative_assistant.yml @@ -0,0 +1,49 @@ +- type: job + id: AdministrativeAssistant + name: job-name-admin-assistant + description: job-description-admin-assistant + playTimeTracker: JobAdminAssistant + requirements: + - !type:DepartmentTimeRequirement + department: Engineering + min: 10800 # 3 hours + - !type:DepartmentTimeRequirement + department: Logistics + min: 10800 # 3 hours + - !type:DepartmentTimeRequirement + department: Medical + min: 10800 # 3 hours + - !type:DepartmentTimeRequirement + department: Civilian + min: 10800 # 3 hours + - !type:DepartmentTimeRequirement + department: Security + min: 10800 # 3 hours + - !type:DepartmentTimeRequirement + department: Justice + min: 10800 # 3 hours + - !type:DepartmentTimeRequirement + department: Epistemics + min: 10800 # 3 hours + startingGear: AdminAssistantGear + icon: "JobIconAdminAssitant" + supervisors: job-supervisors-command + canBeAntag: false + access: + - Command + - Maintenance + special: + - !type:AddImplantSpecial + implants: [MindShieldImplant] + - !type:AddComponentSpecial + components: + - type: CommandStaff + +- type: startingGear + id: AdminAssistantGear + equipment: + jumpsuit: ClothingUniformJumpsuitAdminAssistant + id: AdminAssistantPDA + ears: ClothingHeadsetAdminAssist + shoes: ClothingShoesLeather + innerClothingSkirt: ClothingUniformJumpskirtAdminAssistant \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Roles/play_time_trackers.yml b/Resources/Prototypes/DeltaV/Roles/play_time_trackers.yml index 4ab6f3eed45..782163f83cd 100644 --- a/Resources/Prototypes/DeltaV/Roles/play_time_trackers.yml +++ b/Resources/Prototypes/DeltaV/Roles/play_time_trackers.yml @@ -6,3 +6,6 @@ - type: playTimeTracker id: JobCourier + +- type: playTimeTracker + id: JobAdminAssistant \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/StatusEffects/job.yml b/Resources/Prototypes/DeltaV/StatusEffects/job.yml index 894e1586b40..c99d98ce517 100644 --- a/Resources/Prototypes/DeltaV/StatusEffects/job.yml +++ b/Resources/Prototypes/DeltaV/StatusEffects/job.yml @@ -1,27 +1,34 @@ -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMedicalBorg icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: MedicalBorg -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconChiefJustice icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: ChiefJustice -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconClerk icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: Clerk -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconProsecutor icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: Prosecutor #need prosecutor + +- type: jobIcon + parent: JobIcon + id: JobIconAdminAssitant + icon: + sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi + state: AdminAssistant \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/name_identifier_groups.yml b/Resources/Prototypes/DeltaV/name_identifier_groups.yml new file mode 100644 index 00000000000..aeb5cf152ad --- /dev/null +++ b/Resources/Prototypes/DeltaV/name_identifier_groups.yml @@ -0,0 +1,4 @@ +# used by the nanochatcard numbers +- type: nameIdentifierGroup + id: NanoChat + maxValue: 9999 diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index fdec5232026..afe189748c5 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -206,7 +206,7 @@ - type: FingerprintMask - type: entity - parent: ClothingHandsBase + parent: [ClothingHandsBase, BaseToggleClothing] id: ClothingHandsGlovesSpaceNinja name: space ninja gloves description: These black nano-enhanced gloves insulate from electricity and provide fire resistance. @@ -233,7 +233,31 @@ - type: FingerprintMask - type: Thieving stripTimeReduction: 1 + - type: ToggleClothing + action: ActionToggleNinjaGloves - type: NinjaGloves + abilities: + - components: + - type: BatteryDrainer + - type: StunProvider + noPowerPopup: ninja-no-power + whitelist: + components: + - Stamina + - type: EmagProvider + whitelist: + components: + - Airlock + - objective: StealResearchObjective + components: + - type: ResearchStealer + - objective: TerrorObjective + components: + - type: CommsHacker + threats: NinjaThreats + - objective: MassArrestObjective + components: + - type: CriminalRecordsHacker - type: entity parent: ClothingHandsGlovesColorBlack diff --git a/Resources/Prototypes/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Entities/Clothing/Head/hats.yml index fe590c55628..65ac24d5b53 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hats.yml @@ -263,6 +263,10 @@ - type: ContainerContainer containers: storagebase: !type:Container + - type: PilotedClothing + pilotWhitelist: + tags: + - ChefPilot - type: Tag tags: - ClothMade @@ -829,6 +833,21 @@ coefficients: Blunt: 0.95 +- type: entity + parent: ClothingHeadBase + id: ClothingHeadHatHolyWatermelon + name: watermelon halo + description: Holy moly. + components: + - type: Sprite + sprite: Clothing/Head/Hats/holyhatmelon.rsi + - type: Clothing + sprite: Clothing/Head/Hats/holyhatmelon.rsi + - type: Armor + modifiers: + coefficients: + Caustic: 0.95 + - type: entity parent: ClothingHeadBase id: ClothingHeadHatSyndie diff --git a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml index be7bda91008..3a5056340cc 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml @@ -206,6 +206,22 @@ slots: - Hair +- type: entity + parent: ClothingHeadHatHoodCarp + id: ClothingHeadHelmetHardsuitCarp + categories: [ HideSpawnMenu ] + components: + - type: PressureProtection + highPressureMultiplier: 0.6 + lowPressureMultiplier: 1000 + - type: TemperatureProtection + coefficient: 0.2 + - type: BreathMask + # this is on the hood so you only fool the fish if you wear the whole set + # wear carp suit and security helmet, they'll know you are fake + - type: FactionClothing + faction: Dragon + - type: entity parent: ClothingHeadBase id: ClothingHeadHatHoodMoth diff --git a/Resources/Prototypes/Entities/Clothing/Head/misc.yml b/Resources/Prototypes/Entities/Clothing/Head/misc.yml index c7ba6e0b32a..bd7899d75f8 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/misc.yml @@ -172,6 +172,36 @@ - type: AddAccentClothing accent: OwOAccent +- type: entity + parent: [ClothingHeadHatCatEars, BaseToggleClothing] + id: ClothingHeadHatCatEarsValid + suffix: Valid, DO NOT MAP + components: + - type: ToggleClothing + action: ActionBecomeValid + disableOnUnequip: true + - type: ComponentToggler + parent: true + components: + - type: KillSign + - type: Tag + tags: [] # ignore "WhitelistChameleon" tag + - type: Sprite + sprite: Clothing/Head/Hats/catears.rsi + - type: Clothing + sprite: Clothing/Head/Hats/catears.rsi + - type: AddAccentClothing + accent: OwOAccent + +- type: entity + categories: [ HideSpawnMenu ] + id: ActionBecomeValid + name: Become Valid + description: "*notices your killsign* owo whats this" + components: + - type: InstantAction + event: !type:ToggleActionEvent + - type: entity parent: ClothingHeadBase id: ClothingHeadHatDogEars diff --git a/Resources/Prototypes/Entities/Clothing/Masks/specific.yml b/Resources/Prototypes/Entities/Clothing/Masks/specific.yml index 35b2806f6e7..90c648c9d88 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/specific.yml @@ -30,16 +30,13 @@ suffix: Voice Mask, Chameleon components: - type: VoiceMask + - type: HideLayerClothing + slots: + - Snout - type: UserInterface interfaces: enum.VoiceMaskUIKey.Key: type: VoiceMaskBoundUserInterface - - type: Tag - tags: - - IPCMaskWearable - - type: HideLayerClothing - slots: - - Snout - type: entity parent: ClothingMaskBase @@ -69,3 +66,41 @@ - type: HideLayerClothing slots: - Snout + +- type: entity + parent: ClothingMaskBase + id: ClothingMaskGoldenCursed + name: golden mask + description: Previously used in strange pantomimes, after one of the actors went mad on stage these masks have avoided use. You swear its face contorts when you're not looking. + components: + - type: Sprite + sprite: Clothing/Mask/goldenmask.rsi + layers: + - state: icon + map: [ "mask" ] + - type: Clothing + sprite: Clothing/Mask/goldenmask.rsi + - type: Appearance + - type: GenericVisualizer + visuals: + enum.CursedMaskVisuals.State: + mask: + Neutral: { state: icon } + Despair: { state: icon-despair } + Joy: { state: icon-joy } + Anger: { state: icon-anger } + - type: Tag + tags: [] # ignore "WhitelistChameleon" tag + - type: SelfEquipOnly + - type: CursedMask + despairDamageModifier: + coefficients: + Blunt: 0.6 + Slash: 0.6 + Piercing: 0.4 + - type: HideLayerClothing + slots: + - Snout + - type: IngestionBlocker + - type: StaticPrice + price: 5000 diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml index 6215fb3b45a..ac4d430557e 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml @@ -154,7 +154,7 @@ - HidesHarpyWings - type: entity - parent: [ClothingOuterBaseLarge, AllowSuitStorageClothing] + parent: [ClothingOuterBaseLarge, AllowSuitStorageClothing, BaseToggleClothing] id: ClothingOuterSuitSpaceNinja name: space ninja suit description: This black technologically advanced, cybernetically-enhanced suit provides many abilities like invisibility or teleportation. @@ -163,9 +163,7 @@ sprite: Clothing/OuterClothing/Suits/spaceninja.rsi - type: Clothing sprite: Clothing/OuterClothing/Suits/spaceninja.rsi - - type: StealthClothing - visibility: 1.1 - toggleAction: ActionTogglePhaseCloak + # hardsuit stuff - type: PressureProtection highPressureMultiplier: 0.6 lowPressureMultiplier: 1000 @@ -178,7 +176,27 @@ Slash: 0.8 Piercing: 0.8 Heat: 0.8 + # phase cloak + - type: ToggleClothing + action: ActionTogglePhaseCloak + - type: ComponentToggler + parent: true + components: + - type: Stealth + minVisibility: 0.1 + lastVisibility: 0.1 + - type: PowerCellDraw + drawRate: 1.8 # 200 seconds on the default cell + # throwing star ability + - type: ItemCreator + action: ActionCreateThrowingStar + charge: 14.4 + spawnedPrototype: ThrowingStarNinja + noPowerPopup: ninja-no-power + # core ninja suit stuff - type: NinjaSuit + - type: UseDelay + delay: 5 # disable time - type: PowerCellSlot cellSlotId: cell_slot # throwing in a recharger would bypass glove charging mechanic @@ -279,3 +297,16 @@ - type: ContainerContainer containers: toggleable-clothing: !type:ContainerSlot {} + +- type: entity + parent: ClothingOuterSuitCarp + id: ClothingOuterHardsuitCarp + suffix: Hardsuit, DO NOT MAP + components: + - type: PressureProtection + highPressureMultiplier: 0.6 + lowPressureMultiplier: 1000 + - type: TemperatureProtection + coefficient: 0.01 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitCarp diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml index 13fbc087164..bb2b163f080 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml @@ -1,45 +1,39 @@ - type: entity - parent: ClothingShoesBase + parent: [ClothingShoesBase, BaseToggleClothing] id: ClothingShoesBootsMag name: magboots description: Magnetic boots, often used during extravehicular activity to ensure the user remains safely attached to the vehicle. components: - - type: Sprite - sprite: Clothing/Shoes/Boots/magboots.rsi - layers: - - state: icon - map: [ "enum.ToggleVisuals.Layer" ] - - type: Clothing - sprite: Clothing/Shoes/Boots/magboots.rsi - - type: Magboots - - type: ClothingSpeedModifier - walkModifier: 0.85 - sprintModifier: 0.8 - enabled: false - - type: Appearance - - type: GenericVisualizer - visuals: - enum.ToggleVisuals.Toggled: - enum.ToggleVisuals.Layer: - True: {state: icon-on} - False: {state: icon} - - type: StaticPrice - price: 200 - - type: Tag - tags: - - WhitelistChameleon - - type: ReverseEngineering - difficulty: 2 - recipes: - - ClothingShoesBootsMag - - type: DamageOtherOnHit - damage: - types: - Blunt: 9 - staminaCost: 11.5 - soundHit: - path: /Audio/Weapons/smash.ogg - + - type: Sprite + sprite: Clothing/Shoes/Boots/magboots.rsi + layers: + - state: icon + map: [ "enum.ToggleVisuals.Layer" ] + - type: Clothing + sprite: Clothing/Shoes/Boots/magboots.rsi + - type: ToggleClothing + action: ActionToggleMagboots + - type: ToggleVerb + text: toggle-magboots-verb-get-data-text + - type: ComponentToggler + components: + - type: NoSlip + - type: Magboots + - type: ClothingSpeedModifier + walkModifier: 0.85 + sprintModifier: 0.8 + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ToggleVisuals.Toggled: + enum.ToggleVisuals.Layer: + True: {state: icon-on} + False: {state: icon} + - type: StaticPrice + price: 200 + - type: Tag + tags: + - WhitelistChameleon - type: entity parent: ClothingShoesBootsMag @@ -52,13 +46,9 @@ state: icon - type: Clothing sprite: Clothing/Shoes/Boots/magboots-advanced.rsi - - type: Magboots - toggleAction: ActionToggleMagbootsAdvanced - type: ClothingSpeedModifier walkModifier: 1 sprintModifier: 1 - enabled: false - - type: NoSlip - type: Tag tags: - WhitelistChameleon @@ -81,8 +71,6 @@ sprite: Clothing/Shoes/Boots/magboots-science.rsi - type: Clothing sprite: Clothing/Shoes/Boots/magboots-science.rsi - - type: Magboots - toggleAction: ActionToggleMagbootsSci - type: entity parent: ClothingShoesBootsMag @@ -93,7 +81,6 @@ - type: ClothingSpeedModifier walkModifier: 1.10 #PVS isn't too much of an issue when you are blind... sprintModifier: 1.10 - enabled: false - type: StaticPrice price: 3000 @@ -108,12 +95,9 @@ state: icon - type: Clothing sprite: Clothing/Shoes/Boots/magboots-syndicate.rsi - - type: Magboots - toggleAction: ActionToggleMagbootsSyndie - type: ClothingSpeedModifier walkModifier: 0.95 sprintModifier: 0.9 - enabled: false - type: GasTank outputPressure: 42.6 air: @@ -121,79 +105,18 @@ volume: 0.75 temperature: 293.15 moles: - - 0.153853429 # oxygen - - 0.153853429 # nitrogen - - type: ActivatableUI - key: enum.SharedGasTankUiKey.Key - - type: UserInterface - interfaces: - enum.SharedGasTankUiKey.Key: - type: GasTankBoundUserInterface - - type: Explosive - explosionType: Default - maxIntensity: 20 - - type: Jetpack - moleUsage: 0.00085 - - type: CanMoveInAir - - type: InputMover - toParent: true - - type: MovementSpeedModifier - weightlessAcceleration: 1 - weightlessFriction: 0.3 - weightlessModifier: 1.2 - - type: Tag - tags: - - WhitelistChameleon - - type: DamageOtherOnHit - damage: - types: - Blunt: 20 - staminaCost: 25 - soundHit: - collection: MetalThud + - 0.153853429 # oxygen + - 0.153853429 # nitrogen + - type: Item + sprite: null + size: Normal - type: entity - id: ActionBaseToggleMagboots + id: ActionToggleMagboots name: Toggle Magboots description: Toggles the magboots on and off. categories: [ HideSpawnMenu ] components: - type: InstantAction - itemIconStyle: NoItem - event: !type:ToggleMagbootsEvent - -- type: entity - id: ActionToggleMagboots - parent: ActionBaseToggleMagboots - categories: [ HideSpawnMenu ] - components: - - type: InstantAction - icon: { sprite: Clothing/Shoes/Boots/magboots.rsi, state: icon } - iconOn: { sprite : Clothing/Shoes/Boots/magboots.rsi, state: icon-on } - -- type: entity - id: ActionToggleMagbootsAdvanced - parent: ActionBaseToggleMagboots - categories: [ HideSpawnMenu ] - components: - - type: InstantAction - icon: { sprite: Clothing/Shoes/Boots/magboots-advanced.rsi, state: icon } - iconOn: Clothing/Shoes/Boots/magboots-advanced.rsi/icon-on.png - -- type: entity - id: ActionToggleMagbootsSci - parent: ActionBaseToggleMagboots - categories: [ HideSpawnMenu ] - components: - - type: InstantAction - icon: { sprite: Clothing/Shoes/Boots/magboots-science.rsi, state: icon } - iconOn: Clothing/Shoes/Boots/magboots-science.rsi/icon-on.png - -- type: entity - id: ActionToggleMagbootsSyndie - parent: ActionBaseToggleMagboots - categories: [ HideSpawnMenu ] - components: - - type: InstantAction - icon: { sprite: Clothing/Shoes/Boots/magboots-syndicate.rsi, state: icon } - iconOn: Clothing/Shoes/Boots/magboots-syndicate.rsi/icon-on.png + itemIconStyle: BigItem + event: !type:ToggleActionEvent diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml b/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml index 888bcd769cf..fae87172238 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml @@ -88,7 +88,7 @@ - type: NoSlip - type: entity - parent: [ClothingShoesBase, PowerCellSlotSmallItem] + parent: [ClothingShoesBase, PowerCellSlotSmallItem, BaseToggleClothing] id: ClothingShoesBootsSpeed name: speed boots description: High-tech boots woven with quantum fibers, able to convert electricity into pure speed! @@ -100,12 +100,11 @@ map: [ "enum.ToggleVisuals.Layer" ] - type: Clothing sprite: Clothing/Shoes/Boots/speedboots.rsi - - type: ToggleClothingSpeed - toggleAction: ActionToggleSpeedBoots + - type: ToggleClothing + action: ActionToggleSpeedBoots - type: ClothingSpeedModifier walkModifier: 1.5 sprintModifier: 1.5 - enabled: false - type: Appearance - type: GenericVisualizer visuals: @@ -128,10 +127,25 @@ id: ActionToggleSpeedBoots name: Toggle Speed Boots description: Toggles the speed boots on and off. - categories: [ HideSpawnMenu ] components: - type: InstantAction - itemIconStyle: NoItem - event: !type:ToggleClothingSpeedEvent - icon: { sprite: Clothing/Shoes/Boots/speedboots.rsi, state: icon } - iconOn: { sprite: Clothing/Shoes/Boots/speedboots.rsi, state: icon-on } + itemIconStyle: BigItem + event: !type:ToggleActionEvent + +- type: entity + parent: ClothingShoesBase + id: ClothingShoesBootsMoon + name: moon boots + description: Special anti-gravity boots developed with a speciality blend of lunar rock gel. Shipped from the Netherlands. + components: + - type: Sprite + sprite: Clothing/Shoes/Boots/moonboots.rsi + layers: + - state: icon + - type: Clothing + sprite: Clothing/Shoes/Boots/moonboots.rsi + - type: AntiGravityClothing + - type: StaticPrice + price: 75 + - type: Tag + tags: [ ] diff --git a/Resources/Prototypes/Entities/Clothing/base_clothing.yml b/Resources/Prototypes/Entities/Clothing/base_clothing.yml index bc592616be9..3d0732d1cab 100644 --- a/Resources/Prototypes/Entities/Clothing/base_clothing.yml +++ b/Resources/Prototypes/Entities/Clothing/base_clothing.yml @@ -51,3 +51,12 @@ - type: GroupExamine - type: Armor modifiers: {} + +# for clothing that can be toggled, like magboots +- type: entity + abstract: true + id: BaseToggleClothing + components: + - type: ItemToggle + onUse: false # can't really wear it like that + - type: ToggleClothing diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_produce.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_produce.yml index a889b939bde..8e70dd2bbdf 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_produce.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_produce.yml @@ -49,6 +49,7 @@ - FoodPeaPod - FoodPumpkin - CottonBol + - FoodCherry chance: 0.8 offset: 0.0 #rare diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml index 193a3a7db3a..11abd00b275 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Food_Drinks/food_single.yml @@ -30,9 +30,6 @@ - FoodBurgerChicken - FoodBurgerDuck - FoodBurgerCheese - - FoodMeatHawaiianKebab - - FoodMeatKebab - - FoodMeatFiestaKebab - FoodNoodlesBoiled - FoodNoodles - FoodNoodlesCopy @@ -77,11 +74,6 @@ - FoodBurgerSpell - FoodBurgerSuper - FoodBurgerCrazy - - FoodMeatHumanKebab - - FoodMeatLizardtailKebab - - FoodMeatRatKebab - - FoodMeatRatdoubleKebab - - FoodMeatSnakeKebab - FoodPizzaArnoldSlice - FoodPizzaCorncobSlice # Nyanotrasen - Corncob Pizza - FoodTacoRat diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index 3911a686266..3cc6d1e35e6 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -1,3 +1,14 @@ +- type: entity + abstract: true + parent: MarkerBase + id: BaseAntagSpawner + name: ghost role spawn point + components: + - type: GhostRole + raffle: + settings: default + - type: GhostRoleAntagSpawner + - type: entity id: SpawnPointGhostRatKing name: ghost role spawn point @@ -62,16 +73,14 @@ state: narsian - type: entity - categories: [ HideSpawnMenu ] + categories: [ HideSpawnMenu, Spawner ] id: SpawnPointGhostNukeOperative name: ghost role spawn point suffix: nukeops - parent: MarkerBase + parent: BaseAntagSpawner components: - type: GhostRole rules: ghost-role-information-nukeop-rules - raffle: - settings: default - type: GhostRoleMobSpawner prototype: MobHumanNukeOp - type: Sprite @@ -82,25 +91,22 @@ state: radiation - type: entity - categories: [ HideSpawnMenu ] + categories: [ HideSpawnMenu, Spawner ] id: SpawnPointLoneNukeOperative name: ghost role spawn point suffix: loneops - parent: MarkerBase + parent: BaseAntagSpawner components: - type: GhostRole name: ghost-role-information-loneop-name description: ghost-role-information-loneop-description rules: ghost-role-information-loneop-rules - raffle: - settings: default requirements: - !type:CharacterOverallTimeRequirement min: 172800 # DeltaV - 48 hours - !type:DepartmentTimeRequirement # DeltaV - Security dept time requirement department: Security time: 36000 # DeltaV - 10 hours - - type: GhostRoleAntagSpawner - type: Sprite sprite: Markers/jobs.rsi layers: @@ -109,9 +115,9 @@ state: radiation - type: entity - parent: MarkerBase + parent: BaseAntagSpawner id: SpawnPointGhostDragon - categories: [ HideSpawnMenu ] + categories: [ HideSpawnMenu, Spawner ] name: ghost role spawn point suffix: dragon components: @@ -119,8 +125,6 @@ name: ghost-role-information-space-dragon-name description: ghost-role-information-space-dragon-description rules: ghost-role-component-default-rules - raffle: - settings: default - type: GhostRoleMobSpawner prototype: MobDragon - type: Sprite @@ -130,19 +134,16 @@ state: alive - type: entity + parent: BaseAntagSpawner id: SpawnPointGhostSpaceNinja + categories: [ HideSpawnMenu, Spawner ] name: ghost role spawn point suffix: space ninja - parent: MarkerBase components: - type: GhostRole name: ghost-role-information-space-ninja-name description: ghost-role-information-space-ninja-description rules: ghost-role-information-space-ninja-rules - raffle: - settings: default - - type: GhostRoleMobSpawner - prototype: MobHumanSpaceNinja - type: Sprite sprite: Markers/jobs.rsi layers: diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 1d0a8d92280..b1344d825c9 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -114,6 +114,11 @@ - type: PowerCellSlot cellSlotId: cell_slot fitsInCharger: true + - type: ItemToggle + onUse: false # no item-borg toggling sorry + - type: AccessToggle + # TODO: refactor movement to just be based on toggle like speedboots but for the boots themselves + # TODO: or just have sentient speedboots be fast idk - type: PowerCellDraw drawRate: 0.6 - type: ItemSlots @@ -241,9 +246,20 @@ deviceNetId: Wireless receiveFrequencyId: CyborgControl transmitFrequencyId: RoboticsConsole + - type: OnUseTimerTrigger + delay: 10 + examinable: false + beepSound: + path: /Audio/Effects/Cargo/buzz_two.ogg + params: + volume: -4 + # prevent any funnies if someone makes a cyborg item... + - type: AutomatedTimer + - type: ExplodeOnTrigger # explosion does most of its damage in the center and less at the edges - type: Explosive explosionType: Minibomb + deleteAfterExplosion: false # let damage threshold gib the borg totalIntensity: 30 intensitySlope: 20 maxIntensity: 20 @@ -293,6 +309,8 @@ - type: MovementAlwaysTouching - type: Speech speechSounds: SyndieBorg + allowedEmotes: + - Laugh - type: Vocal sounds: Unsexed: UnisexSiliconSyndicate diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 6f6e4f30bd4..f1932e3ece2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1421,7 +1421,7 @@ description: ghost-role-information-monkey-description - type: GhostTakeoverAvailable - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 5 Piercing: 4 @@ -1472,8 +1472,7 @@ components: # make the player a traitor once its taken - type: AutoTraitor - giveUplink: false - giveObjectives: false + profile: TraitorReinforcement - type: entity id: MobMonkeySyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink @@ -1571,7 +1570,7 @@ description: Cousins to the sentient race of lizard people, kobolds blend in with their natural habitat and are as nasty as monkeys; ready to pull out your hair and stab you to death. components: - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 2 Piercing: 7 @@ -1603,8 +1602,7 @@ components: # make the player a traitor once its taken - type: AutoTraitor - giveUplink: false - giveObjectives: false + profile: TraitorReinforcement - type: entity id: MobKoboldSyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink @@ -1746,6 +1744,7 @@ tags: - Trash - VimPilot + - ChefPilot - Mouse - Meat - type: Respirator @@ -1788,6 +1787,11 @@ interfaces: enum.SurgeryUIKey.Key: type: SurgeryBui + - type: FoodSequenceElement + entries: + Taco: RatTaco + Burger: RatBurger + Skewer: RatSkewer - type: entity parent: MobMouse @@ -1856,6 +1860,9 @@ Base: dead-1 Dead: Base: splat-1 + - type: Item + size: Tiny + heldPrefix: 1 - type: entity parent: MobMouse @@ -1882,6 +1889,44 @@ Base: dead-2 Dead: Base: splat-2 + - type: Item + size: Tiny + heldPrefix: 2 + +- type: entity + name: cancer mouse + description: Toxic. Squeak! + parent: MobMouse + id: MobMouseCancer + components: + - type: Sprite + color: LightGreen + - type: PointLight + color: LightGreen + radius: 5 + energy: 5 + netsync: false + - type: RadiationSource + intensity: 0.3 + - type: Bloodstream + bloodReagent: UnstableMutagen + - type: SolutionContainerManager + solutions: + food: + reagents: + - ReagentId: UncookedAnimalProteins + Quantity: 3 + - ReagentId: Uranium + Quantity: 10 + - type: Butcherable + spawned: + - id: FoodMeatRat + amount: 1 + - id: SheetUranium1 + amount: 1 + - type: Damageable + damageContainer: Biological + damageModifierSet: Zombie - type: entity name: lizard #Weh @@ -2403,7 +2448,7 @@ - DoorBumpOpener - FootstepSound - type: Tool # Open door from xeno.yml. - speed: 1.5 + speedModifier: 1.5 qualities: - Prying useSound: @@ -3409,6 +3454,7 @@ - type: Tag tags: - VimPilot + - ChefPilot - Trash - Hamster - Meat diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml b/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml new file mode 100644 index 00000000000..8cbd40b5cc7 --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml @@ -0,0 +1,182 @@ +- type: entity + id: BaseMobAsteroid + parent: + - BaseMob + - MobDamageable + - MobAtmosExposed + - MobCombat + abstract: true + components: + - type: Reactive + groups: + Flammable: [Touch] + Extinguish: [Touch] + Acidic: [Touch, Ingestion] + - type: Body + prototype: Animal + - type: Climbing + - type: NameIdentifier + group: GenericNumber + - type: StatusEffects + allowed: + - SlowedDown + - Stutter + - Stun + - Electrocution + - TemporaryBlindness + - RadiationProtection + - Drowsiness + - type: StandingState + - type: Tag + tags: + - DoorBumpOpener + +- type: entity + id: MobGoliath + parent: BaseMobAsteroid + name: goliath + description: A massive beast that uses long tentacles to ensnare its prey, threatening them is not advised under any conditions. + components: + - type: Sprite + sprite: Mobs/Aliens/Asteroid/goliath.rsi + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: goliath + - type: DamageStateVisuals + states: + Alive: + Base: goliath + Dead: + Base: goliath_dead + - type: MovementSpeedModifier + baseWalkSpeed : 2.50 + baseSprintSpeed : 2.50 + - type: MobThresholds + thresholds: + 0: Alive + 300: Dead + - type: MeleeWeapon + soundHit: + path: "/Audio/Weapons/smash.ogg" + angle: 0 + attackRate: 0.75 + animation: WeaponArcPunch + damage: + types: + Slash: 15 + Piercing: 10 + - type: NpcFactionMember + factions: + - SimpleHostile + - type: HTN + rootTask: + task: GoliathCompound + blackboard: + VisionRadius: !type:Single + 6 + AggroVisionRadius: !type:Single + 10 + - type: NPCUseActionOnTarget + actionId: ActionGoliathTentacle + - type: Tag + tags: + - CannotSuicide + - Goliath + - FootstepSound + - type: NoSlip + - type: Butcherable + spawned: + - id: FoodMeatGoliath + amount: 3 + - id: MaterialGoliathHide1 + +- type: entity + id: ActionGoliathTentacle + name: "[color=red]Tentacle Slam[/color]" + description: Use your tentacles to grab and stun a target player! + components: + - type: EntityWorldTargetAction + raiseOnUser: true + icon: + sprite: Mobs/Aliens/Asteroid/goliath.rsi + state: goliath_tentacle_spawn + iconOn: + sprite: Mobs/Aliens/Asteroid/goliath.rsi + state: goliath_tentacle_wiggle + sound: + path: "/Audio/Weapons/slash.ogg" + event: !type:GoliathSummonTentacleAction + useDelay: 8 + range: 10 + +- type: entity + id: GoliathTentacle + name: tentacle + components: + - type: Transform + anchored: True + - type: Physics + bodyType: Static + canCollide: true + - type: InteractionOutline + - type: Sprite + sprite: Mobs/Aliens/Asteroid/goliath.rsi + layers: + - state: goliath_tentacle_wiggle + - type: StunOnContact + blacklist: + tags: + - Goliath + - type: Fixtures + fixtures: + fix: + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + mask: + - Impassable + layer: + - Impassable + hard: false + - type: TimedDespawn #do this shit by hand because of fucking course. + lifetime: 0.4 + - type: SpawnOnDespawn + prototype: EffectGoliathTentacleRetract + +- type: entity + id: BaseEffectGoliathTentacleSpawn + categories: [ HideSpawnMenu ] + name: tentacle + abstract: true + components: + - type: Transform + anchored: True + - type: Physics + bodyType: Static + canCollide: false + - type: Sprite + sprite: Mobs/Aliens/Asteroid/goliath.rsi + - type: InteractionOutline + - type: TimedDespawn + lifetime: 0.7 + +- type: entity + id: EffectGoliathTentacleSpawn + parent: BaseEffectGoliathTentacleSpawn + categories: [ HideSpawnMenu ] + name: tentacle + components: + - type: Sprite + state: goliath_tentacle_spawn + - type: SpawnOnDespawn + prototype: GoliathTentacle + +- type: entity + id: EffectGoliathTentacleRetract + parent: BaseEffectGoliathTentacleSpawn + categories: [ HideSpawnMenu ] + components: + - type: Sprite + state: goliath_tentacle_retract + - type: EffectVisuals + - type: AnimationPlayer diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index bf13f9dfa49..d241173f2e6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -15,7 +15,7 @@ true - type: NpcFactionMember factions: - - SimpleHostile + - Dragon - type: Sprite drawdepth: Mobs sprite: Mobs/Aliens/Carps/space.rsi diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 8e84f46a693..bf7087fa03e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -25,6 +25,8 @@ - ForcedSleep - TemporaryBlindness - Pacified + - RadiationProtection + - Drowsiness - type: Buckle - type: StandingState - type: Tag @@ -99,6 +101,8 @@ - TemporaryBlindness - Pacified - StaminaModifier + - RadiationProtection + - Drowsiness - type: Bloodstream bloodMaxVolume: 150 - type: MobPrice diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 61f04df68d3..198a7405d9a 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -382,4 +382,206 @@ parent: MobCobraSpace suffix: "Salvage Ruleset" components: - - type: SalvageMobRestrictions \ No newline at end of file + - type: SalvageMobRestrictions + +- type: entity + parent: SimpleSpaceMobBase + id: MobSnail + name: snail + description: Revolting unless you're french. + components: + - type: Body + prototype: Mouse + - type: GhostRole + makeSentient: true + allowSpeech: false + allowMovement: true + name: ghost-role-information-snail-name + description: ghost-role-information-snail-description + rules: ghost-role-information-freeagent-rules + - type: GhostTakeoverAvailable + - type: Sprite + drawdepth: SmallMobs + sprite: Mobs/Animals/snail.rsi + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: snail + - type: Item + size: Tiny + - type: NpcFactionMember + factions: + - Mouse + - type: HTN + rootTask: + task: MouseCompound + - type: Physics + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.2 + density: 100 + mask: + - SmallMobMask + layer: + - SmallMobLayer + - type: MobState + - type: Deathgasp + - type: MobStateActions + actions: + Critical: + - ActionCritSuccumb + - ActionCritFakeDeath + - ActionCritLastWords + - type: MobThresholds + thresholds: + 0: Alive + 10: Critical + 20: Dead + - type: MovementSpeedModifier + baseWalkSpeed : 2 + baseSprintSpeed : 3 + - type: DamageStateVisuals + states: + Alive: + Base: snail + Critical: + Base: dead + Dead: + Base: dead + - type: Food + - type: Thirst + startingThirst: 25 # spawn with Okay thirst state + thresholds: + OverHydrated: 35 + Okay: 25 + Thirsty: 15 + Parched: 10 + Dead: 0 + baseDecayRate: 0.04 + - type: Hunger + currentHunger: 25 # spawn with Okay hunger state + thresholds: + Overfed: 35 + Okay: 25 + Peckish: 15 + Starving: 10 + Dead: 0 + baseDecayRate: 0.1 + - type: Extractable + grindableSolutionName: food + - type: SolutionContainerManager + solutions: + food: + reagents: + - ReagentId: UncookedAnimalProteins + Quantity: 3 + - type: Butcherable + spawned: + - id: FoodMeatSnail + amount: 1 + - type: Tag + tags: + - Trash + - VimPilot + - ChefPilot + - Meat + - type: CombatMode + combatToggleAction: ActionCombatModeToggleOff + - type: Bloodstream + bloodMaxVolume: 30 + bloodReagent: Cryoxadone + - type: CanEscapeInventory + - type: MobPrice + price: 50 + - type: BadFood + - type: NonSpreaderZombie + - type: FireVisuals + sprite: Mobs/Effects/onfire.rsi + normalState: Mouse_burning + - type: Temperature + heatDamageThreshold: 500 + coldDamageThreshold: 0 + - type: Reactive + reactions: + - reagents: [TableSalt, Saline] + methods: [Touch, Ingestion, Injection] + effects: + - !type:HealthChange + scaleByQuantity: true + damage: + types: + Caustic: 1 + - !type:PopupMessage + type: Local + visualType: Large + messages: [ "snail-hurt-by-salt-popup" ] + probability: 0.66 + +- type: entity + parent: MobSnail + id: MobSnailInstantDeath + suffix: Smite + components: + - type: MobStateActions + actions: + Alive: + - ActionSmite + Critical: + - ActionCritSuccumb + - ActionCritFakeDeath + - ActionCritLastWords + - type: Godmode + - type: MovementAlwaysTouching + +- type: entity + parent: MobSnail + id: MobSnailSpeed + suffix: Speed + components: + - type: GhostRole + name: ghost-role-information-snailspeed-name + description: ghost-role-information-snailspeed-description + rules: ghost-role-information-freeagent-rules + - type: Sprite + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: spacesnail + - type: DamageStateVisuals + states: + Alive: + Base: spacesnail + Critical: + Base: spacesnaildead + Dead: + Base: spacesnaildead + - type: MovementSpeedModifier + baseWalkSpeed : 5 #he go fast, also they cant slip so its probably fine. + baseSprintSpeed : 7 +# - type: ActiveJetpack # I think this will need a custom component to not make tests angry. + - type: MovementAlwaysTouching + +- type: entity + parent: MobSnail + id: MobSnailMoth + name: Snoth + components: + - type: Body + prototype: Mothroach + - type: GhostRole + name: ghost-role-information-snoth-name + description: ghost-role-information-snoth-description + rules: ghost-role-information-freeagent-rules + - type: Sprite + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: snoth + - type: DamageStateVisuals + states: + Alive: + Base: snoth + Critical: + Base: snothdead + Dead: + Base: snothdead diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 04c4dd083e0..7358d072360 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -22,7 +22,7 @@ NavSmash: !type:Bool true - type: Tool - speed: 1.5 + speedModifier: 1.5 qualities: - Prying - type: Prying diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml index 37cfa7b46de..bc532e65d7b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml @@ -133,7 +133,7 @@ factions: - PetsNT - type: Tool - speed: 3 + speedModifier: 3 qualities: - Prying - type: Sprite @@ -177,7 +177,7 @@ factions: - PetsNT - type: Tool - speed: 3 + speedModifier: 3 qualities: - Prying - type: Sprite @@ -225,7 +225,7 @@ factions: - PetsNT - type: Tool - speed: 3 + speedModifier: 3 qualities: - Prying - type: Inventory @@ -259,7 +259,7 @@ factions: - PetsNT - type: Tool - speed: 3 + speedModifier: 3 qualities: - Prying - type: Inventory @@ -293,7 +293,7 @@ factions: - PetsNT - type: Tool - speed: 3 + speedModifier: 3 qualities: - Prying - type: Sprite diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index c9aac35732b..c91b209e8c3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -78,8 +78,10 @@ - type: RadarConsole followEntity: true - type: CargoOrderConsole + - type: BankClient - type: CrewMonitoringConsole - type: GeneralStationRecordConsole + canDeleteEntries: true - type: DeviceNetwork deviceNetId: Wireless receiveFrequencyId: CrewMonitor @@ -92,8 +94,7 @@ - type: Stripping - type: SolutionScanner - type: IgnoreUIRange - - type: ShowRevIcons - - type: ShowZombieIcons + - type: ShowAntagIcons - type: Inventory templateId: aghost - type: InventorySlots diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 02e414cb4a2..0f24313712a 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -147,8 +147,6 @@ components: - type: Dragon spawnRiftAction: ActionSpawnRift - - type: GenericAntag - rule: Dragon - type: GuideHelp guides: - MinorAntagonists diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index 51078d1b100..baa0df7b816 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -230,7 +230,7 @@ - type: Hands - type: ComplexInteraction - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 5 Piercing: 4 diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 4284632d288..d76e698183e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -25,10 +25,9 @@ name: syndicate agent suffix: Human, Traitor components: - # make the player a traitor once its taken - - type: AutoTraitor - giveUplink: false - giveObjectives: false + # make the player a traitor once its taken + - type: AutoTraitor + profile: TraitorReinforcement - type: entity parent: MobHumanSyndicateAgentBase @@ -69,28 +68,3 @@ - Syndicate - type: Psionic powerRollMultiplier: 7 - -# Space Ninja -- type: entity - categories: [ HideSpawnMenu ] - name: Space Ninja - parent: MobHuman - id: MobHumanSpaceNinja - components: - - type: RandomHumanoidAppearance - randomizeName: false - - type: Loadout - prototypes: [SpaceNinjaGear] - - type: NpcFactionMember - factions: - - Syndicate - - type: SpaceNinja - - type: GenericAntag - rule: Ninja - - type: AutoImplant - implants: - - DeathAcidifierImplant - - type: RandomMetadata - nameSegments: - - names_ninja_title - - names_ninja diff --git a/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml b/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml index 3fd48858ca8..214d80402f4 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml @@ -1,4 +1,18 @@ -# Random humanoids +## Random humanoids + +- type: randomHumanoidSettings + id: EventHumanoid + components: + - type: RandomHumanoidAppearance + randomizeName: false + - type: GhostTakeoverAvailable + +- type: randomHumanoidSettings + id: EventHumanoidMindShielded + parent: EventHumanoid + components: + - type: MindShield + - type: AntagImmune ## Death Squad @@ -16,9 +30,9 @@ - NamesLastMilitary - type: RandomHumanoidSpawner settings: DeathSquad - - type: InitialInfectedExempt - type: randomHumanoidSettings + parent: EventHumanoidMindShielded id: DeathSquad randomizeName: false speciesBlacklist: @@ -26,20 +40,17 @@ - Monkey # Shitmed Change - Kobold # Shitmed Change components: - - type: MindShield - type: GhostRole name: ghost-role-information-Death-Squad-name description: ghost-role-information-Death-Squad-description raffle: settings: short - - type: GhostTakeoverAvailable - type: Loadout prototypes: [ DeathSquadGear ] - type: RandomMetadata nameSegments: - NamesFirstMilitaryLeader - NamesLastMilitary - - type: InitialInfectedExempt ## ERT Leader @@ -58,7 +69,6 @@ - NamesLastMilitary - type: RandomHumanoidSpawner settings: ERTLeader - - type: InitialInfectedExempt - type: randomHumanoidSettings id: ERTLeader @@ -68,7 +78,6 @@ - Monkey # Shitmed Change - Kobold # Shitmed Change components: - - type: MindShield - type: GhostRole name: ghost-role-information-ert-leader-name description: ghost-role-information-ert-leader-description @@ -81,7 +90,6 @@ nameSegments: - NamesFirstMilitaryLeader - NamesLastMilitary - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTLeaderEVA @@ -94,7 +102,6 @@ state: ertleadereva - type: RandomHumanoidSpawner settings: ERTLeaderEVA - - type: InitialInfectedExempt - type: randomHumanoidSettings id: ERTLeaderEVA @@ -109,7 +116,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTLeaderGearEVA ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTLeaderEVALecter @@ -118,7 +124,6 @@ components: - type: RandomHumanoidSpawner settings: ERTLeaderEVALecter - - type: InitialInfectedExempt - type: randomHumanoidSettings id: ERTLeaderEVALecter @@ -133,7 +138,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTLeaderGearEVALecter ] - - type: InitialInfectedExempt ## ERT Chaplain @@ -170,7 +174,6 @@ - NamesLastMilitary - type: Loadout prototypes: [ ERTChaplainGear ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTChaplainEVA @@ -197,7 +200,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTChaplainGearEVA ] - - type: InitialInfectedExempt ## ERT Janitor @@ -234,7 +236,6 @@ - NamesLastMilitary - type: Loadout prototypes: [ ERTJanitorGear ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTJanitorEVA @@ -261,7 +262,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTJanitorGearEVA ] - - type: InitialInfectedExempt ## ERT Engineer @@ -298,7 +298,6 @@ - NamesLastMilitary - type: Loadout prototypes: [ ERTEngineerGear ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTEngineerEVA @@ -325,7 +324,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTEngineerGearEVA ] - - type: InitialInfectedExempt ## ERT Security @@ -362,7 +360,6 @@ - NamesLastMilitary - type: Loadout prototypes: [ ERTSecurityGear ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTSecurityEVA @@ -389,7 +386,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTSecurityGearEVA ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTSecurityEVALecter @@ -412,7 +408,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTSecurityGearEVALecter ] - - type: InitialInfectedExempt ## ERT Medic @@ -449,7 +444,6 @@ - NamesLastMilitary - type: Loadout prototypes: [ ERTMedicalGear ] - - type: InitialInfectedExempt - type: entity id: RandomHumanoidSpawnerERTMedicalEVA @@ -476,7 +470,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ ERTMedicalGearEVA ] - - type: InitialInfectedExempt ## CBURN @@ -490,7 +483,6 @@ state: cburn - type: RandomHumanoidSpawner settings: CBURNAgent - - type: InitialInfectedExempt - type: randomHumanoidSettings id: CBURNAgent @@ -512,7 +504,6 @@ nameSegments: - NamesFirstMilitary - NamesLastMilitary - - type: InitialInfectedExempt ## Central Command - type: entity @@ -524,7 +515,6 @@ state: centcom - type: RandomHumanoidSpawner settings: CentcomOfficial - - type: InitialInfectedExempt - type: randomHumanoidSettings id: CentcomOfficial @@ -542,7 +532,6 @@ - type: GhostTakeoverAvailable - type: Loadout prototypes: [ CentcomGear ] - - type: InitialInfectedExempt ## Syndicate diff --git a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml index c3ae577120d..9ff065ef01d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml @@ -136,4 +136,4 @@ - type: HumanoidAppearance species: IPC - type: Inventory - templateId: ipc + templateId: ipc \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 07398299f37..a150744651f 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -66,6 +66,7 @@ icon: Interface/Actions/scream.png checkCanInteract: false event: !type:BooActionEvent + startDelay: true useDelay: 120 - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index c19eb1edc4a..affdada43ee 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -141,11 +141,10 @@ - TemporaryBlindness - Pacified - StaminaModifier + - RadiationProtection + - Drowsiness - PsionicsDisabled - PsionicallyInsulated - - type: Reflect - enabled: false - reflectProb: 0 - type: Body prototype: Human requiredLegs: 2 @@ -282,6 +281,8 @@ - StaminaModifier - PsionicsDisabled - PsionicallyInsulated + - RadiationProtection + - Drowsiness - type: Blindable # Other - type: Temperature diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml index 701b8ac28ef..55fda71f0fd 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml @@ -118,6 +118,9 @@ solution: drink - type: FitsInDispenser solution: drink + - type: Tag + tags: + - DrinkGlass # Transformable container - normal glass - type: entity @@ -2369,44 +2372,6 @@ - type: Sprite sprite: Objects/Consumable/Drinks/shake-white.rsi -- type: entity - parent: DrinkGlassBase - id: DrinkRamen - name: cup ramen - description: Just add 10ml boiling water. A taste that reminds you of your school years. - components: - - type: SolutionContainerManager - solutions: - drink: - maxVol: 40 #big cup - reagents: - - ReagentId: DryRamen - Quantity: 25 - - ReagentId: Soysauce - Quantity: 5 - - type: Sprite - sprite: Objects/Consumable/Drinks/ramen.rsi - - type: Tag - tags: - - Trash - - type: SpaceGarbage - -- type: entity - parent: DrinkRamen - id: DrinkHellRamen - name: hell ramen - description: Just add 10ml boiling water. Super spicy flavor. - components: - - type: SolutionContainerManager - solutions: - drink: - maxVol: 40 - reagents: - - ReagentId: DryRamen - Quantity: 25 - - ReagentId: CapsaicinOil - Quantity: 5 - - type: entity parent: DrinkGlass id: DrinkBloodGlass diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml index 7a4983b14d5..1198cf41f26 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml @@ -92,7 +92,7 @@ - type: Tool qualities: - Rolling - speed: 0.75 # not as good as a rolling pin but does the job + speedModifier: 0.75 # not as good as a rolling pin but does the job - type: PhysicalComposition materialComposition: Glass: 100 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml index 728ca962f9f..65259836341 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml @@ -62,7 +62,7 @@ - type: Tool qualities: - Rolling - speed: 0.25 # its small so takes longer to roll the entire dough flat + speedModifier: 0.25 # its small so takes longer to roll the entire dough flat - type: SpaceGarbage - type: TrashOnSolutionEmpty solution: drink @@ -444,6 +444,7 @@ whitelist: tags: - Cola + hideStackVisualsWhenClosed: false - type: StorageFill contents: - id: DrinkColaCan diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml index e40ac40a289..20d99623f0b 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml @@ -34,7 +34,9 @@ flavors: - bread - type: Tag - tags: [] #override bread + tags: + - Bread + - Slice - type: SolutionContainerManager solutions: food: @@ -116,6 +118,8 @@ - type: Tag tags: - Fruit + - Bread + - Slice - type: entity name: cornbread @@ -274,6 +278,8 @@ - type: Tag tags: - Meat + - Bread + - Slice - type: entity name: mimana bread @@ -418,6 +424,8 @@ - type: Tag tags: - Meat + - Bread + - Slice - type: entity name: spider meat bread @@ -476,6 +484,8 @@ - type: Tag tags: - Meat + - Bread + - Slice - type: entity name: tofu bread @@ -585,6 +595,8 @@ - type: Tag tags: - Meat + - Bread + - Slice # Other than bread/slices @@ -594,9 +606,6 @@ id: FoodBreadBaguette description: Bon appétit! components: - - type: Tag - tags: - - Baguette - type: Sprite state: baguette - type: SliceableFood @@ -615,6 +624,21 @@ Quantity: 1 - ReagentId: Blackpepper Quantity: 1 + - type: Clothing + slots: [ BELT ] + equippedPrefix: baguette + quickEquip: false + - type: Item + inhandVisuals: + left: + - state: baguette-inhand-left + right: + - state: baguette-inhand-right + - type: MeleeWeapon + wideAnimationRotation: -120 + damage: + types: + Blunt: 1 # bonk # Tastes like France. - type: entity @@ -824,6 +848,7 @@ tags: - VimPilot - DoorBumpOpener + - Bread - type: CanEscapeInventory baseResistTime: 2 - type: Puller diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml index c939dec52c6..2e23ab4e69a 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml @@ -23,6 +23,9 @@ Quantity: 5 - type: Item size: Normal + - type: Tag + tags: + - Cake - type: entity parent: FoodCakeBase @@ -45,6 +48,10 @@ Quantity: 1 - type: Item size: Tiny + - type: Tag + tags: + - Cake + - Slice # Custom Cake Example @@ -63,6 +70,7 @@ slice: FoodCakeBlueberrySlice - type: Tag tags: + - Cake - Fruit - type: entity @@ -78,7 +86,9 @@ color: blue - type: Tag tags: + - Cake - Fruit + - Slice # Cake @@ -203,6 +213,7 @@ slice: FoodCakeOrangeSlice - type: Tag tags: + - Cake - Fruit - type: entity @@ -214,7 +225,9 @@ state: orange-slice - type: Tag tags: + - Cake - Fruit + - Slice # Tastes like sweetness, cake, oranges. - type: entity @@ -229,6 +242,7 @@ slice: FoodCakeLimeSlice - type: Tag tags: + - Cake - Fruit - type: entity @@ -240,7 +254,9 @@ state: lime-slice - type: Tag tags: + - Cake - Fruit + - Slice # Tastes like sweetness, cake, limes. - type: entity @@ -255,6 +271,7 @@ slice: FoodCakeLemonSlice - type: Tag tags: + - Cake - Fruit - type: entity @@ -266,7 +283,9 @@ state: lemon-slice - type: Tag tags: + - Cake - Fruit + - Slice # Tastes like sweetness, cake, lemons. - type: entity @@ -296,6 +315,7 @@ Quantity: 5 - type: Tag tags: + - Cake - Fruit - type: entity @@ -323,7 +343,9 @@ Quantity: 1 - type: Tag tags: + - Cake - Fruit + - Slice - type: entity name: chocolate cake @@ -379,6 +401,7 @@ slice: FoodCakeAppleSlice - type: Tag tags: + - Cake - Fruit - type: entity @@ -391,7 +414,9 @@ state: apple-slice - type: Tag tags: + - Cake - Fruit + - Slice # Tastes like sweetness, cake, slime. - type: entity @@ -436,6 +461,7 @@ Quantity: 11 - type: Tag tags: + - Cake - Fruit - type: entity @@ -457,7 +483,9 @@ Quantity: 2.2 - type: Tag tags: + - Cake - Fruit + - Slice # Tastes like sweetness, cake, pumpkin. - type: entity @@ -686,6 +714,7 @@ tags: - VimPilot - DoorBumpOpener + - Cake - type: CanEscapeInventory baseResistTime: 2 - type: Puller @@ -752,3 +781,10 @@ color: "#FFFF00" radius: 1.4 energy: 1.4 + - type: Tag + tags: + - Slice + - type: FoodSequenceElement + entries: + Taco: Suppermatter + Burger: SuppermatterBurger diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml index b1bbdfb5305..73483c0cf62 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml @@ -163,9 +163,6 @@ # Nuggets -- type: Tag - id: Nugget - - type: entity name: chicken nugget parent: FoodBakedBase @@ -530,6 +527,9 @@ Quantity: 5 - ReagentId: Theobromine Quantity: 3 + - type: Tag + tags: + - Slice - type: entity name: special brownies @@ -585,6 +585,9 @@ Quantity: 3 - ReagentId: THC Quantity: 25 + - type: Tag + tags: + - Slice - type: entity name: onion rings @@ -609,3 +612,31 @@ Quantity: 1 - ReagentId: Vitamin Quantity: 1 + +- type: entity + name: croissant + parent: FoodBakedBase + id: FoodBakedCroissant + description: Buttery, flaky goodness. + components: + - type: FlavorProfile + flavors: + - bread + - butter + - type: Sprite + state: croissant + - type: SolutionContainerManager + solutions: + food: + maxVol: 7 + reagents: + - ReagentId: Nutriment + Quantity: 3 + - ReagentId: Butter + Quantity: 2 + - ReagentId: Vitamin + Quantity: 1 + - type: DamageOtherOnHit + damage: + types: + Blunt: 0 # so the damage stats icon doesn't immediately give away the syndie ones \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml index 8cd1c5dfab6..717f2629ca7 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml @@ -23,7 +23,8 @@ - ReagentId: Vitamin Quantity: 5 - type: Food #All pies here made with a pie tin; unless you're some kind of savage, you're probably not destroying this when you eat or slice the pie! - trash: FoodPlateTin + trash: + - FoodPlateTin - type: SliceableFood count: 4 - type: Tag @@ -52,6 +53,10 @@ Quantity: 1.2 - ReagentId: Vitamin Quantity: 1 + - type: Tag + tags: + - Pie + - Slice # Pie @@ -94,6 +99,7 @@ tags: - Fruit - Pie + - Slice # Tastes like pie, apple. - type: entity @@ -182,6 +188,7 @@ tags: - Fruit - Pie + - Slice # Tastes like pie, cream, banana. - type: entity @@ -224,6 +231,7 @@ tags: - Fruit - Pie + - Slice # Tastes like pie, blackberries. - type: entity @@ -263,6 +271,7 @@ tags: - Fruit - Pie + - Slice # Tastes like pie, cherries. - type: entity @@ -302,6 +311,7 @@ tags: - Meat - Pie + - Slice # Tastes like pie, meat. - type: entity @@ -342,6 +352,7 @@ tags: - Meat - Pie + - Slice # Tastes like pie, meat, acid. - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml index bdce1d44086..154f34063c8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml @@ -59,6 +59,7 @@ tags: - Pizza - ReptilianFood + - Slice # Pizza @@ -135,6 +136,7 @@ tags: - Meat - Pizza + - Slice # Tastes like crust, tomato, cheese, meat. - type: entity @@ -291,6 +293,7 @@ tags: - Meat - Pizza + - Slice # Tastes like crust, tomato, cheese, meat, laziness. - type: entity @@ -391,6 +394,7 @@ tags: - Meat - Pizza + - Slice # Tastes like crust, tomato, cheese, sausage, sass. - type: entity @@ -411,9 +415,13 @@ - state: pineapple - type: SliceableFood slice: FoodPizzaPineappleSlice + - type: Tag + tags: + - Meat + - Pizza - type: entity - name: slice of pineapple pizza + name: slice of Hawaiian pizza parent: FoodPizzaSliceBase id: FoodPizzaPineappleSlice description: A slice of joy/sin. @@ -432,6 +440,7 @@ tags: - Meat - Pizza + - Slice # Tastes like crust, tomato, cheese, pineapple, ham. #TODO: This is a meme pizza from /tg/. It has specially coded mechanics. @@ -504,6 +513,7 @@ tags: - Meat - Pizza + - Slice # Tastes like crust, tomato, cheese, pepperoni, 9 millimeter bullets. #TODO: Make this do poison damage and make cut pizza slices eventually rot into this. diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml index dc1a4ec74cd..3fbf14a6d60 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml @@ -88,7 +88,8 @@ - sweet - funny - type: Food - trash: FoodTinPeachesTrash + trash: + - FoodTinPeachesTrash - type: Tag tags: - Fruit @@ -110,7 +111,8 @@ - type: Sprite sprite: Objects/Consumable/Food/Tins/maint_peaches.rsi - type: Food - trash: FoodTinPeachesMaintTrash + trash: + - FoodTinPeachesMaintTrash # only exists for backwards compatibility with a few maps, nothing else uses it - type: entity @@ -147,7 +149,8 @@ - salty - cheap - type: Food - trash: FoodTinBeansTrash + trash: + - FoodTinBeansTrash - type: entity parent: FoodTinBaseTrash @@ -176,7 +179,8 @@ - salty - cheap - type: Food - trash: FoodTinMRETrash + trash: + - FoodTinMRETrash - type: Tag tags: - Meat diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml index 22e6fce8e14..58eb02f595a 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml @@ -19,7 +19,71 @@ reagents: - ReagentId: Nutriment Quantity: 6.66 # 1/3 of a loaf of bread, technically losing 0.01 nutriment per batch of three buns over making bread loaves/slices + - type: Butcherable + butcherDelay: 1 + spawned: + - id: FoodBreadBunTop + amount: 1 + - id: FoodBreadBunBottom + amount: 1 +- type: entity + id: FoodBreadBunBottom + parent: FoodBreadSliceBase + name: bottom bun + description: It's time to start building the burger tower. + components: + - type: Item + size: Normal #patch until there is an adequate resizing system in place + - type: Food + - type: Sprite + drawdepth: Mobs + noRot: true + sprite: Objects/Consumable/Food/burger_sequence.rsi + layers: + - state: bun_bottom + - map: ["foodSequenceLayers"] + - type: FoodSequenceStartPoint + key: Burger + maxLayers: 10 + startPosition: 0, 0 + offset: 0, 0.07 + minLayerOffset: -0.05, 0 + maxLayerOffset: 0.05, 0 + nameGeneration: food-sequence-burger-gen + - type: Appearance + - type: FoodMetamorphableByAdding + - type: SolutionContainerManager + solutions: + food: + maxVol: 5 + canReact: false # Dont want cause reactions inside burgers after merging ingredients + reagents: + - ReagentId: Nutriment + Quantity: 3.3 # 1/2 of a bun + +- type: entity + id: FoodBreadBunTop + parent: FoodBreadSliceBase + name: top bun + description: the perfect finish for your burger tower + components: + - type: Food + - type: Sprite + sprite: Objects/Consumable/Food/burger_sequence.rsi + layers: + - state: bun_top + - type: SolutionContainerManager + solutions: + food: + maxVol: 5 + reagents: + - ReagentId: Nutriment + Quantity: 3.3 # 1/2 of a bun + - type: FoodSequenceElement + entries: + Burger: BunTopBurger + # Base - type: entity @@ -620,7 +684,8 @@ description: An elusive rib shaped burger with limited availability across the galaxy. Not as good as you remember it. components: - type: Food - trash: FoodKebabSkewer + trash: + - FoodKebabSkewer - type: FlavorProfile flavors: - bun @@ -919,4 +984,4 @@ - type: Tag tags: - Meat - \ No newline at end of file + diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml index 8cd2fa70ee5..a4593d50063 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml @@ -11,7 +11,8 @@ - Egg - Meat - type: Food - trash: Eggshells + trash: + - Eggshells - type: Sprite sprite: Objects/Consumable/Food/egg.rsi - type: Item diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml index c760e5e3ebb..6465b5d06f6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml @@ -88,7 +88,8 @@ - state: popsicle color: orange - type: Food - trash: FoodFrozenPopsicleTrash + trash: + - FoodFrozenPopsicleTrash - type: entity name: berry creamsicle @@ -103,7 +104,8 @@ - state: popsicle color: red - type: Food - trash: FoodFrozenPopsicleTrash + trash: + - FoodFrozenPopsicleTrash - type: Tag tags: - Fruit @@ -119,7 +121,8 @@ - state: stick - state: jumbo - type: Food - trash: FoodFrozenPopsicleTrash + trash: + - FoodFrozenPopsicleTrash - type: SolutionContainerManager solutions: food: @@ -149,7 +152,8 @@ - state: alpha-filling # color: foo - type: Food - trash: FoodFrozenSnowconeTrash + trash: + - FoodFrozenSnowconeTrash - type: SolutionContainerManager solutions: food: @@ -187,7 +191,8 @@ - state: cone - state: berry - type: Food - trash: FoodFrozenSnowconeTrash + trash: + - FoodFrozenSnowconeTrash - type: Tag tags: - Fruit @@ -203,7 +208,8 @@ - state: cone - state: fruitsalad - type: Food - trash: FoodFrozenSnowconeTrash + trash: + - FoodFrozenSnowconeTrash - type: Tag tags: - Fruit @@ -219,7 +225,8 @@ - state: cone - state: clown - type: Food - trash: FoodFrozenSnowconeTrash + trash: + - FoodFrozenSnowconeTrash - type: entity name: mime snowcone @@ -232,7 +239,8 @@ - state: cone - state: mime - type: Food - trash: FoodFrozenSnowconeTrash + trash: + - FoodFrozenSnowconeTrash - type: entity name: rainbow snowcone @@ -245,7 +253,8 @@ - state: cone - state: rainbow - type: Food - trash: FoodFrozenSnowconeTrash + trash: + - FoodFrozenSnowconeTrash # Trash diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml index c6c7e5677e5..a458147aa39 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml @@ -34,6 +34,9 @@ solution: food - type: TrashOnSolutionEmpty solution: food + - type: Tag + tags: + - Ingredient - type: entity abstract: true @@ -272,6 +275,9 @@ reagents: - ReagentId: Nutriment Quantity: 15 + - type: Tag + tags: + - Ingredient - type: entity name: dough @@ -302,6 +308,9 @@ - dough - type: Sprite state: dough-slice + - type: Tag + tags: + - Slice - type: entity name: cornmeal dough @@ -331,6 +340,9 @@ - dough - type: Sprite state: cornmealdough-slice + - type: Tag + tags: + - Slice - type: entity name: tortilla dough @@ -363,6 +375,9 @@ - type: Construction graph: Tortilla node: start + - type: Tag + tags: + - Slice - type: entity name: flattened tortilla dough @@ -418,6 +433,9 @@ - type: Construction graph: Pizza node: flat + - type: SliceableFood + count: 3 + slice: FoodCroissantRaw - type: entity name: pizza bread @@ -448,6 +466,58 @@ components: - type: Sprite state: butter + - type: Slippery + - type: StepTrigger + intersectRatio: 0.2 + - type: CollisionWake + enabled: false + - type: Physics + bodyType: Dynamic + - type: Fixtures + fixtures: + slips: + shape: + !type:PhysShapeAabb + bounds: "-0.3,-0.2,0.3,0.2" + layer: + - SlipLayer + hard: false + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.3,-0.2,0.3,0.2" + density: 10 + mask: + - ItemMask + - type: SolutionContainerManager + solutions: + food: + maxVol: 18 + reagents: + - ReagentId: Butter + Quantity: 15 + - type: SliceableFood + count: 3 + slice: FoodButterSlice + +- type: entity + name: butter slice + parent: FoodBakingBase + id: FoodButterSlice + description: A pat of delicious, golden, fatty goodness. + components: + - type: Sprite + state: butter-slice + - type: SolutionContainerManager + solutions: + food: + maxVol: 6 + reagents: + - ReagentId: Butter + Quantity: 5 + - type: Tag + tags: + - Slice - type: entity name: stick of cannabis butter @@ -503,6 +573,13 @@ reagents: - ReagentId: Nutriment Quantity: 5 + - type: Tag + tags: + - Slice + - type: FoodSequenceElement + entries: + Taco: CheeseTaco + Burger: CheeseBurger - type: entity name: chèvre log @@ -550,6 +627,9 @@ Quantity: 1 - ReagentId: Vitamin Quantity: 0.2 + - type: Tag + tags: + - Slice - type: entity name: tofu @@ -595,6 +675,9 @@ Quantity: 3 - ReagentId: Nutriment Quantity: 2 + - type: Tag + tags: + - Slice - type: entity name: burned mess @@ -648,6 +731,9 @@ reagents: - ReagentId: CocoaPowder Quantity: 2 + - type: Tag + tags: + - Ingredient - type: entity parent: BaseFoodCondimentPacket @@ -668,3 +754,22 @@ - type: SolutionContainerVisuals maxFillLevels: 2 fillBaseName: packet-solid- + +- type: entity + name: raw croissant + parent: FoodBakingBase + id: FoodCroissantRaw + description: Buttery, flaky goodness waiting to happen. + components: + - type: FlavorProfile + flavors: + - dough + - type: Sprite + state: croissant-raw + - type: SolutionContainerManager + solutions: + food: + maxVol: 4 + reagents: + - ReagentId: Nutriment + Quantity: 3 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml index 74277cf7ff4..530ca126a59 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml @@ -331,7 +331,8 @@ description: BBQ ribs, slathered in a healthy coating of BBQ sauce. The least vegan thing to ever exist. components: - type: Food - trash: FoodKebabSkewer + trash: + - FoodKebabSkewer - type: FlavorProfile flavors: - meaty @@ -593,7 +594,8 @@ description: Buttery. components: - type: Food - trash: FoodPlate + trash: + - FoodPlate - type: FlavorProfile flavors: - corn diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml index 58af9cf3bd8..6b7dc94b070 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml @@ -210,7 +210,6 @@ node: start defaultTarget: filet migrawr - - type: entity name: raw penguin meat parent: FoodMeatRawBase @@ -503,6 +502,9 @@ id: FoodMeatWheat description: This doesn't look like meat, but your standards aren't that high to begin with. components: + - type: FlavorProfile + flavors: + - falsemeat - type: Sprite state: clump - type: SolutionContainerManager @@ -599,6 +601,8 @@ - type: SliceableFood count: 3 slice: FoodMeatTomatoCutlet + - type: StaticPrice + price: 100 - type: entity name: salami @@ -674,6 +678,26 @@ - type: Sprite state: slime +- type: entity + name: raw snail meat + parent: FoodMeatRawBase + id: FoodMeatSnail + description: Improved with salt. + components: + - type: Sprite + state: snail + - type: SolutionContainerManager + solutions: + food: + maxVol: 15 + reagents: + - ReagentId: UncookedAnimalProteins + Quantity: 3 + - ReagentId: Fat + Quantity: 3 + - ReagentId: Water + Quantity: 4 #It makes saline if you add salt! + # Cooked - type: entity @@ -736,6 +760,10 @@ - type: Construction graph: MeatSteak node: meat steak + - type: FoodSequenceElement + entries: + Burger: MeatSteak + Taco: MeatSteak - type: entity name: bacon @@ -764,6 +792,13 @@ Quantity: 5 - ReagentId: Protein Quantity: 5 + - type: Construction + graph: Bacon + node: bacon + - type: FoodSequenceElement + entries: + Burger: MeatBecon + Taco: MeatBecon - type: entity name: cooked bear @@ -793,6 +828,10 @@ - type: Construction graph: BearSteak node: filet migrawr + - type: FoodSequenceElement + entries: + Burger: MeatBearBurger + Taco: MeatBear - type: entity name: penguin filet @@ -821,6 +860,10 @@ - type: Construction graph: PenguinSteak node: cooked penguin + - type: FoodSequenceElement + entries: + Burger: MeatPenguinBurger + Taco: MeatPenguin - type: entity name: cooked chicken @@ -849,6 +892,10 @@ - type: Construction graph: ChickenSteak node: cooked chicken + - type: FoodSequenceElement + entries: + Burger: MeatChicken + Taco: MeatChicken - type: entity name: fried chicken @@ -877,6 +924,10 @@ Quantity: 5 - ReagentId: Protein Quantity: 5 + - type: FoodSequenceElement + entries: + Burger: MeatChicken + Taco: MeatChicken - type: entity name: cooked duck @@ -905,6 +956,10 @@ - type: Construction graph: DuckSteak node: cooked duck + - type: FoodSequenceElement + entries: + Burger: MeatDuck + Taco: MeatDuck - type: entity name: cooked crab @@ -933,6 +988,10 @@ - type: Construction graph: CrabSteak node: cooked crab + - type: FoodSequenceElement + entries: + Burger: MeatCrabBurger + Taco: MeatCrab - type: entity name: goliath steak @@ -959,6 +1018,10 @@ - type: Construction graph: GoliathSteak node: goliath steak + - type: FoodSequenceElement + entries: + Burger: MeatGoliathBurger + Taco: MeatGoliath - type: entity name: rouny steak @@ -989,6 +1052,10 @@ - type: Construction graph: RounySteak node: rouny steak + - type: FoodSequenceElement + entries: + Burger: MeatXeno + Taco: MeatXeno - type: entity name: lizard steak @@ -1018,6 +1085,10 @@ - type: Construction graph: LizardSteak node: lizard steak + - type: FoodSequenceElement + entries: + Burger: MeatLizardBurger + Taco: MeatLizard - type: entity name: boiled spider leg @@ -1040,6 +1111,10 @@ Quantity: 5 - ReagentId: Protein Quantity: 5 + - type: FoodSequenceElement + entries: + Burger: MeatSpiderBurger + Taco: MeatSpider - type: entity name: meatball @@ -1061,6 +1136,39 @@ Quantity: 5 - ReagentId: Protein Quantity: 5 + - type: FoodSequenceElement + entries: + Burger: MeatBall + Taco: MeatBall + +- type: entity + name: boiled snail + parent: FoodMeatBase + id: FoodMeatSnailCooked + description: Improved with salt. + components: + - type: Tag + tags: + - Cooked + - Meat + - type: Sprite + layers: + - state: snail-cooked + - type: SolutionContainerManager + solutions: + food: + maxVol: 15 + reagents: + - ReagentId: Nutriment + Quantity: 3 + - ReagentId: Protein + Quantity: 3 + - ReagentId: Water + Quantity: 4 # makes saline if you add salt! + - type: FoodSequenceElement + entries: + Burger: MeatSnail + Taco: MeatSnail # Cutlets @@ -1087,6 +1195,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: Cutlet + node: start + defaultTarget: cutlet - type: entity name: raw bear cutlet @@ -1112,6 +1224,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: BearCutlet + node: start + defaultTarget: bear cutlet - type: entity name: raw penguin cutlet @@ -1135,6 +1251,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: PenguinCutlet + node: start + defaultTarget: penguin cutlet - type: entity name: raw chicken cutlet @@ -1158,6 +1278,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: ChickenCutlet + node: start + defaultTarget: chicken cutlet - type: entity name: raw duck cutlet @@ -1181,6 +1305,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: DuckCutlet + node: start + defaultTarget: duck cutlet - type: entity name: raw lizard cutlet @@ -1207,6 +1335,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: LizardCutlet + node: start + defaultTarget: lizard cutlet - type: entity name: raw spider cutlet @@ -1229,6 +1361,10 @@ Quantity: 3 - ReagentId: Fat Quantity: 2 + - type: Construction + graph: SpiderCutlet + node: start + defaultTarget: spider cutlet - type: entity name: raw xeno cutlet @@ -1253,6 +1389,10 @@ reagents: - ReagentId: SulfuricAcid Quantity: 20 + - type: Construction + graph: XenoCutlet + node: start + defaultTarget: xeno cutlet - type: entity name: raw killer tomato cutlet @@ -1267,6 +1407,8 @@ - type: Sprite state: salami-slice color: red + - type: StaticPrice + price: 30 - type: entity name: salami slice @@ -1313,6 +1455,13 @@ Quantity: 2 - ReagentId: Protein Quantity: 2 + - type: Construction + graph: Cutlet + node: cutlet + - type: FoodSequenceElement + entries: + Burger: MeatCutlet + Taco: MeatCutlet - type: entity name: bear cutlet @@ -1338,6 +1487,13 @@ Quantity: 2 - ReagentId: Protein Quantity: 2 + - type: Construction + graph: BearCutlet + node: bear cutlet + - type: FoodSequenceElement + entries: + Burger: BearCutletBurger + Taco: BearCutlet - type: entity name: penguin cutlet @@ -1361,6 +1517,13 @@ Quantity: 2 - ReagentId: Protein Quantity: 2 + - type: Construction + graph: PenguinCutlet + node: penguin cutlet + - type: FoodSequenceElement + entries: + Burger: PenguinCutletBurger + Taco: PenguinCutlet - type: entity name: chicken cutlet @@ -1384,6 +1547,13 @@ Quantity: 2 - ReagentId: Protein Quantity: 2 + - type: Construction + graph: ChickenCutlet + node: chicken cutlet + - type: FoodSequenceElement + entries: + Burger: ChickenCutlet + Taco: ChickenCutlet - type: entity name: duck cutlet @@ -1407,6 +1577,13 @@ Quantity: 2 - ReagentId: Protein Quantity: 2 + - type: Construction + graph: DuckCutlet + node: duck cutlet + - type: FoodSequenceElement + entries: + Burger: DuckCutlet + Taco: DuckCutlet - type: entity name: lizard cutlet @@ -1431,6 +1608,13 @@ Quantity: 2 - ReagentId: Protein Quantity: 2 + - type: Construction + graph: LizardCutlet + node: lizard cutlet + - type: FoodSequenceElement + entries: + Burger: LizardCutletBurger + Taco: LizardCutlet - type: entity name: spider cutlet @@ -1453,6 +1637,13 @@ Quantity: 1 - ReagentId: Protein Quantity: 1 + - type: Construction + graph: SpiderCutlet + node: spider cutlet + - type: FoodSequenceElement + entries: + Burger: SpiderCutletBurger + Taco: SpiderCutlet - type: entity name: xeno cutlet @@ -1475,3 +1666,10 @@ Quantity: 1 - ReagentId: Protein Quantity: 1 + - type: Construction + graph: XenoCutlet + node: xeno cutlet + - type: FoodSequenceElement + entries: + Burger: XenoCutlet + Taco: XenoCutlet diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index f9bd17860c8..8fab5db343d 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -56,6 +56,28 @@ tags: - Wheat +- type: entity + name: meatwheat bushel + description: Some blood-drenched wheat stalks. You can crush them into what passes for meat if you squint hard enough. + id: MeatwheatBushel + parent: ProduceBase + components: + - type: Sprite + sprite: Objects/Specific/Hydroponics/meatwheat.rsi + - type: SolutionContainerManager + solutions: + food: + reagents: + - ReagentId: UncookedAnimalProteins + Quantity: 10 + - type: SpawnItemsOnUse + items: + - id: FoodMeatWheat + sound: + path: /Audio/Voice/Slime/slime_squish.ogg + - type: Produce + seedId: meatwheat + - type: entity name: oat bushel description: Eat oats, do squats. @@ -95,6 +117,21 @@ - type: Produce seedId: sugarcane +- type: entity + name: papercane roll + description: Why do we even need to grow paper? + id: Papercane + parent: ProduceBase + components: + - type: Sprite + sprite: Objects/Specific/Hydroponics/papercane.rsi + - type: SolutionContainerManager + - type: Produce + seedId: papercane + - type: Log + spawnedPrototype: SheetPaper1 + spawnCount: 2 + - type: entity parent: FoodProduceBase id: FoodLaughinPeaPod @@ -189,7 +226,7 @@ - type: Produce seedId: nettle - type: MeleeChemicalInjector - transferAmount: 3 #To OD someone you would need 2 nettles and about 6-7 hits, the DOT is likely to crit them if they are running away with almost no health + transferAmount: 3 #TODO someone you would need 2 nettles and about 6-7 hits, the DOT is likely to crit them if they are running away with almost no health solution: food pierceArmor: false - type: Extractable @@ -239,7 +276,8 @@ flavors: - banana - type: Food - trash: TrashBananaPeel + trash: + - TrashBananaPeel - type: SolutionContainerManager solutions: food: @@ -262,6 +300,10 @@ tags: - Fruit - Banana + - type: FoodSequenceElement + entries: + Burger: Banana + Taco: Banana - type: entity name: mimana @@ -274,7 +316,8 @@ - banana - nothing - type: Food - trash: TrashMimanaPeel + trash: + - TrashMimanaPeel - type: SolutionContainerManager solutions: food: @@ -298,6 +341,10 @@ - type: Tag tags: - Fruit + - type: FoodSequenceElement + entries: + Burger: Mimana + Taco: Mimana - type: entity name: banana peel @@ -443,6 +490,10 @@ Quantity: 10 - ReagentId: Oculine Quantity: 2 + - type: FoodSequenceElement + entries: + Burger: CarrotBurger + Taco: Carrot - type: entity name: cabbage @@ -469,6 +520,10 @@ - type: Tag tags: - Vegetable + - type: FoodSequenceElement + entries: + Burger: CabbageBurger + Taco: Cabbage - type: entity name: garlic @@ -497,6 +552,10 @@ - type: Tag tags: - Vegetable + - type: FoodSequenceElement + entries: + Burger: GarlicBurger + Taco: Garlic - type: entity name: lemon @@ -529,6 +588,10 @@ tags: - Lemon - Fruit + - type: FoodSequenceElement + entries: + Burger: Lemon + Taco: Lemon - type: entity name: lemoon @@ -560,6 +623,10 @@ - type: Tag tags: - Fruit + - type: FoodSequenceElement + entries: + Burger: Lemoon + Taco: Lemoon - type: entity name: lime @@ -583,6 +650,10 @@ tags: - Lime - Fruit + - type: FoodSequenceElement + entries: + Burger: Lime + Taco: Lime - type: entity name: orange @@ -605,6 +676,51 @@ - type: Tag tags: - Fruit + - type: FoodSequenceElement + entries: + Burger: Orange + Taco: Orange + +- type: entity + name: extradimensional orange + parent: FoodProduceBase + id: FoodExtradimensionalOrange + description: You can hardly wrap your head around this thing. + components: + - type: FlavorProfile + flavors: + - truenature + - type: SolutionContainerManager + solutions: + food: + maxVol: 14 + reagents: + - ReagentId: Haloperidol + Quantity: 5 + - ReagentId: Nutriment + Quantity: 5 + - ReagentId: Vitamin + Quantity: 4 + - type: Sprite + sprite: Objects/Specific/Hydroponics/extradimensional_orange.rsi + scale: 0.5,0.5 + - type: Produce + seedId: extradimensionalOrange + - type: PotencyVisuals + minimumScale: 0.5 # reduce this in size because the sprite is way too big + maximumScale: 1 + - type: Extractable + juiceSolution: + reagents: + - ReagentId: JuiceOrange + Quantity: 10 + - type: Tag + tags: + - Fruit + - type: FoodSequenceElement + entries: + Burger: ExtradimensionalOrangeBurger + Taco: ExtradimensionalOrange - type: entity name: pineapple @@ -673,6 +789,10 @@ tags: - Potato - Vegetable + - type: FoodSequenceElement + entries: + Burger: Potato + Taco: Potato - type: entity @@ -729,6 +849,11 @@ tags: - Fruit - Vegetable + - type: FoodSequenceElement + entries: + Skewer: TomatoSkewer + Burger: Tomato + Taco: Tomato - type: entity name: blue tomato @@ -774,6 +899,10 @@ tags: - Fruit - Vegetable + - type: FoodSequenceElement + entries: + Burger: BlueTomato + Taco: BlueTomato - type: entity name: blood tomato @@ -817,6 +946,11 @@ tags: - Fruit # Fuck you they're a fruit - Vegetable + - type: FoodSequenceElement + entries: + Skewer: TomatoSkewer + Burger: BloodTomato + Taco: BloodTomato - type: entity name: eggplant @@ -875,6 +1009,50 @@ - type: Tag tags: - Fruit + - type: FoodSequenceElement + entries: + Burger: Apple + Taco: Apple + +- type: entity + name: golden apple + parent: FoodProduceBase + id: FoodGoldenApple + description: It should be shaped like a cube, shouldn't it? + components: + - type: FlavorProfile + flavors: + - apple + - metallic + - type: SolutionContainerManager + solutions: + food: + maxVol: 30 + reagents: + - ReagentId: Nutriment + Quantity: 10 + - ReagentId: Vitamin + Quantity: 4 + - ReagentId: DoctorsDelight + Quantity: 13 + - type: Sprite + sprite: Objects/Specific/Hydroponics/golden_apple.rsi + - type: Produce + seedId: goldenApple + - type: Extractable + juiceSolution: + reagents: + - ReagentId: JuiceApple + Quantity: 10 + - ReagentId: Gold + Quantity: 10 + - type: Tag + tags: + - Fruit + - type: FoodSequenceElement + entries: + Burger: GoldenApple + Taco: GoldenApple - type: entity name: cocoa pod @@ -919,7 +1097,8 @@ flavors: - corn - type: Food - trash: FoodCornTrash + trash: + - FoodCornTrash - type: SolutionContainerManager solutions: food: @@ -945,6 +1124,11 @@ Quantity: 3 - ReagentId: Enzyme Quantity: 2 + - type: FoodSequenceElement + entries: + Burger: Corn + Taco: Corn + Skewer: CornSkewer - type: entity name: corn cob @@ -1068,6 +1252,9 @@ state: slice - type: Extractable grindableSolutionName: food + - type: Tag + tags: + - Slice - type: entity name: pineapple slice @@ -1088,6 +1275,11 @@ - type: Tag tags: - Fruit + - Slice + - type: FoodSequenceElement + entries: + Burger: PineappleSliceBurger + Taco: PineappleSlice - type: entity name: onion slice @@ -1111,6 +1303,14 @@ Quantity: 1 - ReagentId: Vitamin Quantity: 1 + - type: Tag + tags: + - Vegetable + - Slice + - type: FoodSequenceElement + entries: + Burger: OnionSliceBurger + Taco: OnionSlice - type: entity name: red onion slice @@ -1134,6 +1334,14 @@ Quantity: 1 - ReagentId: Vitamin Quantity: 1 + - type: Tag + tags: + - Vegetable + - Slice + - type: FoodSequenceElement + entries: + Burger: OnionRedSliceBurger + Taco: OnionRedSlice - type: entity name: chili pepper @@ -1141,27 +1349,32 @@ id: FoodChiliPepper description: Spicy, best not touch your eyes. components: - - type: FlavorProfile - flavors: - - spicy - - type: SolutionContainerManager - solutions: - food: - maxVol: 18 - reagents: - - ReagentId: Nutriment - Quantity: 4 - - ReagentId: CapsaicinOil - Quantity: 10 - - ReagentId: Vitamin - Quantity: 4 - - type: Sprite - sprite: Objects/Specific/Hydroponics/chili.rsi - - type: Produce - seedId: chili - - type: Tag - tags: - - Vegetable + - type: FlavorProfile + flavors: + - spicy + - type: SolutionContainerManager + solutions: + food: + maxVol: 18 + reagents: + - ReagentId: Nutriment + Quantity: 4 + - ReagentId: CapsaicinOil + Quantity: 10 + - ReagentId: Vitamin + Quantity: 4 + - type: Sprite + sprite: Objects/Specific/Hydroponics/chili.rsi + - type: Produce + seedId: chili + - type: Tag + tags: + - Vegetable + - type: FoodSequenceElement + entries: + Taco: ChiliPepper + Burger: ChiliPepper + Skewer: ChiliPepperSkewer - type: entity name: chilly pepper @@ -1169,25 +1382,30 @@ id: FoodChillyPepper description: Icy hot. components: - - type: FlavorProfile - flavors: - - spicy - - cold - - type: SolutionContainerManager - solutions: - food: - maxVol: 18 - reagents: - - ReagentId: Nutriment - Quantity: 4 - - ReagentId: Frostoil - Quantity: 10 - - ReagentId: Vitamin - Quantity: 4 - - type: Sprite - sprite: Objects/Specific/Hydroponics/chilly.rsi - - type: Produce - seedId: chilly + - type: FlavorProfile + flavors: + - spicy + - cold + - type: SolutionContainerManager + solutions: + food: + maxVol: 18 + reagents: + - ReagentId: Nutriment + Quantity: 4 + - ReagentId: Frostoil + Quantity: 10 + - ReagentId: Vitamin + Quantity: 4 + - type: Sprite + sprite: Objects/Specific/Hydroponics/chilly.rsi + - type: Produce + seedId: chilly + - type: FoodSequenceElement + entries: + Taco: ChillyPepper + Burger: ChillyPepper + Skewer: ChillyPepperSkewer - type: entity name: aloe @@ -1216,6 +1434,10 @@ - type: Tag tags: - Vegetable + - type: FoodSequenceElement + entries: + Taco: Aloe + Burger: Aloe - type: entity name: poppy @@ -1248,6 +1470,10 @@ - type: Tag tags: - Flower # TODO add "RedFlower" or "Poppy" tag, when other color flowers will be + - type: FoodSequenceElement + entries: + Taco: Poppy + Burger: Poppy - type: entity name: hairflower @@ -1284,6 +1510,10 @@ - type: Tag tags: - Flower + - type: FoodSequenceElement + entries: + Taco: Lily + Burger: Lily - type: entity name: lingzhi @@ -1310,6 +1540,10 @@ seedId: lingzhi - type: Extractable grindableSolutionName: food + - type: FoodSequenceElement + entries: + Taco: Lingzhi + Burger: Lingzhi - type: entity name: ambrosia vulgaris @@ -1349,6 +1583,10 @@ - type: Tag tags: - Ambrosia + - type: FoodSequenceElement + entries: + Taco: AmbrosiaVulgaris + Burger: AmbrosiaVulgarisBurger - type: entity name: ambrosia deus @@ -1386,6 +1624,10 @@ - type: Tag tags: - Ambrosia + - type: FoodSequenceElement + entries: + Taco: AmbrosiaDeus + Burger: AmbrosiaDeusBurger - type: entity name: galaxythistle @@ -1413,6 +1655,78 @@ tags: - Galaxythistle - Fruit # Probably? + - type: FoodSequenceElement + entries: + Taco: Galaxythistle + Burger: GalaxythistleBurger + +- type: entity + name: glasstle + parent: FoodProduceBase + id: FoodGlasstle + description: A fragile crystal plant with lot of spiky thorns. + components: + - type: Item + size: Small + sprite: Objects/Specific/Hydroponics/glasstle.rsi + heldPrefix: produce + - type: FlavorProfile + flavors: + - sharp + - type: SolutionContainerManager + solutions: + food: + maxVol: 15 + reagents: + - ReagentId: Razorium + Quantity: 15 + - type: Sprite + sprite: Objects/Specific/Hydroponics/glasstle.rsi + - type: Produce + seedId: glasstle + - type: Extractable + grindableSolutionName: food + - type: Damageable + damageContainer: Inorganic + - type: ToolRefinable + refineResult: + - id: SheetGlass1 + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 10 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: GlassBreak + params: + volume: -4 + - !type:SpawnEntitiesBehavior + spawn: + ShardGlass: + min: 1 + max: 1 + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: DamageOnHit + damage: + types: + Blunt: 10 + - type: MeleeWeapon + wideAnimationRotation: 60 + damage: + types: + Slash: 15 + soundHit: + path: /Audio/Weapons/bladeslice.ogg + - type: Tag + tags: + - Galaxythistle + - type: FoodSequenceElement + entries: + Taco: Glasstle + Burger: GlasstleBurger - type: entity name: fly amanita @@ -1439,6 +1753,10 @@ - type: Extractable grindableSolutionName: food - type: BadFood + - type: FoodSequenceElement + entries: + Taco: FlyAmanita + Burger: FlyAmanita - type: entity name: gatfruit @@ -1463,10 +1781,60 @@ - type: Produce seedId: gatfruit - type: Food - trash: WeaponRevolverPython + trash: + - WeaponRevolverPython - type: Tag tags: - Fruit # It's in the name + - type: FoodSequenceElement + entries: + Taco: Gatfruit + Burger: GatfruitBurger + +- type: entity + name: capfruit + parent: FoodProduceBase + id: FoodRealCapfruit + description: A soft but smooth gun-shaped fruit. + components: + - type: FlavorProfile + flavors: + - plastic + - type: SolutionContainerManager + solutions: + food: + maxVol: 10 + reagents: + - ReagentId: Nutriment + Quantity: 5 + - ReagentId: Sulfur + Quantity: 5 + - type: Sprite + sprite: Objects/Specific/Hydroponics/capfruit.rsi + - type: Produce + seedId: realCapfruit + - type: Food + trash: + - RevolverCapGun + - type: Tag + tags: + - Fruit + - type: FoodSequenceElement + entries: + Taco: Capfruit + Burger: CapfruitBurger + +- type: entity + name: capfruit + parent: FoodRealCapfruit + id: FoodFakeCapfruit + suffix: Fake + components: + - type: Produce + seedId: fakeCapfruit + - type: Food + trash: + - RevolverCapGunFake - type: entity name: rice bushel @@ -1510,6 +1878,10 @@ - type: Tag tags: - Vegetable + - type: FoodSequenceElement + entries: + Taco: Soybeans + Burger: SoybeansBurger - type: entity name: spaceman's trumpet @@ -1538,6 +1910,10 @@ - Flower - type: Instrument #hehe trumpet program: 56 + - type: FoodSequenceElement + entries: + Taco: SpacemansTrumpet + Burger: SpacemansTrumpetBurger - type: entity name: koibean @@ -1566,6 +1942,10 @@ - type: Tag tags: - Vegetable + - type: FoodSequenceElement + entries: + Taco: Koibean + Burger: KoibeanBurger - type: entity name: watermelon @@ -1658,6 +2038,118 @@ reagents: - ReagentId: JuiceWatermelon Quantity: 4 + - type: Tag + tags: + - Fruit + - Slice + - type: FoodSequenceElement + entries: + Burger: WatermelonSliceBurger + Taco: WatermelonSlice + Skewer: WatermelonSliceSkewer + +- type: entity + name: holymelon + parent: [FoodProduceBase, ItemHeftyBase] + id: FoodHolymelon + description: The water within this melon has been blessed by some deity that's particularly fond of watermelon. + components: + - type: Item + size: Small + - type: FlavorProfile + flavors: + - holy + - watermelon + - type: SolutionContainerManager + solutions: + food: + maxVol: 25 + reagents: + - ReagentId: Nutriment + Quantity: 10 + - ReagentId: Vitamin + Quantity: 5 + - ReagentId: HolyWater + Quantity: 10 + - type: Sprite + sprite: Objects/Specific/Hydroponics/holymelon.rsi + - type: Produce + seedId: watermelon + - type: Extractable + juiceSolution: + reagents: + - ReagentId: Wine + Quantity: 20 + - type: Damageable + damageContainer: Biological + - type: DamageOnHighSpeedImpact + minimumSpeed: 0.1 + damage: + types: + Blunt: 1 + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 1 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: desecration + - !type:SpillBehavior + solution: food + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: SliceableFood + count: 5 + slice: FoodHolymelonSlice + - type: Butcherable + butcheringType: Knife + spawned: + - id: ClothingHeadHatHolyWatermelon + - type: Tag + tags: + - Fruit + +- type: entity + name: holymelon slice + parent: ProduceSliceBase + id: FoodHolymelonSlice + description: Juicy golden and red slice. + components: + - type: Item + size: Tiny + - type: FlavorProfile + flavors: + - holy + - watermelon + - type: Sprite + sprite: Objects/Specific/Hydroponics/holymelon.rsi + - type: SolutionContainerManager + solutions: + food: + maxVol: 5 + reagents: + - ReagentId: Nutriment + Quantity: 2 + - ReagentId: Vitamin + Quantity: 1 + - ReagentId: HolyWater + Quantity: 2 + - type: Extractable + juiceSolution: + reagents: + - ReagentId: Wine + Quantity: 4 + - type: Tag + tags: + - Fruit + - Slice + - type: FoodSequenceElement + entries: + Burger: HolymelonSliceBurger + Taco: HolymelonSlice + Skewer: HolymelonSliceSkewer - type: entity name: grapes @@ -1720,6 +2212,10 @@ - type: Tag tags: - Fruit + - type: FoodSequenceElement + entries: + Taco: Berries + Burger: BerriesBurger - type: entity name: bungo fruit @@ -1731,7 +2227,8 @@ flavors: - bungo - type: Food - trash: FoodBungoPit + trash: + - FoodBungoPit - type: SolutionContainerManager solutions: food: @@ -1748,6 +2245,10 @@ - type: Tag tags: - Fruit + - type: FoodSequenceElement + entries: + Taco: Bungo + Burger: Bungo - type: entity name: bungo pit @@ -1803,6 +2304,38 @@ tags: - Vegetable +- type: entity + parent: FoodProduceBase + id: FoodWorldPeas + name: cluster of world peas + description: It's rumored to bring peace to any who consume it. + components: + - type: FlavorProfile + flavors: + - numbingtranquility + - type: SolutionContainerManager + solutions: + food: + maxVol: 8 + reagents: + - ReagentId: Happiness + Quantity: 3 + - ReagentId: Nutriment + Quantity: 3 + - ReagentId: Pax + Quantity: 2 + - type: Sprite + sprite: Objects/Specific/Hydroponics/world_pea.rsi + - type: Produce + seedId: worldPea + - type: Tag + tags: + - Vegetable + - type: FoodSequenceElement + entries: + Taco: Pea + Burger: Pea + - type: entity name: pumpkin parent: FoodProduceBase @@ -1913,3 +2446,70 @@ - CottonBoll - type: Extractable grindableSolutionName: food + +- type: entity + name: cherry + parent: FoodProduceBase + id: FoodCherry + description: Juicy red cherry with a pit inside. + components: + - type: FlavorProfile + flavors: + - cherry + - type: Food + trash: + - TrashCherryPit + - type: SolutionContainerManager + solutions: + food: + maxVol: 8 + reagents: + - ReagentId: Nutriment + Quantity: 3 + - ReagentId: Vitamin + Quantity: 3 + - type: Sprite + sprite: Objects/Specific/Hydroponics/cherry.rsi + - type: Produce + seedId: cherry + - type: Extractable + juiceSolution: + reagents: + - ReagentId: JuiceCherry + Quantity: 5 + - type: Tag + tags: + - Fruit + - type: FoodSequenceElement + entries: + Taco: Cherry + Burger: Cherry + +- type: entity + name: cherry pit + parent: FoodInjectableBase + id: TrashCherryPit + components: + - type: Sprite + sprite: Objects/Specific/Hydroponics/cherry.rsi + state: pit + - type: Item + sprite: Objects/Specific/Hydroponics/cherry.rsi + heldPrefix: pit + - type: Tag + tags: + - Recyclable + - Trash + - type: SolutionContainerManager + solutions: + food: + maxVol: 1 + reagents: + - ReagentId: Toxin + Quantity: 1 + - type: Extractable + grindableSolutionName: food + - type: Seed + seedId: cherry + - type: SpaceGarbage + - type: BadFood diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml index 6c16a541cd9..3f75a5c8c82 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml @@ -1,39 +1,6 @@ # When adding new food also add to random spawner located in Resources\Prototypes\Entities\Markers\Spawners\Random\Food_Drinks\food_single.yml -# Base - -- type: entity - parent: FoodInjectableBase - id: FoodSkewerBase - abstract: true - components: - - type: Food - trash: FoodKebabSkewer - - type: Sprite - sprite: Objects/Consumable/Food/skewer.rsi - - type: SolutionContainerManager - solutions: - food: - maxVol: 10 - reagents: - - ReagentId: Nutriment - Quantity: 8 - - type: Item - size: Small - storedRotation: -90 - # Kebabs -# Thoughts on custom kebab system. Kebab items are separated into layers. Only -# cutlets and specific items should be put on skewers, which is why i've tagged the cutlets -# among other reasons. The system would specify between large and small skewer items, since -# there is only so much space the sprite can hold. There are four "spots" on the skewer for -# items, which means only 2 large items at a time can be on one. For examplehe thought it -# you can put two lizard tails on a skewer with two cutlets inbetween. -# This system of layers and spots also means that skewers can be progressively "eaten", meaning -# that layers are made invisible as it gets eaten. - -# NOTE: Rats take up spots 2 and 4 and tails take up 1 and 3. This is important for sprite layering. - - type: entity name: skewer parent: BaseItem @@ -42,80 +9,49 @@ components: - type: Sprite sprite: Objects/Consumable/Food/skewer.rsi - state: skewer + state: + layers: + - state: skewer + - map: ["foodSequenceLayers"] + - type: LandAtCursor + - type: Fixtures + fixtures: + fix1: + shape: !type:PolygonShape + vertices: + - -0.40,-0.20 + - -0.30,-0.30 + - 0.50,0.10 + - 0.40,0.20 + density: 20 + mask: + - ItemMask + restitution: 0.3 + friction: 0.2 + - type: DamageOtherOnHit + damage: + types: + Piercing: 6 + - type: ThrowingAngle + angle: 245 + - type: EmbeddableProjectile + offset: -0.15,0.0 + - type: MeleeWeapon + wideAnimationRotation: -120 + damage: + types: + Piercing: 8 + angle: 0 + animation: WeaponArcThrust + soundHit: + path: /Audio/Weapons/bladeslice.ogg - type: Tag tags: - Trash - Skewer - -# Custom Kebab Example - -- type: entity - name: Hawaiian kebab - parent: FoodSkewerBase - id: FoodMeatHawaiianKebab - description: A delicious kebab made of pineapple, ham and green peppers. - components: - - type: Sprite - layers: - - state: skewer - - state: skewer-meat-alpha1 - color: yellow - - state: skewer-meat-alpha2 - color: brown - - state: skewer-meat-alpha3 - color: green - - state: skewer-meat-alpha4 - color: brown - - type: Tag - tags: - - Meat - - Fruit - -- type: entity - name: meat kebab - parent: FoodSkewerBase - id: FoodMeatKebab - description: Delicious meat, on a stick. - components: - - type: Sprite - layers: - - state: skewer - - state: skewer-meat1 - - state: skewer-meat2 - - state: skewer-meat3 - - state: skewer-meat4 - - type: Tag - tags: - - Meat - -- type: entity - name: human kebab - parent: FoodMeatKebab - id: FoodMeatHumanKebab - description: Human meat. On a stick! - -- type: entity - name: lizard-tail kebab - parent: FoodMeatKebab - id: FoodMeatLizardtailKebab - description: Severed lizard tail on a stick. - components: - - type: Sprite - layers: - - state: skewer - - state: skewer-tail1 - -- type: entity - name: rat kebab - parent: FoodMeatKebab - id: FoodMeatRatKebab - description: Not so delicious rat meat, on a stick. - components: - - type: Sprite - layers: - - state: skewer - - state: skewer-rat1 + - type: Food + trash: + - FoodKebabSkewer - type: SolutionContainerManager solutions: food: @@ -127,15 +63,14 @@ - type: entity name: double rat kebab - parent: FoodMeatKebab + parent: FoodKebabSkewer id: FoodMeatRatdoubleKebab description: A double serving of not so delicious rat meat, on a stick. components: - type: Sprite layers: - state: skewer - - state: skewer-rat1 - - state: skewer-rat2 + - state: skewer-rat - type: SolutionContainerManager solutions: food: @@ -147,32 +82,39 @@ - type: entity name: fiesta kebab - parent: FoodSkewerBase + parent: FoodKebabSkewer id: FoodMeatFiestaKebab description: Always a cruise ship party somewhere in the world, right? components: - type: Sprite layers: - state: skewer - - state: skewer-pepper4 - - state: skewer-corn2 - - state: skewer-mushroom2 - - state: skewer-tomato1 + - state: skewer-pepper + - state: skewer-corn + - state: skewer-mushroom + - state: skewer-tomato - type: entity name: snake kebab - parent: FoodSkewerBase + parent: FoodKebabSkewer id: FoodMeatSnakeKebab description: Snake meat on a stick. It's a little tough. components: - type: Sprite layers: - state: skewer - - state: skewer-snake1 - - state: skewer-snake2 + - state: skewer-snake - type: SolutionContainerManager solutions: food: - reagents: - - ReagentId: Nutriment - Quantity: 12 + canReact: false # Dont want cause reactions inside skewers after merging ingredients + maxVol: 0 + - type: FoodSequenceStartPoint + key: Skewer + maxLayers: 4 + startPosition: -0.27, -0.19 + inverseLayers: true + offset: 0.2, 0.1 + nameGeneration: food-sequence-skewer-gen + contentSeparator: ", " + allowHorizontalFlip: false \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml index 9e36d34af7c..bdeeeccad47 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml @@ -41,7 +41,8 @@ state: boritos - type: Item - type: Food - trash: FoodPacketBoritosTrash + trash: + - FoodPacketBoritosTrash - type: entity name: C&Ds @@ -56,7 +57,8 @@ state: cnds - type: Item - type: Food - trash: FoodPacketCnDsTrash + trash: + - FoodPacketCnDsTrash - type: entity name: cheesie honkers @@ -72,7 +74,8 @@ state: cheesiehonkers - type: Item - type: Food - trash: FoodPacketCheesieTrash + trash: + - FoodPacketCheesieTrash - type: entity name: chips @@ -89,7 +92,8 @@ state: chips - type: Item - type: Food - trash: FoodPacketChipsTrash + trash: + - FoodPacketChipsTrash - type: entity name: chocolate bar @@ -183,7 +187,8 @@ state: pistachio - type: Item - type: Food - trash: FoodPacketPistachioTrash + trash: + - FoodPacketPistachioTrash - type: Tag tags: - Fruit # Seed of a fruit, you can yell at me @@ -204,7 +209,8 @@ - type: Item heldPrefix: popcorn - type: Food - trash: FoodPacketPopcornTrash + trash: + - FoodPacketPopcornTrash - type: entity name: 4no raisins @@ -219,7 +225,8 @@ state: raisins - type: Item - type: Food - trash: FoodPacketRaisinsTrash + trash: + - FoodPacketRaisinsTrash - type: Tag tags: - Fruit @@ -237,7 +244,8 @@ state: semki - type: Item - type: Food - trash: FoodPacketSemkiTrash + trash: + - FoodPacketSemkiTrash - type: entity name: sus jerky @@ -252,7 +260,8 @@ state: susjerky - type: Item - type: Food - trash: FoodPacketSusTrash + trash: + - FoodPacketSusTrash - type: Tag tags: - Meat @@ -270,7 +279,49 @@ state: syndicakes - type: Item - type: Food - trash: FoodPacketSyndiTrash + trash: + - FoodPacketSyndiTrash + +- type: entity + parent: FoodSnackBase + id: DrinkRamen + name: cup ramen + description: A cheap food with a taste that reminds you of your school years. + components: + - type: RefillableSolution + solution: food + - type: InjectableSolution + solution: food + - type: SolutionContainerManager + solutions: + food: + maxVol: 50 #big cup + reagents: + - ReagentId: DryRamen + Quantity: 30 + - ReagentId: Soysauce + Quantity: 5 + - type: Sprite + state: ramen + - type: Food + trash: + - FoodPacketCupRamenTrash + +- type: entity + parent: DrinkRamen + id: DrinkHellRamen + name: hell ramen + description: Super spicy flavor! + components: + - type: SolutionContainerManager + solutions: + food: + maxVol: 50 + reagents: + - ReagentId: DryRamen + Quantity: 30 + - ReagentId: CapsaicinOil + Quantity: 5 - type: entity name: chow mein @@ -296,7 +347,8 @@ - ReagentId: Soysauce Quantity: 2 - type: Food - trash: FoodPacketChowMeinTrash + trash: + - FoodPacketChowMeinTrash - type: entity name: dan dan noodles @@ -324,7 +376,8 @@ - ReagentId: Soysauce Quantity: 2 - type: Food - trash: FoodPacketDanDanTrash + trash: + - FoodPacketDanDanTrash - type: entity name: fortune cookie @@ -349,7 +402,8 @@ heldPrefix: packet size: Tiny - type: Food - trash: FoodCookieFortune + trash: + - FoodCookieFortune - type: entity id: FoodSnackNutribrick @@ -573,6 +627,15 @@ - type: Sprite state: syndicakes-trash +- type: entity + categories: [ HideSpawnMenu ] + parent: FoodPacketTrash + id: FoodPacketCupRamenTrash + name: empty cup ramen + components: + - type: Sprite + state: ramen + - type: entity categories: [ HideSpawnMenu ] parent: FoodPacketTrash @@ -642,7 +705,8 @@ state: proteinbar - type: Item - type: Food - trash: FoodPacketProteinbarTrash + trash: + - FoodPacketProteinbarTrash - type: SolutionContainerManager solutions: food: @@ -780,7 +844,8 @@ state: tidegobs - type: Item - type: Food - trash: FoodPacketTidegobsTrash + trash: + - FoodPacketTidegobsTrash - type: entity parent: FoodSnackBase @@ -796,7 +861,8 @@ state: saturno - type: Item - type: Food - trash: FoodPacketSaturnosTrash + trash: + - FoodPacketSaturnosTrash - type: entity parent: FoodSnackBase @@ -812,7 +878,8 @@ state: jupiter - type: Item - type: Food - trash: FoodPacketJoveGelloTrash + trash: + - FoodPacketJoveGelloTrash - type: entity parent: FoodSnackBase @@ -828,7 +895,8 @@ state: pluto - type: Item - type: Food - trash: FoodPacketPlutoniumrodsTrash + trash: + - FoodPacketPlutoniumrodsTrash - type: entity parent: FoodSnackBase @@ -846,7 +914,8 @@ state: mars - type: Item - type: Food - trash: FoodPacketMarsFroukaTrash + trash: + - FoodPacketMarsFroukaTrash - type: entity parent: FoodSnackBase @@ -862,7 +931,8 @@ state: venus - type: Item - type: Food - trash: FoodPacketVenusTrash + trash: + - FoodPacketVenusTrash - type: entity parent: FoodSnackBase @@ -879,7 +949,8 @@ state: oort - type: Item - type: Food - trash: FoodPacketOortrocksTrash + trash: + - FoodPacketOortrocksTrash # Weeb Vend - type: entity @@ -890,13 +961,14 @@ components: - type: FlavorProfile flavors: - - spicy + - spicy - type: Sprite sprite: Objects/Consumable/Food/bay_food.rsi state: weebonuts - type: Item - type: Food - trash: FoodPacketRedalertnutsTrash + trash: + - FoodPacketRedalertnutsTrash - type: entity parent: FoodSnackBase @@ -942,7 +1014,8 @@ state: chocobanana - type: Item - type: Food - trash: FoodPacketStickTrash + trash: + - FoodPacketStickTrash - type: entity parent: FoodSnackBase @@ -959,7 +1032,8 @@ state: dango - type: Item - type: Food - trash: FoodPacketStickTrash + trash: + - FoodPacketStickTrash # Old food - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml index 6b96f3bcb36..d5cb4311c46 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml @@ -8,7 +8,8 @@ - type: Item storedRotation: -90 - type: Food - trash: FoodBowlBig + trash: + - FoodBowlBig utensil: Spoon - type: SolutionContainerManager solutions: @@ -970,7 +971,8 @@ flavors: - miso - type: Food - trash: FoodBowlFancy + trash: + - FoodBowlFancy - type: Sprite layers: - state: bowl2 @@ -1225,3 +1227,33 @@ - Fruit - Soup # Tastes like bungo, hot curry. + +- type: entity + name: escargot + parent: FoodBowlBase + id: FoodSoupEscargot + description: A creamy and rich bowl of snails, bon appetit! + components: + - type: FlavorProfile + flavors: + - creamy + - slimy + - type: Sprite + layers: + - state: bowl + - state: escargot + - type: SolutionContainerManager + solutions: + food: + maxVol: 20 + reagents: + - ReagentId: Nutriment + Quantity: 6 + - ReagentId: Vitamin + Quantity: 6 + - ReagentId: Allicin + Quantity: 3 + - type: Tag + tags: + - Meat + - Soup diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/taco.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/taco.yml index 9df2f3039e9..2a83b14d037 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/taco.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/taco.yml @@ -9,18 +9,33 @@ - type: Item storedRotation: -90 - type: Food + transferAmount: 3 - type: Sprite - sprite: Objects/Consumable/Food/taco.rsi + sprite: Objects/Consumable/Food/taco_sequence.rsi layers: - - state: tacoshell + - state: tacoshell_back + - map: ["foodSequenceLayers"] + - state: tacoshell_forward - type: SolutionContainerManager solutions: food: + canReact: false # Dont want cause reactions inside tacos after merging ingredients maxVol: 10 reagents: - ReagentId: Nutriment - Quantity: 6.66 # Just using the same values as the bun values, since the recipe for taco shells is roughly the same as buns. -# Base + Quantity: 6.66 + - type: FoodSequenceStartPoint + key: Taco + maxLayers: 3 + startPosition: -0.15, 0 + offset: 0.15, 0 + minLayerOffset: 0, 0 + maxLayerOffset: 0, 0.05 + nameGeneration: food-sequence-taco-gen + contentSeparator: ", " + - type: Appearance + +# Old tacos - type: entity parent: FoodInjectableBase @@ -167,4 +182,4 @@ state: softtaco - type: Tag tags: - - Meat + - Meat \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml index a34af1c9d82..ac5b3a42a5c 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml @@ -617,10 +617,6 @@ materialRequirements: Steel: 1 Cable: 2 - - type: ReverseEngineering # Delta - difficulty: 3 - recipes: - - CrewMonitoringServerMachineCircuitboard - type: entity id: CryoPodMachineCircuitboard @@ -649,7 +645,7 @@ - type: Sprite state: medical - type: MachineBoard - prototype: chem_master + prototype: ChemMaster requirements: Capacitor: 1 materialRequirements: @@ -1236,6 +1232,21 @@ tags: - MicrowaveMachineBoard +- type: entity + id: SyndicateMicrowaveMachineCircuitboard + parent: BaseMachineCircuitboard + name: donk co. microwave machine board + components: + - type: Sprite + state: service + - type: MachineBoard + prototype: SyndicateMicrowave + requirements: + Capacitor: 1 + materialRequirements: + Glass: 2 + Cable: 2 + - type: entity id: FatExtractorMachineCircuitboard parent: BaseMachineCircuitboard @@ -1590,6 +1601,23 @@ recipes: - ShuttleGunKineticCircuitboard +- type: entity + parent: BaseMachineCircuitboard + id: StationAnchorCircuitboard + name: station anchor machine board + description: A machine printed circuit board for a station anchor. + components: + - type: MachineBoard + prototype: StationAnchor + requirements: + Capacitor: 4 + MatterBin: 3 + materialRequirements: + Steel: 10 + Glass: 5 + CableHV: 8 + Uranium: 2 + - type: entity parent: BaseMachineCircuitboard id: ReagentGrinderIndustrialMachineCircuitboard @@ -1641,7 +1669,3 @@ materialRequirements: Steel: 5 Cable: 1 - - type: ReverseEngineering # Nyano - difficulty: 2 - recipes: - - TraversalDistorterMachineCircuitboard diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index 7681a459cb1..e1fd49972c7 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -412,10 +412,6 @@ price: 150 - type: ComputerBoard prototype: ComputerMassMedia - - type: ReverseEngineering # Nyano - difficulty: 3 - recipes: - - ComputerMassMediaCircuitboard - type: entity parent: BaseComputerCircuitboard diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml index 660a8c9e60c..832bf0fe745 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml @@ -1,3 +1,5 @@ + + - type: entity parent: BaseItem id: ReinforcementRadioSyndicate @@ -54,6 +56,23 @@ prototype: MobMonkeySyndicateAgentNukeops selectablePrototypes: ["SyndicateMonkeyNukeops", "SyndicateKoboldNukeops"] +- type: entity + parent: ReinforcementRadioSyndicate + id: ReinforcementRadioSyndicateSyndiCat + name: syndicat reinforcement radio + description: Calls in a faithfully trained cat with a microbomb to assist you. + components: + - type: GhostRole + name: ghost-role-information-SyndiCat-name + description: ghost-role-information-SyndiCat-description + rules: ghost-role-information-syndicate-reinforcement-rules + raffle: + settings: default + - type: GhostRoleMobSpawner + prototype: MobCatSyndy + - type: EmitSoundOnUse + sound: /Audio/Animals/cat_meow.ogg + - type: entity parent: ReinforcementRadioSyndicate id: ReinforcementRadioSyndicateCyborgAssault # Reinforcement radio exclusive to nukeops uplink diff --git a/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml b/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml new file mode 100644 index 00000000000..259323fede3 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml @@ -0,0 +1,11 @@ +- type: entity + abstract: true + parent: [BaseItem, PowerCellSlotSmallItem] + id: BaseHandheldComputer + components: + - type: ActivatableUIRequiresPowerCell + - type: ItemToggle + onUse: false # above component does the toggling + - type: PowerCellDraw + drawRate: 0 + useRate: 20 diff --git a/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml index 0563826a570..e9108fd341b 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml @@ -92,4 +92,42 @@ - type: LogProbeCartridge - type: GuideHelp guides: - - Forensics + - Forensics + +- type: entity + parent: BaseItem + id: MedTekCartridge + name: MedTek cartridge + description: A program that provides medical diagnostic tools. + components: + - type: Sprite + sprite: Objects/Devices/cartridge.rsi + state: cart-med + - type: Icon + sprite: Objects/Devices/cartridge.rsi + state: cart-med + - type: Cartridge + programName: med-tek-program-name + icon: + sprite: Objects/Specific/Medical/healthanalyzer.rsi + state: icon + - type: MedTekCartridge + +- type: entity + parent: BaseItem + id: AstroNavCartridge + name: AstroNav cartridge + description: A program for navigation that provides GPS coordinates. + components: + - type: Sprite + sprite: Objects/Devices/cartridge.rsi + state: cart-nav + - type: Icon + sprite: Objects/Devices/cartridge.rsi + state: cart-nav + - type: Cartridge + programName: astro-nav-program-name + icon: + sprite: Objects/Devices/gps.rsi + state: icon + - type: AstroNavCartridge diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 2dbcfc60ab3..055e8062da9 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -1,6 +1,6 @@ - type: entity abstract: true - parent: BaseItem + parent: [ BaseItem, StorePresetUplink ] #PDA's have uplinks so they have to inherit the data. id: BasePDA name: PDA description: Personal Data Assistant. @@ -76,6 +76,7 @@ - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge + - NanoChatCartridge # DeltaV cartridgeSlot: priority: -1 name: device-pda-slot-component-slot-name-cartridge @@ -118,10 +119,14 @@ id: BaseMedicalPDA abstract: true components: - - type: HealthAnalyzer - scanDelay: 1 - scanningEndSound: - path: "/Audio/Items/Medical/healthscanner.ogg" + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - MedTekCartridge + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -154,7 +159,7 @@ parent: BaseMedicalPDA id: MedicalInternPDA name: medical intern PDA - description: Why isn't it white? Has a built-in health analyzer. + description: Why isn't it white? components: - type: Pda id: MedicalInternIDCard @@ -195,6 +200,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -372,6 +378,8 @@ - NotekeeperCartridge - NewsReaderCartridge - MailMetricsCartridge + - NanoChatCartridge + - StockTradingCartridge # DeltaV - StockTrading - type: entity name: LO special PDA unit # DeltaV - Logistics Department replacing Cargo @@ -392,7 +400,9 @@ - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - - MailMetricsCartridge + - MailMetricsCartridge # DeltaV - MailMetrics courier tracker + - StockTradingCartridge # DeltaV - StockTrading + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -407,6 +417,13 @@ borderColor: "#e39751" - type: Icon state: pda-cargo + - type: CartridgeLoader # DeltaV + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - StockTradingCartridge # DeltaV - StockTrading + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -422,6 +439,14 @@ accentVColor: "#8900c9" - type: Icon state: pda-miner + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - AstroNavCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -462,6 +487,7 @@ - NotekeeperCartridge - NewsReaderCartridge - GlimmerMonitorCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -489,6 +515,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -642,7 +669,7 @@ parent: BaseMedicalPDA id: CMOPDA name: chief medical officer PDA - description: Extraordinarily shiny and sterile. Has a built-in health analyzer. + description: Extraordinarily shiny and sterile. components: - type: Pda id: CMOIDCard @@ -686,7 +713,7 @@ parent: BaseMedicalPDA id: MedicalPDA name: medical PDA - description: Shiny and sterile. Has a built-in health analyzer. + description: Shiny and sterile. components: - type: Pda id: MedicalIDCard @@ -710,7 +737,7 @@ parent: BaseMedicalPDA id: ParamedicPDA name: paramedic PDA - description: Shiny and sterile. Has a built-in rapid health analyzer. + description: Shiny and sterile. components: - type: Pda id: ParamedicIDCard @@ -769,6 +796,7 @@ - NotekeeperCartridge - NewsReaderCartridge - GlimmerMonitorCartridge + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -791,6 +819,7 @@ - NotekeeperCartridge - NewsReaderCartridge - GlimmerMonitorCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -812,6 +841,7 @@ - NotekeeperCartridge - NewsReaderCartridge - GlimmerMonitorCartridge + - NanoChatCartridge # DeltaV - type: entity parent: SciencePDA @@ -844,11 +874,13 @@ state: pda-hos - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - - CrewManifestCartridge - - NotekeeperCartridge - - NewsReaderCartridge - - CrimeAssistCartridge - - SecWatchCartridge + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - CrimeAssistCartridge # DeltaV + - SecWatchCartridge # DeltaV: SecWatch replaces WantedList + - LogProbeCartridge + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -871,6 +903,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -893,6 +926,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -914,6 +948,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -933,6 +968,15 @@ borderColor: "#00842e" - type: Icon state: pda-centcom + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - SecWatchCartridge # DeltaV: SecWatch replaces WantedList + - LogProbeCartridge + - NanoChatCartridge # DeltaV - type: entity parent: CentcomPDA @@ -946,11 +990,16 @@ scanDelay: 0 - type: CartridgeLoader uiKey: enum.PdaUiKey.Key + notificationsEnabled: false + diskSpace: 10 # DeltaV preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - LogProbeCartridge + - SecWatchCartridge # DeltaV: SecWatch replaces WantedList + - StockTradingCartridge # Delta-V + - NanoChatCartridge # DeltaV - type: entity parent: CentcomPDA @@ -1033,6 +1082,7 @@ uiKey: enum.PdaUiKey.Key preinstalled: - NotekeeperCartridge + - NanoChatCartridge # DeltaV cartridgeSlot: priority: -1 name: Cartridge @@ -1060,6 +1110,15 @@ accentVColor: "#447987" - type: Icon state: pda-ert + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - SecWatchCartridge # DeltaV: SecWatch replaces WantedList + - LogProbeCartridge + - NanoChatCartridge # DeltaV - type: entity parent: ERTLeaderPDA @@ -1096,14 +1155,19 @@ id: ERTMedicPDA name: ERT Medic PDA suffix: Medic - description: Red for firepower, it's shiny and sterile. Has a built-in rapid health analyzer. + description: Red for firepower, it's shiny and sterile. components: - type: Pda id: ERTMedicIDCard - - type: HealthAnalyzer - scanDelay: 1 - scanningEndSound: - path: "/Audio/Items/Medical/healthscanner.ogg" + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - MedTekCartridge + - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: ERTLeaderPDA @@ -1129,7 +1193,7 @@ accentVColor: "#447987" - type: entity - parent: BasePDA + parent: BaseMedicalPDA id: PsychologistPDA name: psychologist PDA description: Looks immaculately cleaned. @@ -1168,6 +1232,13 @@ borderColor: "#3f3f74" - type: Icon state: pda-reporter + - type: CartridgeLoader # DeltaV + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - StockTradingCartridge # DeltaV - StockTrading + - NanoChatCartridge # DeltaV - type: entity parent: BasePDA @@ -1213,17 +1284,19 @@ state: pda-detective - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - - CrewManifestCartridge - - NotekeeperCartridge - - NewsReaderCartridge - - CrimeAssistCartridge - - SecWatchCartridge + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - CrimeAssistCartridge # DeltaV + - SecWatchCartridge # DeltaV: SecWatch replaces WantedList + - LogProbeCartridge + - NanoChatCartridge # DeltaV - type: entity parent: BaseMedicalPDA id: BrigmedicPDA name: brigmedic PDA - description: I wonder whose pulse is on the screen? I hope he doesnt stop... PDA has a built-in health analyzer. + description: I wonder whose pulse is on the screen? I hope it doesn't stop... components: - type: Pda id: BrigmedicIDCard @@ -1242,11 +1315,12 @@ state: pda-brigmedic - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - - CrewManifestCartridge - - NotekeeperCartridge - - NewsReaderCartridge - - CrimeAssistCartridge - - SecWatchCartridge + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - CrimeAssistCartridge # DeltaV + - SecWatchCartridge # DeltaV: SecWatch replaces WantedList + - NanoChatCartridge # DeltaV - type: entity parent: ClownPDA @@ -1306,7 +1380,7 @@ parent: BaseMedicalPDA id: SeniorPhysicianPDA name: senior physician PDA - description: Smells faintly like iron and chemicals. Has a built-in health analyzer. + description: Smells faintly like iron and chemicals. components: - type: Pda id: SeniorPhysicianIDCard @@ -1345,6 +1419,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: SyndiPDA @@ -1381,6 +1456,8 @@ uiKey: enum.PdaUiKey.Key preinstalled: - NotekeeperCartridge + - MedTekCartridge + - NanoChatCartridge cartridgeSlot: priority: -1 name: Cartridge diff --git a/Resources/Prototypes/Entities/Objects/Devices/radio.yml b/Resources/Prototypes/Entities/Objects/Devices/radio.yml index 43f84fe404e..77b6cac2d37 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/radio.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/radio.yml @@ -4,6 +4,7 @@ parent: BaseItem id: RadioHandheld components: + - type: TelecomExempt - type: RadioMicrophone broadcastChannel: Handheld - type: RadioSpeaker @@ -39,4 +40,4 @@ sprite: Objects/Devices/securityhandy.rsi - type: Item sprite: Objects/Devices/securityhandy.rsi - heldPrefix: walkietalkie \ No newline at end of file + heldPrefix: walkietalkie diff --git a/Resources/Prototypes/Entities/Objects/Devices/station_beacon.yml b/Resources/Prototypes/Entities/Objects/Devices/station_beacon.yml index 28d6ddea691..897250e94bb 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/station_beacon.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/station_beacon.yml @@ -430,6 +430,14 @@ - type: NavMapBeacon defaultText: station-beacon-gravgen +- type: entity + parent: DefaultStationBeaconEngineering + id: DefaultStationBeaconAnchor + suffix: Anchor + components: + - type: NavMapBeacon + defaultText: station-beacon-anchor + - type: entity parent: DefaultStationBeaconEngineering id: DefaultStationBeaconSingularity diff --git a/Resources/Prototypes/Entities/Objects/Devices/station_map.yml b/Resources/Prototypes/Entities/Objects/Devices/station_map.yml index 0d2f890a1d3..54fc4a70c5a 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/station_map.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/station_map.yml @@ -1,9 +1,9 @@ - type: entity + parent: BaseItem id: BaseHandheldStationMap name: station map description: Displays a readout of the current station. abstract: true - parent: BaseItem components: - type: StationMap - type: Sprite @@ -34,14 +34,9 @@ - type: entity id: HandheldStationMap parent: - - BaseHandheldStationMap - - PowerCellSlotSmallItem + - BaseHandheldStationMap + - BaseHandheldComputer suffix: Handheld, Powered - components: - - type: PowerCellDraw - drawRate: 0 - useRate: 20 - - type: ActivatableUIRequiresPowerCell - type: entity id: HandheldStationMapEmpty diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index 3a30afc4a7f..a1500ea6896 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -36,6 +36,7 @@ id: TranslatorPoweredBase parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ] components: + - type: ItemToggle - type: PowerCellDraw drawRate: 1 - type: ItemSlots diff --git a/Resources/Prototypes/Entities/Objects/Fun/darts.yml b/Resources/Prototypes/Entities/Objects/Fun/darts.yml index 4c7ae68420b..b02797917c6 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/darts.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/darts.yml @@ -10,6 +10,7 @@ offset: 0.0,0.0 - type: ThrowingAngle angle: 315 + - type: LandAtCursor - type: Fixtures fixtures: fix1: diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml index 0adbc492308..806470607a0 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml @@ -17,7 +17,7 @@ type: InstrumentBoundUserInterface requireInputValidation: false enum.StationMapUiKey.Key: - type: UntrackedStationMapBoundUserInterface + type: StationMapBoundUserInterface requireInputValidation: false - type: Sprite sprite: Objects/Fun/pai.rsi diff --git a/Resources/Prototypes/Entities/Objects/Magic/books.yml b/Resources/Prototypes/Entities/Objects/Magic/books.yml index 7697e46b32a..5651e3e50f6 100644 --- a/Resources/Prototypes/Entities/Objects/Magic/books.yml +++ b/Resources/Prototypes/Entities/Objects/Magic/books.yml @@ -52,7 +52,6 @@ - type: Store refundAllowed: true ownerOnly: true # get your own tome! - preset: StorePresetSpellbook balance: WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are @@ -66,7 +65,6 @@ - type: Store refundAllowed: false ownerOnly: true # get your own tome! - preset: StorePresetSpellbook balance: WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are diff --git a/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml b/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml index 2880bd00ec4..2c818e7c973 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml @@ -6,6 +6,8 @@ description: A small piece of crystal. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite layers: - sprite: Objects/Materials/Shards/crystal.rsi diff --git a/Resources/Prototypes/Entities/Objects/Materials/materials.yml b/Resources/Prototypes/Entities/Objects/Materials/materials.yml index fde9aa692d6..75a02f05a49 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/materials.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/materials.yml @@ -12,7 +12,6 @@ - type: Tag tags: - RawMaterial - - NoPaint - type: Damageable damageContainer: Inorganic - type: Destructible @@ -118,6 +117,9 @@ - type: Appearance - type: Food requiresSpecialDigestion: true + - type: FlavorProfile + flavors: + - fiber - type: SolutionContainerManager solutions: food: @@ -351,8 +353,17 @@ components: - type: Stack stackType: Diamond + baseLayer: base + layerStates: + - diamond + - diamond_2 + - diamond_3 - type: Sprite state: diamond + layers: + - state: diamond + map: ["base"] + - type: Appearance - type: Item heldPrefix: diamond - type: Extractable @@ -363,6 +374,10 @@ reagents: - ReagentId: Carbon Quantity: 20 + - type: Material + - type: PhysicalComposition + materialComposition: + Diamond: 100 - type: entity parent: MaterialDiamond @@ -393,6 +408,9 @@ - type: Appearance - type: Food requiresSpecialDigestion: true + - type: FlavorProfile + flavors: + - fiber - type: SolutionContainerManager solutions: food: @@ -494,13 +512,14 @@ layerStates: - bananium - bananium_1 - - type: RadiationSource + - type: RadiationSource #Delta V - Adds radiation intensity: 0.1 - type: FlavorProfile flavors: - banana - type: Food - trash: TrashBananiumPeel + trash: + - TrashBananiumPeel - type: BadFood - type: SolutionContainerManager solutions: @@ -624,3 +643,59 @@ components: - type: Stack count: 1 + +- type: entity + parent: MaterialBase + id: MaterialGunpowder + name: gunpowder + description: An explosive compound. + components: + - type: Stack + stackType: Gunpowder + count: 1 + - type: Sprite + sprite: Objects/Misc/reagent_fillings.rsi + state: powderpile + color: darkgray + - type: PhysicalComposition + materialComposition: + Gunpowder: 100 + - type: Item + size: Tiny + +- type: entity + parent: MaterialBase + id: MaterialGoliathHide + name: goliath hide plates + description: Pieces of a goliath's rocky hide, these might be able to make your suit a bit more durable to attack from the local fauna. + suffix: Full + components: + - type: Sprite + sprite: Objects/Materials/hide.rsi + layers: + - state: goliath_hide + map: [ "base" ] + - type: StaticPrice + price: 0 + - type: StackPrice + price: 1500 + - type: Appearance + - type: Stack + stackType: GoliathHide + baseLayer: base + layerStates: + - goliath_hide + - goliath_hide_2 + - goliath_hide_3 + - type: Item + size: Large + shape: + - 0,0,2,2 + +- type: entity + parent: MaterialGoliathHide + id: MaterialGoliathHide1 + suffix: 1 + components: + - type: Stack + count: 1 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Materials/ore.yml b/Resources/Prototypes/Entities/Objects/Materials/ore.yml index 521d3cf1ec8..690e7e36d26 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/ore.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/ore.yml @@ -66,6 +66,37 @@ - type: Stack count: 1 +- type: entity + parent: OreBase + id: DiamondOre + name: diamond ore + suffix: Full + components: + - type: Stack + stackType: DiamondOre + - type: Sprite + state: diamond + - type: Material + - type: PhysicalComposition + materialComposition: + RawDiamond: 500 + - type: Extractable + grindableSolutionName: diamondore + - type: SolutionContainerManager + solutions: + diamondore: + reagents: + - ReagentId: Carbon + Quantity: 20 + +- type: entity + parent: DiamondOre + id: DiamondOre1 + suffix: Single + components: + - type: Stack + count: 1 + - type: entity parent: OreBase id: SteelOre diff --git a/Resources/Prototypes/Entities/Objects/Materials/shards.yml b/Resources/Prototypes/Entities/Objects/Materials/shards.yml index d0ff7b924ce..6456396e00a 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/shards.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/shards.yml @@ -5,6 +5,8 @@ description: It's a shard of some unknown material. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite layers: - sprite: Objects/Materials/Shards/shard.rsi @@ -99,7 +101,7 @@ components: - type: Sprite color: "#bbeeff" - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - type: DamageUserOnTrigger @@ -134,7 +136,7 @@ damage: types: Slash: 4.5 - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: PartRodMetal1 @@ -170,14 +172,14 @@ damage: types: Slash: 5.5 + - type: ToolRefinable + refineResult: + - id: SheetGlass1 + - id: SheetPlasma1 - type: EmbedPassiveDamage damage: types: Slash: 0.15 - - type: WelderRefinable - refineResult: - - id: SheetGlass1 - - id: SheetPlasma1 - type: DamageUserOnTrigger damage: types: @@ -213,7 +215,7 @@ types: Slash: 4.5 Radiation: 2 - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: SheetUranium1 @@ -248,7 +250,7 @@ components: - type: Sprite color: "#e0aa36" - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: SheetBrass1 diff --git a/Resources/Prototypes/Entities/Objects/Misc/books.yml b/Resources/Prototypes/Entities/Objects/Misc/books.yml index fca4b5c59dd..a839d287ff8 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/books.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/books.yml @@ -116,10 +116,10 @@ - Bartender - type: entity - id: BookChefGaming + id: BookHowToCookForFortySpaceman parent: BaseItem - name: chef gaming - description: A book about cooking written by a gamer chef. + name: How To Cook For Forty Spacemen + description: A book about cooking written by a space chef. components: - type: Sprite sprite: Objects/Misc/books.rsi diff --git a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml index dc9ee0d09df..f774ec1640a 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml @@ -5,6 +5,8 @@ description: In Space Glasgow this is called a conversation starter. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: MeleeWeapon attackRate: .71 range: 1.4 @@ -33,6 +35,6 @@ materialComposition: Glass: 50 - type: SpaceGarbage - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml index c3b0eebe982..e8f629531e0 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml @@ -55,7 +55,7 @@ - type: Tool qualities: - Rolling - speed: 0.5 # its very big, akward to use + speedModifier: 0.5 # its very big, akward to use - type: Appearance - type: GenericVisualizer visuals: diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml index 621fb4557d0..89cd8abb924 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml @@ -24,6 +24,8 @@ - WhitelistChameleon - type: StealTarget stealGroup: IDCard + - type: NanoChatCard # DeltaV + - type: MiningPoints # DeltaV #IDs with layers @@ -641,24 +643,13 @@ - type: Sprite layers: - state: centcom - - state: departmenthead - color: "#1B67A5" - - state: subdepartment - color: "#1B67A5" - - state: cc - type: Item heldPrefix: blue - - type: IdCard - jobTitle: Central Commander - jobIcon: JobIconNanotrasen - - type: Access - groups: - - AllAccess - tags: - - CentralCommand + - type: PresetIdCard + job: CentralCommandOfficial - type: entity - parent: CentcomIDCard + parent: IDCardStandard id: ERTLeaderIDCard name: ERT leader ID card components: @@ -670,13 +661,13 @@ - state: subdepartment color: "#1B67A5" - state: captain - - type: IdCard - jobTitle: ERT Company Commander + - type: PresetIdCard + job: ERTLeader - type: Item heldPrefix: gold - type: entity - parent: ERTLeaderIDCard + parent: IDCardStandard id: ERTChaplainIDCard name: ERT chaplain ID card components: @@ -686,15 +677,15 @@ - state: departmenthead color: "#1B67A5" - state: subdepartment - color: "#C96DBF" + color: "#1B67A5" - state: chaplain - - type: IdCard - jobTitle: ERT Soul Officer + - type: PresetIdCard + job: ERTChaplain - type: Item heldPrefix: blue - type: entity - parent: ERTChaplainIDCard + parent: IDCardStandard id: ERTEngineerIDCard name: ERT engineer ID card components: @@ -706,11 +697,11 @@ - state: subdepartment color: "#F39F27" - state: stationengineer - - type: IdCard - jobTitle: ERT Field Engineer + - type: PresetIdCard + job: ERTEngineer - type: entity - parent: ERTChaplainIDCard + parent: IDCardStandard id: ERTJanitorIDCard name: ERT janitor ID card components: @@ -722,11 +713,11 @@ - state: subdepartment color: "#58C800" - state: janitor - - type: IdCard - jobTitle: ERT Custodian + - type: PresetIdCard + job: ERTJanitor - type: entity - parent: ERTChaplainIDCard + parent: IDCardStandard id: ERTMedicIDCard name: ERT medic ID card components: @@ -738,11 +729,11 @@ - state: subdepartment color: "#5B97BC" - state: medicaldoctor - - type: IdCard - jobTitle: ERT Medical Doctor + - type: PresetIdCard + job: ERTMedical - type: entity - parent: ERTChaplainIDCard + parent: IDCardStandard id: ERTSecurityIDCard name: ERT security ID card components: @@ -752,10 +743,10 @@ - state: departmenthead color: "#1B67A5" - state: subdepartment - color: "#CB0000" - - state: securityofficer - - type: IdCard - jobTitle: ERT Field Officer + color: "#1B67A5" + - state: cc + - type: PresetIdCard + job: ERTSecurity - type: entity parent: IDCardStandard @@ -773,8 +764,8 @@ - state: cc - type: Item heldPrefix: blue - - type: IdCard - jobTitle: Central Commander + - type: PresetIdCard + job: CentralCommandOfficial - type: Access tags: - Maintenance @@ -810,8 +801,8 @@ - state: death - type: Item heldPrefix: blue - - type: IdCard - jobTitle: Centcom Agent + - type: PresetIdCard + job: DeathSquad - type: entity name: passenger ID card @@ -834,63 +825,6 @@ color: "#878787" - state: passenger - type: AgentIDCard - icons: - # TODO figure out a better way of doing this. - # Probably by adding a bool or icon-category data-field to the icon prototype? - - JobIconDetective - - JobIconQuarterMaster - - JobIconBotanist - - JobIconBoxer - - JobIconAtmosphericTechnician - - JobIconNanotrasen - - JobIconPrisoner - - JobIconJanitor - - JobIconChemist - - JobIconStationEngineer - - JobIconSecurityOfficer - - JobIconChiefMedicalOfficer - - JobIconRoboticist - - JobIconChaplain - - JobIconLawyer - - JobIconUnknown - - JobIconLibrarian - - JobIconCargoTechnician - - JobIconScientist - - JobIconResearchAssistant - - JobIconGeneticist - - JobIconClown - - JobIconCaptain - - JobIconHeadOfPersonnel - - JobIconVirologist - - JobIconShaftMiner - - JobIconPassenger - - JobIconChiefEngineer - - JobIconBartender - - JobIconHeadOfSecurity - - JobIconBrigmedic - - JobIconMedicalDoctor - - JobIconParamedic - - JobIconChef - - JobIconWarden - - JobIconResearchDirector - - JobIconMime - - JobIconMusician - - JobIconReporter - - JobIconPsychologist - - JobIconMedicalIntern - - JobIconTechnicalAssistant - - JobIconServiceWorker - - JobIconSecurityCadet - - JobIconZookeeper - - JobIconPrisonGuard # Nyanotrasen - PrisonGuard, see Resources/Prototypes/Nyanotrasen/Roles/Jobs/Security/prisonguard.yml - - JobIconMailCarrier # Nyanotrasen - MailCarrier, see Resources/Prototypes/Nyanotrasen/Roles/Jobs/Cargo/mail-carrier.yml - - JobIconMartialArtist # Nyanotrasen - MartialArtist, see Resources/Prototypes/Nyanotrasen/Roles/Jobs/Wildcards/martialartist.yml - - JobIconGladiator # Nyanotrasen - Gladiator, see Resources/Prototypes/Nyanotrasen/Roles/Jobs/Wildcards/gladiator.yml - - JobIconForensicMantis # Nyanotrasen - ForensicMantis, see Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml - - JobIconClerk # Delta V - Added justice dept - - JobIconChiefJustice # Delta V - Added justice dept - - JobIconProsecutor # Delta V - Added justice dept - - JobIconVisitor - type: ActivatableUI key: enum.AgentIDCardUiKey.Key inHandsOnly: true @@ -1075,8 +1009,8 @@ - state: cc - type: Item heldPrefix: blue - - type: IdCard - jobTitle: Centcom Quarantine Officer + - type: PresetIdCard + job: CBURN - type: entity parent: IDCardStandard @@ -1094,7 +1028,7 @@ - state: clown color: "#7C0A0A" - type: IdCard - jobTitle: Cluwne + jobTitle: job-title-cluwne - type: Unremoveable - type: entity @@ -1161,3 +1095,17 @@ - type: PresetIdCard job: SeniorOfficer +- type: entity + parent: IDCardStandard + id: AdminAssistantIDCard + name: administrative assistant ID card + components: + - type: Sprite + layers: + - state: silver + - sprite: DeltaV/Objects/Misc/id_cards.rsi + state: idadminassistant + - type: Item + heldPrefix: silver + - type: PresetIdCard + job: AdministrativeAssistant \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml index 7ee2582545b..fd1e76d6385 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml @@ -355,6 +355,7 @@ id: PenEmbeddable abstract: true components: + - type: LandAtCursor - type: DamageOtherOnHit damage: types: @@ -735,6 +736,7 @@ tags: - Write - type: CargoOrderConsole + - type: BankClient - type: ActivatableUI verbText: qm-clipboard-computer-verb-text key: enum.CargoConsoleUiKey.Orders diff --git a/Resources/Prototypes/Entities/Objects/Misc/secret_documents.yml b/Resources/Prototypes/Entities/Objects/Misc/secret_documents.yml deleted file mode 100644 index ae6238d87cc..00000000000 --- a/Resources/Prototypes/Entities/Objects/Misc/secret_documents.yml +++ /dev/null @@ -1,16 +0,0 @@ -- type: entity - parent: BaseItem - id: BookSecretDocuments - name: "emergency security orders" - description: TOP SECRET. These documents specify the Emergency Orders that the HoS must carry out when ordered by Central Command. - components: - - type: Sprite - sprite: Objects/Misc/bureaucracy.rsi - layers: - - state: folder-sec-doc - - type: Tag - tags: - - Book - - HighRiskItem - - type: StealTarget - stealGroup: BookSecretDocuments diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index 36b1970f3e5..82d900db57a 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -143,7 +143,7 @@ - Cuffable # useless if you cant be cuffed - type: entity - parent: BaseSubdermalImplant + parent: [ BaseSubdermalImplant, StorePresetUplink ] id: UplinkImplant name: uplink implant description: This implant lets the user access a hidden Syndicate uplink at will. @@ -155,7 +155,6 @@ components: - Hands # prevent mouse buying grenade penguin since its not telepathic - type: Store - preset: StorePresetUplink balance: Telecrystal: 0 - type: UserInterface diff --git a/Resources/Prototypes/Entities/Objects/Power/lights.yml b/Resources/Prototypes/Entities/Objects/Power/lights.yml index 289736f6717..81225cab815 100644 --- a/Resources/Prototypes/Entities/Objects/Power/lights.yml +++ b/Resources/Prototypes/Entities/Objects/Power/lights.yml @@ -66,7 +66,7 @@ materialComposition: Glass: 25 - type: SpaceGarbage - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 @@ -274,7 +274,7 @@ - type: Construction graph: CyanLight node: icon - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: ShardCrystalCyan @@ -294,7 +294,7 @@ - type: Construction graph: BlueLight node: icon - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: ShardCrystalBlue @@ -314,7 +314,7 @@ - type: Construction graph: PinkLight node: icon - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: ShardCrystalPink @@ -334,7 +334,7 @@ - type: Construction graph: OrangeLight node: icon - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: ShardCrystalOrange @@ -354,7 +354,7 @@ - type: Construction graph: RedLight node: icon - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: ShardCrystalRed @@ -374,7 +374,7 @@ - type: Construction graph: GreenLight node: icon - - type: WelderRefinable + - type: ToolRefinable refineResult: - id: SheetGlass1 - id: ShardCrystalGreen diff --git a/Resources/Prototypes/Entities/Objects/Shields/shields.yml b/Resources/Prototypes/Entities/Objects/Shields/shields.yml index f0d9a7c0b1b..ac23bd3995d 100644 --- a/Resources/Prototypes/Entities/Objects/Shields/shields.yml +++ b/Resources/Prototypes/Entities/Objects/Shields/shields.yml @@ -325,8 +325,10 @@ path: /Audio/Weapons/ebladehum.ogg - type: ItemToggleSize activatedSize: Huge - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.6 + - type: ComponentToggler + components: + - type: DisarmMalus + malus: 0.6 - type: Sprite sprite: Objects/Weapons/Melee/e_shield.rsi layers: @@ -358,7 +360,6 @@ energy: 2 color: blue - type: Reflect - enabled: false reflectProb: 0.95 innate: true reflects: @@ -436,8 +437,10 @@ path: /Audio/Weapons/telescopicoff.ogg params: volume: -5 - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.6 + - type: ComponentToggler + components: + - type: DisarmMalus + malus: 0.6 - type: ItemToggleSize activatedSize: Huge - type: Sprite diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/leaves.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/leaves.yml index c26ba925e0d..8c29b5d6753 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/leaves.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/leaves.yml @@ -217,4 +217,4 @@ tags: - Smokable - type: Item - size: Tiny + size: Tiny \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml index 92aa22a8bae..090f8f67af6 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml @@ -22,6 +22,17 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/wheat.rsi +- type: entity + parent: SeedBase + name: packet of meatwheat seeds + description: "If you ever wanted to drive a vegetarian to insanity, here's how." + id: MeatwheatSeeds + components: + - type: Seed + seedId: meatwheat + - type: Sprite + sprite: Objects/Specific/Hydroponics/meatwheat.rsi + - type: entity parent: SeedBase name: packet of oat seeds @@ -133,6 +144,17 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/orange.rsi +- type: entity + parent: SeedBase + name: packet of extradimensional orange seeds + description: "Polygonal seeds." + id: ExtradimensionalOrangeSeeds + components: + - type: Seed + seedId: extradimensionalOrange + - type: Sprite + sprite: Objects/Specific/Hydroponics/extradimensional_orange.rsi + - type: entity parent: SeedBase name: packet of pineapple seeds @@ -163,6 +185,16 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/sugarcane.rsi +- type: entity + parent: SeedBase + name: packet of papercane seeds + id: PapercaneSeeds + components: + - type: Seed + seedId: papercane + - type: Sprite + sprite: Objects/Specific/Hydroponics/papercane.rsi + - type: entity parent: SeedBase name: packet of tower cap spores @@ -233,6 +265,16 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/apple.rsi +- type: entity + parent: SeedBase + name: packet of golden apple seeds + id: GoldenAppleSeeds + components: + - type: Seed + seedId: goldenApple + - type: Sprite + sprite: Objects/Specific/Hydroponics/golden_apple.rsi + - type: entity parent: SeedBase name: packet of corn seeds @@ -417,6 +459,17 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/galaxythistle.rsi +- type: entity + parent: SeedBase + name: packet of glasstle seeds + description: "Scars of gloomy nights." + id: GlasstleSeeds + components: + - type: Seed + seedId: glasstle + - type: Sprite + sprite: Objects/Specific/Hydroponics/glasstle.rsi + - type: entity parent: SeedBase name: packet of fly amanita spores @@ -511,6 +564,16 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/watermelon.rsi +- type: entity + parent: SeedBase + name: packet of holymelon seeds + id: HolymelonSeeds + components: + - type: Seed + seedId: holymelon + - type: Sprite + sprite: Objects/Specific/Hydroponics/holymelon.rsi + - type: entity parent: SeedBase name: packet of grape seeds @@ -563,6 +626,17 @@ - type: Sprite sprite: Objects/Specific/Hydroponics/pea.rsi +- type: entity + parent: SeedBase + id: WorldPeaSeeds + name: packet of world pea seeds + description: "These rather large seeds give off a soothing blue glow." + components: + - type: Seed + seedId: worldPea + - type: Sprite + sprite: Objects/Specific/Hydroponics/world_pea.rsi + - type: entity parent: SeedBase name: packet of pumpkin seeds @@ -592,3 +666,32 @@ seedId: pyrotton - type: Sprite sprite: Objects/Specific/Hydroponics/pyrotton.rsi + +- type: entity + parent: SeedBase + id: RealCapfruitSeeds + name: packet of capfruit seeds + description: "Is it real, or is it fake?" + components: + - type: Seed + seedId: realCapfruit + - type: Sprite + sprite: Objects/Specific/Hydroponics/capfruit.rsi + +- type: entity + parent: RealCapfruitSeeds + id: FakeCapfruitSeeds + suffix: Fake + components: + - type: Seed + seedId: fakeCapfruit + +- type: entity + parent: SeedBase + name: packet of cherry seeds + id: CherrySeeds + components: + - type: Seed + seedId: cherry + - type: Sprite + sprite: Objects/Specific/Hydroponics/cherry.rsi diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml index 2aee628394f..cb556c7d4d9 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml @@ -244,6 +244,7 @@ embedOnThrow: True - type: ThrowingAngle angle: 0 + - type: LandAtCursor - type: Ammo muzzleFlash: null - type: Projectile diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml index 1a905685934..0a7041ea548 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml @@ -31,6 +31,11 @@ size: Large - type: Speech speechVerb: Robotic + - type: ItemToggle + soundActivate: + path: /Audio/Items/Defib/defib_safety_on.ogg + soundDeactivate: + path: /Audio/Items/Defib/defib_safety_off.ogg - type: Defibrillator zapHeal: types: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml index a9d6bbb20f5..93208f80c00 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml @@ -1,19 +1,14 @@ - type: entity name: handheld crew monitor suffix: DO NOT MAP - parent: - - BaseItem - - PowerCellSlotSmallItem + parent: BaseHandheldComputer + # CMO-only bud, don't add more. id: HandheldCrewMonitor description: A hand-held crew monitor displaying the status of suit sensors. components: - type: Sprite sprite: Objects/Specific/Medical/handheldcrewmonitor.rsi state: scanner - - type: PowerCellDraw - drawRate: 0 - useRate: 20 - - type: ActivatableUIRequiresPowerCell - type: ActivatableUI key: enum.CrewMonitoringUIKey.Key - type: UserInterface diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml index 6a0f0a3c23d..e852b217cba 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml @@ -444,7 +444,41 @@ amount: 5 - type: entity - name: pill (iron 10u) + name: pill + suffix: Potassium iodide 10u + parent: Pill + id: PillPotassiumIodide + components: + - type: Pill + pillType: 8 + - type: Sprite + state: pill9 + - type: Label + currentLabel: potassium iodide 10u + - type: SolutionContainerManager + solutions: + food: + maxVol: 20 + reagents: + - ReagentId: PotassiumIodide + Quantity: 10 + +- type: entity + name: pill canister + parent: PillCanister + id: PillCanisterPotassiumIodide + suffix: Potassium iodide 10u, 5 + components: + - type: Label + currentLabel: potassium iodide 10u + - type: StorageFill + contents: + - id: PillPotassiumIodide + amount: 5 + +- type: entity + name: pill + suffix: Iron 10u parent: Pill id: PillIron components: @@ -782,6 +816,10 @@ prob: 0.10 maxAmount: 7 orGroup: RandomPill + - id: PillPotassiumIodide + prob: 0.10 + maxAmount: 7 + orGroup: RandomPill - id: PillIron prob: 0.10 maxAmount: 7 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml index 47e633276b8..df9c27149d3 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml @@ -21,6 +21,8 @@ interfaces: enum.HealthAnalyzerUiKey.Key: type: HealthAnalyzerBoundUserInterface + - type: ItemToggle + onUse: false - type: HealthAnalyzer scanningEndSound: path: "/Audio/Items/Medical/healthscanner.ogg" diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml index 66e7d6e428e..4763687a150 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml @@ -332,7 +332,7 @@ - type: Tool qualities: - Sawing - speed: 1.0 + speedModifier: 1.0 - type: MeleeWeapon attackRate: 1.3333 range: 1.35 @@ -388,7 +388,7 @@ - type: Tool qualities: - Sawing - speed: 0.5 + speedModifier: 0.5 - type: BoneSaw # Shitmed speed: 0.5 @@ -429,7 +429,7 @@ - type: Tool qualities: - Sawing - speed: 1.5 + speedModifier: 1.5 - type: SurgeryTool startSound: path: /Audio/Medical/Surgery/saw.ogg @@ -474,7 +474,7 @@ - type: Tool qualities: - Sawing - speed: 2.0 + speedModifier: 2.0 - type: BoneSaw # Shitmed speed: 2 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml index 3c3b2154226..a90f7099adc 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml @@ -40,23 +40,22 @@ - state: screen shader: unshaded visible: false - map: ["enum.PowerDeviceVisualLayers.Powered"] + map: ["enum.ToggleVisuals.Layer"] - type: Appearance - type: GenericVisualizer visuals: - enum.ProximityBeeperVisuals.Enabled: - enum.PowerDeviceVisualLayers.Powered: + enum.ToggleVisuals.Toggled: + enum.ToggleVisuals.Layer: True: { visible: true } False: { visible: false } + - type: ItemToggle - type: ProximityBeeper - type: ProximityDetector - enabled: false range: 20 criteria: components: - Anomaly - type: Beeper - enabled: false minBeepInterval: 0.15 maxBeepInterval: 1.00 beepSound: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml index a36bfaf676b..cf5f61dddde 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml @@ -14,7 +14,7 @@ slots: - belt - type: Item - size: Ginormous + size: Huge # DeltaV: Was Ginormous, lets it fit in conscription bag - type: Storage maxItemSize: Normal grid: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml b/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml index 478aec4ce49..13c23d55463 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml @@ -365,6 +365,7 @@ - state: refill_sec - type: entity + abstract: true # DeltaV: Salvage vendor doesn't have stock anymore parent: BaseVendingMachineRestock id: VendingMachineRestockSalvageEquipment name: Salvage Vendor restock box diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml b/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml index d6855d630c6..a09bd42e435 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml @@ -79,7 +79,6 @@ - !type:DoActsBehavior acts: [ "Destruction" ] - type: Label - originalName: jug - type: Tag tags: - ChemDispensable diff --git a/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml b/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml index 4f760066843..d3dc41a74a7 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml @@ -48,7 +48,7 @@ # Uplinks - type: entity - parent: BaseItem + parent: [ BaseItem, StorePresetUplink ] id: BaseUplinkRadio name: syndicate uplink description: Suspiciously looking old radio... @@ -68,7 +68,6 @@ - type: ActivatableUI key: enum.StoreUiKey.Key - type: Store - preset: StorePresetUplink balance: Telecrystal: 0 @@ -78,7 +77,6 @@ suffix: 20 TC components: - type: Store - preset: StorePresetUplink balance: Telecrystal: 20 @@ -88,7 +86,6 @@ suffix: 25 TC components: - type: Store - preset: StorePresetUplink balance: Telecrystal: 25 @@ -99,19 +96,29 @@ suffix: 40 TC, NukeOps components: - type: Store - preset: StorePresetUplink balance: Telecrystal: 40 - type: Tag tags: - NukeOpsUplink +- type: entity + parent: BaseUplinkRadio + id: BaseUplinkRadio60TC + suffix: 60 TC, LoneOps + components: + - type: Store + balance: + Telecrystal: 60 + - type: Tag + tags: + - NukeOpsUplink + - type: entity parent: BaseUplinkRadio id: BaseUplinkRadioDebug suffix: DEBUG components: - type: Store - preset: StorePresetUplink balance: Telecrystal: 99999 diff --git a/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml b/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml index 399e14cc975..fa6a506bdf9 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml @@ -21,7 +21,7 @@ - Cutting useSound: path: /Audio/Items/wirecutter.ogg - speed: 0.05 + speedModifier: 0.05 - type: Item sprite: Objects/Tools/Cowtools/haycutters.rsi @@ -46,7 +46,7 @@ - Screwing useSound: collection: Screwdriver - speed: 0.05 + speedModifier: 0.05 - type: entity name: wronch @@ -69,7 +69,7 @@ - Anchoring useSound: path: /Audio/Items/ratchet.ogg - speed: 0.05 + speedModifier: 0.05 - type: entity name: cowbar @@ -93,7 +93,7 @@ - Prying useSound: path: /Audio/Items/crowbar.ogg - speed: 0.05 + speedModifier: 0.05 - type: ToolTileCompatible - type: Prying @@ -127,7 +127,7 @@ size: Small sprite: Objects/Tools/Cowtools/cowelder.rsi - type: Tool - speed: 0.05 + speedModifier: 0.05 - type: entity name: milkalyzer diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml index c8e47b6fdae..4db76a97968 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml @@ -62,7 +62,7 @@ - type: Tool qualities: - Rolling - speed: 0.6 # fairly unwieldly but nice round surface + speedModifier: 0.6 # fairly unwieldly but nice round surface - type: entity parent: GasTankRoundBase diff --git a/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml b/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml index 38b1f7cda9f..6491f699f00 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml @@ -1,6 +1,6 @@ - type: entity name: handheld mass scanner - parent: [ BaseItem, PowerCellSlotSmallItem] + parent: BaseHandheldComputer id: HandHeldMassScanner description: A hand-held mass scanner. components: @@ -27,7 +27,6 @@ - type: PowerCellDraw drawRate: 3 useRate: 100 - - type: ActivatableUIRequiresPowerCell - type: ActivatableUI key: enum.RadarConsoleUiKey.Key inHandsOnly: true diff --git a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml index 3beb11f4238..70e0fbbce72 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml @@ -21,7 +21,7 @@ - type: Tool qualities: - Prying - speed: 1.5 + speedModifier: 1.5 useSound: /Audio/Items/jaws_pry.ogg - type: Prying pryPowered: true @@ -86,7 +86,7 @@ - type: Tool qualities: - Prying - speed: 3.0 + speedModifier: 3.0 - type: MultipleTool entries: - behavior: Prying diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index 2836f5539d1..cc38a5bcde5 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -428,7 +428,7 @@ - type: Tool qualities: - Screwing - speed: 1.5 + speedModifier: 1.5 useSound: /Audio/Items/drill_use.ogg - type: MultipleTool statusShowBehavior: true @@ -631,7 +631,7 @@ - type: Tool qualities: - Screwing - speed: 1.2 # Kept for future adjustments. Currently 1.2x for balance + speedModifier: 1.2 # Kept for future adjustments. Currently 1.2x for balance useSound: /Audio/Items/drill_use.ogg - type: ToolTileCompatible - type: MultipleTool diff --git a/Resources/Prototypes/Entities/Objects/Tools/welders.yml b/Resources/Prototypes/Entities/Objects/Tools/welders.yml index 4b08655a30b..8c0e1e1716c 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/welders.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/welders.yml @@ -64,8 +64,10 @@ - type: ItemToggleSize activatedSize: Large - type: ItemToggleHot - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.6 + - type: ComponentToggler + components: + - type: DisarmMalus + malus: 0.6 - type: ToggleableLightVisuals spriteLayer: flame inhandVisuals: @@ -165,7 +167,7 @@ Quantity: 250 maxVol: 250 - type: Tool - speed: 1.3 + speedModifier: 1.3 - type: entity name: experimental welding tool @@ -216,7 +218,7 @@ Quantity: 50 maxVol: 50 - type: Tool - speed: 0.7 + speedModifier: 0.7 - type: PointLight enabled: false radius: 1.0 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/ied.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/firebomb.yml similarity index 83% rename from Resources/Prototypes/Entities/Objects/Weapons/Bombs/ied.yml rename to Resources/Prototypes/Entities/Objects/Weapons/Bombs/firebomb.yml index c4420a43a36..a0e6fe77287 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/ied.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/firebomb.yml @@ -3,8 +3,8 @@ # with that you could make napalm ied instead of welding fuel with no additional complexity - type: entity parent: BaseItem - id: ImprovisedExplosive - name: improvised explosive device + id: FireBomb + name: fire bomb description: A weak, improvised incendiary device. components: - type: Sprite @@ -26,10 +26,10 @@ volume: 1 - type: RandomTimerTrigger min: 0 - max: 60 - - type: Explosive # Weak explosion in a very small radius. Doesn't break underplating. - explosionType: Default - totalIntensity: 20 + max: 15 + - type: Explosive # Weak explosion in a very small radius. Ignites surrounding entities. + explosionType: FireBomb + totalIntensity: 25 intensitySlope: 5 maxIntensity: 3 canCreateVacuum: false @@ -50,14 +50,14 @@ acts: ["Destruction"] - !type:ExplodeBehavior - type: Construction - graph: ImprovisedExplosive - node: ied + graph: FireBomb + node: firebomb # has igniter but no fuel or wires - type: entity parent: DrinkColaCanEmpty - id: ImprovisedExplosiveEmpty - name: improvised explosive device + id: FireBombEmpty + name: fire bomb suffix: empty description: A weak, improvised incendiary device. This one has no fuel. components: @@ -74,17 +74,17 @@ True: {state: "base"} False: {state: "base"} - type: Construction - graph: ImprovisedExplosive + graph: FireBomb node: empty - defaultTarget: ied + defaultTarget: firebomb - type: Tag tags: - Trash # no DrinkCan, prevent using it to make another ied - type: entity - parent: ImprovisedExplosiveEmpty - id: ImprovisedExplosiveFuel + parent: FireBombEmpty + id: FireBombFuel suffix: fuel description: A weak, improvised incendiary device. This one is missing wires. components: @@ -102,6 +102,6 @@ Quantity: 30 - type: Construction node: fuel - defaultTarget: ied + defaultTarget: firebomb - type: Tag tags: [] diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/pipebomb.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/pipebomb.yml new file mode 100644 index 00000000000..5fb6829ac2b --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/pipebomb.yml @@ -0,0 +1,62 @@ +- type: entity + parent: GrenadeBase + id: PipeBomb + name: pipe bomb + description: An improvised explosive made from pipes and wire. + components: + - type: Sprite + sprite: Objects/Weapons/Bombs/pipebomb.rsi + layers: + - state: base + map: ["enum.TriggerVisualLayers.Base"] + - state: wires + - type: OnUseTimerTrigger # todo: make it activate through welder/lighter/fire instead + delay: 5 + examinable: false + initialBeepDelay: 0 + beepSound: /Audio/Effects/lightburn.ogg + - type: RandomTimerTrigger + min: 1 + max: 10 + - type: ExplodeOnTrigger + - type: Explosive # Weak explosion in a very small radius. Doesn't break underplating. + explosionType: Default + totalIntensity: 50 + intensitySlope: 5 + maxIntensity: 6 + canCreateVacuum: false + - type: Appearance + - type: TimerTriggerVisuals + - type: Construction + graph: PipeBomb + node: pipebomb + +- type: entity + parent: BaseItem + id: PipeBombGunpowder + name: pipe bomb + description: An improvised explosive made from a pipe. This one has no gunpowder. + suffix: Gunpowder + components: + - type: Sprite + sprite: Objects/Weapons/Bombs/pipebomb.rsi + state: base + - type: Construction + graph: PipeBomb + node: gunpowder + defaultTarget: pipebomb + +- type: entity + parent: BaseItem + id: PipeBombCable + name: pipe bomb + description: An improvised explosive made from a pipe. This one has no cable. + suffix: Cable + components: + - type: Sprite + sprite: Objects/Weapons/Bombs/pipebomb.rsi + state: base + - type: Construction + graph: PipeBomb + node: cable + defaultTarget: pipebomb \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/rifle.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/rifle.yml index 03d6e337994..fef44fe33bb 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/rifle.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/rifle.yml @@ -51,7 +51,7 @@ components: - type: BallisticAmmoProvider capacity: 200 - proto: CartridgeRiflePractice + proto: CartridgeRifleRubber - type: Sprite layers: - state: base-b diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml index e26c4e95404..116fa36e0da 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml @@ -9,8 +9,6 @@ - Cartridge - ShellShotgun - type: CartridgeAmmo - count: 6 - spread: 15 soundEject: collection: ShellEject - type: Sprite @@ -26,13 +24,17 @@ name: shell (.50 beanbag) parent: BaseShellShotgun components: + - type: Tag + tags: + - Cartridge + - ShellShotgun + - ShellShotgunLight - type: Sprite layers: - state: beanbag map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo proto: PelletShotgunBeanbag - count: 1 - type: SpentAmmoVisuals state: "beanbag" @@ -47,8 +49,6 @@ map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo proto: PelletShotgunSlug - count: 1 - spread: 0 - type: SpentAmmoVisuals state: "slug" @@ -57,13 +57,17 @@ name: shell (.50 flare) parent: BaseShellShotgun components: + - type: Tag + tags: + - Cartridge + - ShellShotgun + - ShellShotgunLight - type: Sprite layers: - state: flare map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo proto: PelletShotgunFlare - count: 1 - type: SpentAmmoVisuals state: "flare" @@ -77,7 +81,7 @@ - state: base map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo - proto: PelletShotgun + proto: PelletShotgunSpread - type: entity id: ShellShotgunIncendiary @@ -89,7 +93,7 @@ - state: incendiary map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo - proto: PelletShotgunIncendiary + proto: PelletShotgunIncendiarySpread - type: SpentAmmoVisuals state: "incendiary" @@ -103,7 +107,7 @@ - state: practice map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo - proto: PelletShotgunPractice + proto: PelletShotgunPracticeSpread - type: SpentAmmoVisuals state: "practice" @@ -112,13 +116,17 @@ name: shell (.50 tranquilizer) parent: BaseShellShotgun components: + - type: Tag + tags: + - Cartridge + - ShellShotgun + - ShellShotgunLight - type: Sprite layers: - state: practice map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo proto: PelletShotgunTranquilizer - count: 1 - type: ChemicalAmmo - type: SolutionContainerManager solutions: @@ -137,6 +145,11 @@ description: A homemade shotgun shell that shoots painful glass shrapnel. The spread is so wide that it couldn't hit the broad side of a barn. parent: BaseShellShotgun components: + - type: Tag + tags: + - Cartridge + - ShellShotgun + - ShellShotgunLight - type: Sprite layers: - state: improvised @@ -145,9 +158,7 @@ graph: ImprovisedShotgunShellGraph node: shell - type: CartridgeAmmo - count: 10 - spread: 45 - proto: PelletShotgunImprovised + proto: PelletShotgunImprovisedSpread - type: SpentAmmoVisuals state: "improvised" @@ -161,8 +172,6 @@ - state: depleted-uranium map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo - count: 5 - spread: 6 - proto: PelletShotgunUranium + proto: PelletShotgunUraniumSpread - type: SpentAmmoVisuals state: "depleted-uranium" diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml index bde2731937a..c57a8adaa5f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml @@ -332,7 +332,7 @@ - type: entity id: MagazinePistolSubMachineGunUranium - name: SMG magazine (.35 auto rubber) + name: SMG magazine (.35 auto uranium) parent: BaseMagazinePistolSubMachineGun components: - type: BallisticAmmoProvider diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml index 6e4570e1a16..d8c465cf9e8 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml @@ -42,6 +42,16 @@ types: Piercing: 10 +- type: entity + id: PelletShotgunSpread + categories: [ HideSpawnMenu ] + parent: PelletShotgun + components: + - type: ProjectileSpread + proto: PelletShotgun + count: 6 + spread: 15 + - type: entity id: PelletShotgunIncendiary name: pellet (.50 incendiary) @@ -59,6 +69,16 @@ - type: IgnitionSource ignited: true +- type: entity + id: PelletShotgunIncendiarySpread + categories: [ HideSpawnMenu ] + parent: PelletShotgunIncendiary + components: + - type: ProjectileSpread + proto: PelletShotgunIncendiary + count: 6 + spread: 15 + - type: entity id: PelletShotgunPractice name: pellet (.50 practice) @@ -73,6 +93,16 @@ types: Blunt: 1 +- type: entity + id: PelletShotgunPracticeSpread + categories: [ HideSpawnMenu ] + parent: PelletShotgunPractice + components: + - type: ProjectileSpread + proto: PelletShotgunPractice + count: 6 + spread: 15 + - type: entity id: PelletShotgunImprovised name: improvised pellet @@ -88,6 +118,15 @@ Piercing: 3 Slash: 3 +- type: entity + id: PelletShotgunImprovisedSpread + categories: [ HideSpawnMenu ] + parent: PelletShotgunImprovised + components: + - type: ProjectileSpread + proto: PelletShotgunImprovised + count: 10 + spread: 45 - type: entity id: PelletShotgunTranquilizer @@ -178,6 +217,16 @@ Radiation: 5 Piercing: 5 +- type: entity + id: PelletShotgunUraniumSpread + categories: [ HideSpawnMenu ] + parent: PelletShotgunUranium + components: + - type: ProjectileSpread + proto: PelletShotgunUranium + count: 5 + spread: 6 + - type: entity id: PelletGrapeshot #tally fucking ho name: grapeshot pellet @@ -196,6 +245,16 @@ Piercing: 25 Structural: 5 +- type: entity + id: PelletGrapeshotSpread + categories: [ HideSpawnMenu ] + parent: PelletGrapeshot + components: + - type: ProjectileSpread + proto: PelletGrapeshot + count: 5 + spread: 40 + - type: entity id: PelletGlass name: glass shard @@ -215,3 +274,13 @@ damage: types: Slash: 25 + +- type: entity + id: PelletGlassSpread + parent: PelletGlass + categories: [ HideSpawnMenu ] + components: + - type: ProjectileSpread + proto: PelletGlass + count: 5 + spread: 10 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/toy.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/toy.yml index 34a39c1583e..14595bd34a2 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/toy.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/toy.yml @@ -4,12 +4,31 @@ name: foam dart parent: BaseItem components: - - type: Tag - tags: - - BulletFoam - - Trash - - type: Ammo - - type: Sprite - sprite: Objects/Fun/toys.rsi - layers: - - state: foamdart + - type: Fixtures + fixtures: + fix1: + shape: !type:PolygonShape + vertices: + - -0.05,-0.15 + - -0.05,0.25 + - 0.05,-0.15 + - 0.05,0.25 + density: 20 + mask: + - ItemMask + restitution: 0.3 + friction: 0.2 + - type: Tag + tags: + - BulletFoam + - Trash + - type: Ammo + - type: Sprite + sprite: Objects/Fun/toys.rsi + layers: + - state: foamdart + - type: EmbeddableProjectile + removalTime: .2 + - type: ThrowingAngle + angle: 180 + - type: LandAtCursor diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/explosives.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/explosives.yml index 152de7e92f2..04d405e8815 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/explosives.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/explosives.yml @@ -175,9 +175,7 @@ parent: BaseCannonBall components: - type: CartridgeAmmo - proto: PelletGrapeshot - count: 5 - spread: 40 + proto: PelletGrapeshotSpread deleteOnSpawn: true - type: Sprite sprite: Objects/Weapons/Guns/Ammunition/Explosives/explosives.rsi @@ -190,9 +188,7 @@ parent: BaseCannonBall components: - type: CartridgeAmmo - proto: PelletGlass - count: 5 - spread: 10 + proto: PelletGlassSpread deleteOnSpawn: true - type: Sprite sprite: Objects/Weapons/Guns/Ammunition/Explosives/explosives.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml index a8d326d0611..653deecc4b4 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml @@ -21,7 +21,6 @@ fireOnDropChance: 0.15 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser.ogg - doRecoil: false - type: Battery maxCharge: 1000 startingCharge: 1000 @@ -73,7 +72,6 @@ - SemiAuto soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser.ogg - doRecoil: false - type: MagazineAmmoProvider - type: ItemSlots slots: @@ -860,3 +858,55 @@ - type: Appearance - type: StaticPrice price: 750 + +- type: entity + name: energy shotgun + parent: [BaseWeaponBattery, BaseGunWieldable] + id: WeaponEnergyShotgun + description: A one-of-a-kind prototype energy weapon that uses various shotgun configurations. It offers the possibility of both lethal and non-lethal shots, making it a versatile weapon. + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Battery/energy_shotgun.rsi + layers: + - state: base + map: ["enum.GunVisualLayers.Base"] + - state: mag-unshaded-4 + map: ["enum.GunVisualLayers.MagUnshaded"] + shader: unshaded + - type: Clothing + sprite: Objects/Weapons/Guns/Battery/energy_shotgun.rsi + - type: Gun + fireRate: 2 + soundGunshot: + path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg + - type: ProjectileBatteryAmmoProvider + proto: BulletLaserSpread + fireCost: 150 + - type: BatteryWeaponFireModes + fireModes: + - proto: BulletLaserSpread + fireCost: 150 + - proto: BulletLaserSpreadNarrow + fireCost: 200 + - proto: BulletDisablerSmgSpread + fireCost: 120 + - type: Item + size: Large + shape: + - 0,0,3,1 + sprite: Objects/Weapons/Guns/Battery/inhands_64x.rsi + heldPrefix: energy + - type: Tag + tags: + - HighRiskItem + - type: StealTarget + stealGroup: HoSAntiqueWeapon + - type: GunRequiresWield #remove when inaccuracy on spreads is fixed + - type: Battery + maxCharge: 1200 + startingCharge: 1200 + - type: BatterySelfRecharger + autoRecharge: true + autoRechargeRate: 24 + autoRechargePause: true + autoRechargePauseTime: 10 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml index 72df09ac508..cb557e05974 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml @@ -19,6 +19,7 @@ path: /Audio/Weapons/Guns/Empty/lmg_empty.ogg - type: StaticPrice price: 500 + - type: Execution # No chamber because HMG may want its own - type: MeleeWeapon attackRate: 1.5 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml index 4b6e21a4922..0a412a5480f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml @@ -62,6 +62,7 @@ price: 500 - type: UseDelay delay: 1 + - type: Execution - type: MeleeWeapon attackRate: 1.4 damage: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml index dc49dce0f3a..0fe74553694 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml @@ -33,6 +33,7 @@ collection: MetalThud - type: DamageOtherOnHit staminaCost: 14 + - type: Execution - type: entity name: china lake @@ -180,6 +181,8 @@ - type: TetherGun frequency: 5 dampingRatio: 4 + - type: ItemToggle + onUse: false - type: PowerCellDraw - type: Sprite sprite: Objects/Weapons/Guns/Launchers/tether_gun.rsi @@ -225,6 +228,8 @@ path: /Audio/Weapons/soup.ogg params: volume: 2 + - type: ItemToggle + onUse: false - type: PowerCellDraw - type: Sprite sprite: Objects/Weapons/Guns/Launchers/force_gun.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml index fefc41ae865..32a0e8b4df4 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml @@ -65,6 +65,7 @@ - type: Appearance - type: StaticPrice price: 500 + - type: Execution - type: AmmoCounter - type: MeleeWeapon attackRate: 1.2 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml index acf22532232..c8b6b7af67f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml @@ -122,6 +122,9 @@ path: /Audio/Weapons/Guns/Hits/snap.ogg - type: StaminaDamageOnCollide damage: 22 # 5 hits to stun sounds reasonable + - type: Tag + tags: + - BulletRubber - type: entity id: BaseBulletIncendiary @@ -1041,3 +1044,99 @@ Heat: 20 # Slightly more damage than the 17heat from the Captain's Hitscan lasgun soundHit: collection: MeatLaserImpact + +- type: entity + name: laser bolt + id: BulletLaser + parent: BaseBullet + categories: [ HideSpawnMenu ] + components: + - type: Reflective + reflective: + - Energy + - type: FlyBySound + sound: + collection: EnergyMiss + params: + volume: 5 + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi + layers: + - state: heavylaser + shader: unshaded + - type: Physics + - type: Fixtures + fixtures: + projectile: + shape: + !type:PhysShapeAabb + bounds: "-0.15,-0.3,0.15,0.3" + hard: false + mask: + - Impassable + - BulletImpassable + fly-by: *flybyfixture + - type: Ammo + - type: Projectile + impactEffect: BulletImpactEffectOrangeDisabler + damage: + types: + Heat: 10 + soundHit: + collection: WeakHit + forceSound: true + +- type: entity + name: wide laser barrage + id: BulletLaserSpread + parent: BulletLaser + categories: [ HideSpawnMenu ] + components: + - type: ProjectileSpread + proto: BulletLaser + count: 5 #50 heat damage if you hit all your shots, but wide spread + spread: 30 + +- type: entity + name: narrow laser barrage + id: BulletLaserSpreadNarrow + parent: BulletLaser + categories: [ HideSpawnMenu ] + components: + - type: ProjectileSpread + proto: BulletLaser + count: 4 #52 heat damage if you hit all your shots, but narrower spread + spread: 10 + +- type: entity + name : heavy laser bolt + id: BulletLaserHeavy + parent: BulletLaser + categories: [ HideSpawnMenu ] + components: + - type: Projectile + damage: + types: + Heat: 15 + +- type: entity + name: narrow laser barrage + id: BulletLaserHeavySpread + parent: BulletLaser + categories: [ HideSpawnMenu ] + components: + - type: ProjectileSpread + proto: BulletLaser + count: 3 #45 heat damage if you hit all your shots, but narrower spread + spread: 10 + +- type: entity + name: disabling laser barrage + id: BulletDisablerSmgSpread + parent: BulletDisablerSmg + categories: [ HideSpawnMenu ] + components: + - type: ProjectileSpread + proto: BulletDisablerSmg + count: 3 #bit stronger than a disabler if you hit your shots you goober, still not a 2 hit stun though + spread: 9 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml index 0aa281b95c0..e1753ce81e0 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml @@ -51,6 +51,7 @@ gun_chamber: !type:ContainerSlot - type: StaticPrice price: 500 + - type: Execution - type: MeleeWeapon attackRate: 1.3333 damage: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml index 8d43953a07b..621d2a88ec1 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml @@ -19,6 +19,7 @@ minAngle: 2 maxAngle: 16 fireRate: 8 + burstFireRate: 8 angleIncrease: 3 angleDecay: 16 selectedMode: FullAuto @@ -27,6 +28,7 @@ - FullAuto soundGunshot: path: /Audio/Weapons/Guns/Gunshots/smg.ogg + defaultDirection: 1, 0 - type: ChamberMagazineAmmoProvider soundRack: path: /Audio/Weapons/Guns/Cock/smg_cock.ogg @@ -54,6 +56,7 @@ gun_chamber: !type:ContainerSlot - type: StaticPrice price: 500 + - type: Execution - type: MeleeWeapon attackRate: 1.3333 damage: @@ -146,7 +149,7 @@ - type: entity name: Drozd - parent: [BaseWeaponSubMachineGun, BaseGunWieldable] + parent: BaseWeaponSubMachineGun id: WeaponSubMachineGunDrozd description: An excellent fully automatic Heavy SMG. components: @@ -160,19 +163,20 @@ - type: Clothing sprite: Objects/Weapons/Guns/SMGs/drozd.rsi - type: Wieldable + unwieldOnUse: false - type: GunWieldBonus minAngle: -19 maxAngle: -16 - type: Gun minAngle: 21 maxAngle: 32 - fireRate: 6 + fireRate: 12 + burstFireRate: 12 selectedMode: FullAuto soundGunshot: path: /Audio/Weapons/Guns/Gunshots/atreides.ogg availableModes: - FullAuto - fireOnDropChance: 0.2 - type: ItemSlots slots: gun_magazine: @@ -276,6 +280,8 @@ angleDecay: 6 selectedMode: FullAuto shotsPerBurst: 5 + burstCooldown: 0.2 + burstFireRate: 7 availableModes: - SemiAuto - Burst @@ -314,6 +320,7 @@ - type: StealTarget stealGroup: HoSAntiqueWeapon + # Rubber - type: entity name: Drozd @@ -364,4 +371,4 @@ priority: 1 whitelist: tags: - - CartridgeMagnum + - CartridgeMagnum \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml index 40c85374123..818d3597193 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml @@ -43,6 +43,7 @@ ents: [] - type: StaticPrice price: 500 + - type: Execution - type: MeleeWeapon attackRate: 1.4 damage: @@ -120,6 +121,7 @@ - type: Appearance - type: StaticPrice price: 500 + - type: Execution - type: MeleeWeapon attackRate: 1.4 damage: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml index 86b90f2bdea..7099ce6eea9 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml @@ -39,6 +39,7 @@ ents: [] - type: StaticPrice price: 500 + - type: Execution - type: MeleeWeapon attackRate: 1.3333 damage: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml index 0ad30e9ed6e..676937a5813 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml @@ -1,52 +1,75 @@ - type: entity name: flare gun - parent: BaseWeaponLauncher + parent: BaseItem id: WeaponFlareGun - description: A compact, single-shot pistol that fires shotgun shells. + description: A compact, single-shot pistol that fires shotgun shells. Comes with a safety feature that prevents the user from fitting lethal shells inside. components: - type: Sprite sprite: Objects/Weapons/Guns/Shotguns/flaregun.rsi layers: - - state: icon + - state: base map: ["enum.GunVisualLayers.Base"] - type: Item size: Small sprite: Objects/Weapons/Guns/Shotguns/flaregun.rsi - - type: Gun - fireRate: 8 - selectedMode: SemiAuto - availableModes: - - SemiAuto - soundGunshot: - path: /Audio/Weapons/Guns/Gunshots/flaregun.ogg - - type: BallisticAmmoProvider - whitelist: - tags: - - ShellShotgun - proto: ShellShotgunFlare - capacity: 1 - soundInsert: - path: /Audio/Weapons/Guns/MagIn/shotgun_insert.ogg + - type: ItemSlots + slots: + gun_chamber: + name: Chamber + startingItem: ShellShotgunFlare + priority: 1 + whitelist: + tags: ## TODO: Add a risk of the gun blowing up if using non-light shotgun shells, and then re-enable them. + ## - ShellShotgun + - ShellShotgunLight - type: ContainerContainer containers: - ballistic-ammo: !type:Container - ents: [] + gun_chamber: !type:ContainerSlot + - type: ChamberMagazineAmmoProvider + autoCycle: false + boltClosed: true + canRack: false + soundBoltClosed: /Audio/Weapons/Guns/Cock/revolver_cock.ogg + soundBoltOpened: /Audio/Weapons/Guns/Cock/revolver_cock.ogg + soundRack: /Audio/Weapons/Guns/Cock/revolver_cock.ogg - type: Clothing sprite: Objects/Weapons/Guns/Shotguns/flaregun.rsi quickEquip: false slots: - Belt - suitStorage - - type: MeleeWeapon - attackRate: 1.2 - damage: - types: - Blunt: 6.5 - swapKeys: true - disableHeavy: true - animation: WeaponArcThrust - wideAnimationRotation: 135 - soundHit: - collection: MetalThud - - type: DamageOtherOnHit - staminaCost: 4.5 + - type: Appearance + - type: Gun + fireRate: 8 + selectedMode: SemiAuto + availableModes: + - SemiAuto + soundGunshot: + path: /Audio/Weapons/Guns/Gunshots/flaregun.ogg + + +- type: entity + name: security shell gun + parent: WeaponFlareGun + id: WeaponFlareGunSecurity + description: A modified flare gun originally designed to be used by security to launch non-lethal shotgun shells, however it can also fire lethal shells without risk. + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Shotguns/flaregun_security.rsi + layers: + - state: base + map: ["enum.GunVisualLayers.Base"] + - type: Item + size: Small + sprite: Objects/Weapons/Guns/Shotguns/flaregun_security.rsi + - type: ItemSlots + slots: + gun_chamber: + name: Chamber + priority: 1 + whitelist: + tags: + - ShellShotgun + - type: Tag + tags: + - Sidearm diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml index 2f1527d3592..aef5cfbf8d4 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml @@ -116,6 +116,7 @@ containers: storagebase: !type:Container ents: [] + - type: Execution # shoots bullets instead of throwing them, no other changes - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index dc72d91a64c..20a4d51493b 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -5,6 +5,8 @@ description: A grotesque blade made out of bone and flesh that cleaves through people as a hot knife through butter. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite sprite: Objects/Weapons/Melee/armblade.rsi state: icon diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baguette.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baguette.yml new file mode 100644 index 00000000000..46bd430e5ac --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baguette.yml @@ -0,0 +1,17 @@ +- type: entity + name: baguette + parent: FoodBreadBaguette + id: WeaponBaguette + description: Bon appétit! + suffix: Weapon + components: + - type: MeleeWeapon + wideAnimationRotation: -120 + damage: + types: + Slash: 16 + soundHit: + path: /Audio/Weapons/bladeslice.ogg + - type: Reflect + reflectProb: 0.05 + spread: 90 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml index 60ba7aa2c30..b26ae8ad14d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml @@ -35,7 +35,7 @@ - type: Tool qualities: - Rolling - speed: 0.75 # a bit unwieldly but does the job + speedModifier: 0.75 # a bit unwieldly but does the job - type: Clothing quickEquip: false slots: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml index 0e8b85e5345..42a353675b1 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml @@ -13,10 +13,12 @@ - type: ItemToggleActiveSound activeSound: path: /Audio/Weapons/ebladehum.ogg - - type: ItemToggleSharp + - type: ComponentToggler + components: + - type: Sharp + - type: DisarmMalus + malus: 0.6 - type: ItemToggleHot - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.6 - type: ItemToggleSize activatedSize: Huge - type: ItemToggleMeleeWeapon @@ -86,15 +88,7 @@ right: - state: inhand-right-blade shader: unshaded - - type: DisarmMalus - malus: 0 - type: Reflect - enabled: false - reflectProb: 0.5 - minReflectProb: 0.25 - - type: Tag - tags: - - NoPaint - type: IgnitionSource temperature: 700 # Shitmed Change @@ -130,7 +124,6 @@ suffix: E-Dagger description: 'A dark ink pen.' components: - - type: EnergySword - type: ItemToggle soundActivate: path: /Audio/Weapons/ebladeon.ogg @@ -156,14 +149,11 @@ path: /Audio/Weapons/ebladehum.ogg params: volume: -6 - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.4 - - type: ItemToggleEmbeddableProjectile - activatedOffset: 0.0,0.0 - activatedRemovalTime: 3 - - type: ItemToggleThrowingAngle - activatedAngle: 135 - deleteOnDeactivate: false + - type: ComponentToggler + components: + - type: Sharp + - type: DisarmMalus + malus: 0.4 - type: Sprite sprite: Objects/Weapons/Melee/e_dagger.rsi layers: @@ -247,15 +237,12 @@ id: EnergyCutlass description: An exotic energy weapon, brutal blade crackling with crudely harnessed plasma. #DeltaV - nicer description. components: - - type: EnergySword - type: ItemToggleMeleeWeapon activatedDamage: types: Slash: 10 Heat: 12 deactivatedSecret: true - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.6 - type: Sprite sprite: DeltaV/Objects/Weapons/Melee/e_cutlass.rsi # DeltaV layers: @@ -291,8 +278,8 @@ id: EnergySwordDouble description: Syndicate Command's intern thought that having only one blade on energy swords was not cool enough. This can be stored in pockets. components: - - type: EnergySword - type: ItemToggle + onUse: false # wielding events control it instead soundActivate: path: /Audio/Weapons/ebladeon.ogg params: @@ -317,9 +304,13 @@ path: /Audio/Weapons/ebladehum.ogg params: volume: 3 - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.7 + - type: ComponentToggler + components: + - type: Sharp + - type: DisarmMalus + malus: 0.7 - type: Wieldable + wieldSound: null # esword light sound instead - type: MeleeWeapon wideAnimationRotation: -135 attackRate: .6666 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml index d396aa60cd0..94e66f72ffd 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml @@ -8,6 +8,8 @@ tags: - FireAxe - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite sprite: Objects/Weapons/Melee/fireaxe.rsi state: icon diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml index 75e0e0764c0..aad5a410fdd 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml @@ -28,7 +28,7 @@ - type: Item size: Large - type: Tool - speed: 0.5 # it's very heavy, it rolls slower than a wooden bat + speedModifier: 0.5 # it's very heavy, it rolls slower than a wooden bat - type: UseDelay delay: 2 - type: PhysicalComposition diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml index 8b2649b48a5..e6641d35b13 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml @@ -7,6 +7,8 @@ tags: - Knife - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Utensil types: - Knife @@ -118,6 +120,13 @@ - type: MeleeWeapon wideAnimationRotation: -135 attackRate: .6666 + damage: + types: + Slash: 12 + - type: EmbeddableProjectile + sound: /Audio/Weapons/star_hit.ogg + - type: LandAtCursor + - type: DamageOtherOnHit damage: types: Slash: 9 @@ -186,6 +195,7 @@ Slash: 5 - type: DamageOtherOnHit staminaCost: 5 + - type: LandAtCursor - type: EmbeddableProjectile - type: EmbedPassiveDamage - type: Sprite @@ -310,6 +320,9 @@ damage: types: Slash: 5 + - type: EmbeddableProjectile + sound: /Audio/Weapons/star_hit.ogg + - type: LandAtCursor - type: DamageOtherOnHit ignoreResistances: true damage: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml index 900950cb9ec..b2506325862 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml @@ -1,3 +1,90 @@ +- type: entity + name: pickaxe + parent: BaseItem + id: Pickaxe + description: Notched to perfection, for jamming it into rocks. + components: + - type: Tag + tags: + - Pickaxe + - type: Sprite + sprite: Objects/Weapons/Melee/pickaxe.rsi + state: pickaxe + - type: MeleeWeapon + attackRate: 0.7 + wideAnimationRotation: -135 + soundHit: + path: "/Audio/Weapons/smash.ogg" + params: + volume: -3 + damage: + groups: + Brute: 5 + - type: Wieldable + - type: IncreaseDamageOnWield + damage: + groups: + Brute: 10 + types: + Structural: 30 + - type: Item + size: Normal + shape: + - 0,0,2,0 + - 1,1,1,2 + sprite: Objects/Weapons/Melee/pickaxe.rsi + storedRotation: -45 + - type: UseDelay + +- type: entity + name: mining drill + parent: BaseItem + id: MiningDrill + description: Powerful tool used to quickly drill through rocks. + components: + - type: Item + storedRotation: -90 + - type: Tag + tags: + - Pickaxe + - type: Sprite + sprite: Objects/Tools/handdrill.rsi + state: handdrill + - type: MeleeWeapon + autoAttack: true + angle: 0 + wideAnimationRotation: -90 + soundHit: + path: "/Audio/Items/drill_hit.ogg" + attackRate: 4 + damage: + groups: + Brute: 3 + types: + Structural: 15 + +- type: entity + name: diamond tipped mining drill + parent: MiningDrill + id: MiningDrillDiamond + description: A significantly more efficient mining drill tipped with diamond. + components: + - type: Sprite + sprite: Objects/Tools/handdrilldiamond.rsi + state: handdrill + - type: MeleeWeapon + autoAttack: true + angle: 0 + wideAnimationRotation: -90 + soundHit: + path: "/Audio/Items/drill_hit.ogg" + attackRate: 4 + damage: + groups: + Brute: 6 + types: + Structural: 30 + - type: entity abstract: true parent: BaseItem diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml deleted file mode 100644 index ff01e5692eb..00000000000 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml +++ /dev/null @@ -1,85 +0,0 @@ -- type: entity - name: pickaxe - parent: BaseItem - id: Pickaxe - description: Notched to perfection, for jamming it into rocks - components: - - type: Tag - tags: - - Pickaxe - - type: Sprite - sprite: Objects/Weapons/Melee/pickaxe.rsi - state: pickaxe - - type: MeleeWeapon - attackRate: 1.17 - range: 1.5 - wideAnimationRotation: -135 - soundHit: - path: "/Audio/Weapons/smash.ogg" - params: - volume: -3 - damage: - types: - Blunt: 6 - Piercing: 3 - bluntStaminaDamageFactor: 2.0 - heavyDamageBaseModifier: 1.75 - maxTargets: 5 - angle: 80 - - type: DamageOtherOnHit - staminaCost: 5 - - type: Wieldable - - type: IncreaseDamageOnWield - damage: - types: - Blunt: 5 - Structural: 30 - - type: Item - size: Normal - shape: - - 0,0,2,0 - - 1,1,1,2 - sprite: Objects/Weapons/Melee/pickaxe.rsi - storedRotation: -45 - - type: UseDelay - -- type: entity - name: mining drill - parent: BaseItem - id: MiningDrill - description: Powerful tool used to quickly drill through rocks - components: - - type: Item - storedRotation: -90 - - type: Tag - tags: - - Pickaxe - - type: Sprite - sprite: Objects/Tools/handdrill.rsi - state: handdrill - - type: MeleeWeapon - autoAttack: true - wideAnimationRotation: -90 - soundHit: - path: "/Audio/Items/drill_hit.ogg" - attackRate: .83 - range: 1.5 - damage: - types: - Blunt: 6 - Slash: 3 - Structural: 12 - bluntStaminaDamageFactor: 4.0 - heavyRateModifier: 1 - heavyRangeModifier: 2 - heavyDamageBaseModifier: 1 - angle: 20 - - type: DamageOtherOnHit - staminaCost: 8 - - type: ThrowingAngle - angle: 270 - - - type: ReverseEngineering # Nyano - difficulty: 2 - recipes: - - MiningDrill diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml index ca5efc2daf4..dcfd1537c79 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml @@ -9,6 +9,7 @@ - type: EmbedPassiveDamage - type: ThrowingAngle angle: 225 + - type: LandAtCursor - type: Tag tags: - Spear diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index 2da3d3bb92e..ffdbdc59112 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -1,10 +1,29 @@ - type: entity - name: captain's sabre + name: Sword parent: BaseItem + id: BaseSword + description: A sharp sword. + abstract: true + components: + - type: Sharp + - type: Execution + doAfterDuration: 4.0 + - type: MeleeWeapon + wideAnimationRotation: -135 + - type: Sprite + state: icon + - type: Item + size: Normal + - type: Utensil + types: + - Knife + +- type: entity + name: captain's sabre + parent: BaseSword id: CaptainSabre description: A ceremonial weapon belonging to the captain of the station. components: - - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/captain_sabre.rsi state: icon @@ -30,13 +49,7 @@ - type: ThrowingAngle angle: 225 - type: Reflect - enabled: true - # Design intent: a robust captain or tot can sacrifice movement to make the most of this weapon, but they have to - # really restrict themselves to walking speed or less. - reflectProb: 0.5 - velocityBeforeNotMaxProb: 1.0 - velocityBeforeMinProb: 3.0 - minReflectProb: 0.1 + reflectProb: .1 spread: 90 - type: Item size: Normal @@ -44,15 +57,13 @@ - type: Tag tags: - CaptainSabre - - type: DisarmMalus - type: entity name: katana - parent: BaseItem + parent: BaseSword id: Katana description: Ancient craftwork made with not so ancient plasteel. components: - - type: Sharp - type: Tag tags: - Katana @@ -122,11 +133,10 @@ - type: entity name: machete - parent: BaseItem + parent: BaseSword id: Machete description: A large, vicious looking blade. components: - - type: Sharp - type: Tag tags: - Machete @@ -155,15 +165,13 @@ - type: Item size: Normal sprite: Objects/Weapons/Melee/machete.rsi - - type: DisarmMalus - type: entity name: claymore - parent: BaseItem + parent: BaseSword id: Claymore description: An ancient war blade. components: - - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/claymore.rsi state: icon @@ -196,15 +204,13 @@ sprite: Objects/Weapons/Melee/claymore.rsi slots: - back - - type: DisarmMalus - type: entity name: cutlass - parent: BaseItem + parent: BaseSword id: Cutlass description: A wickedly curved blade, often seen in the hands of space pirates. components: - - type: Sharp - type: Tag tags: - Machete @@ -234,15 +240,13 @@ - type: Item size: Normal sprite: Objects/Weapons/Melee/cutlass.rsi - - type: DisarmMalus - type: entity name: The Throngler - parent: BaseItem + parent: BaseSword id: Throngler description: Why would someone make this? components: - - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/Throngler2.rsi state: icon @@ -261,13 +265,8 @@ path: /Audio/Effects/explosion_small1.ogg - type: DamageOtherOnHit - type: Reflect - enabled: true - reflectProb: 0.5 # In robust hands, deflects as well as an e-sword - velocityBeforeNotMaxProb: 1.0 - velocityBeforeMinProb: 3.0 - minReflectProb: 0.1 + reflectProb: .25 spread: 90 - type: Item size: Ginormous sprite: Objects/Weapons/Melee/Throngler-in-hand.rsi - - type: DisarmMalus diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/telescopic_baton.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/telescopic_baton.yml index 7851a9403b4..c35af489e93 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/telescopic_baton.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/telescopic_baton.yml @@ -20,8 +20,10 @@ path: /Audio/Weapons/telescopicoff.ogg params: volume: -2 - - type: ItemToggleDisarmMalus - activatedDisarmMalus: 0.6 + - type: ComponentToggler + components: + - type: DisarmMalus + malus: 0.6 - type: ItemToggleMeleeWeapon activatedDamage: types: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml index 7bbd0fe8935..f40f599e51c 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml @@ -70,3 +70,11 @@ Heat: 5 - type: Ensnaring destroyOnRemove: true + freeTime: 2.0 + breakoutTime: 3.5 #all bola should generally be fast to remove + walkSpeed: 0.7 #makeshift bola shouldn't slow too much + sprintSpeed: 0.7 + staminaDamage: 55 # Sudden weight increase sapping stamina + canThrowTrigger: true + canMoveBreakout: true + - type: LandAtCursor diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml index 36d4c947fcd..3f3ae0258d6 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml @@ -96,6 +96,7 @@ damage: types: Blunt: 10 + - type: LandAtCursor - type: Damageable damageContainer: Inorganic - type: EmitSoundOnTrigger @@ -224,6 +225,7 @@ damage: types: Blunt: 10 + - type: LandAtCursor - type: EmitSoundOnTrigger sound: path: "/Audio/Effects/flash_bang.ogg" diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/croissant.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/croissant.yml new file mode 100644 index 00000000000..8ae52f4d735 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/croissant.yml @@ -0,0 +1,25 @@ +- type: entity + parent: FoodBakedCroissant + id: WeaponCroissant + name: croissant + description: Buttery, flaky goodness. + suffix: Weapon + components: + - type: Fixtures + fixtures: + fix1: + shape: !type:PhysShapeCircle + radius: 0.2 + density: 5 + mask: + - ItemMask + restitution: 0.3 + friction: 0.2 + - type: EmbeddableProjectile + sound: /Audio/Weapons/star_hit.ogg + - type: LandAtCursor + - type: DamageOtherOnHit + damage: + types: + Slash: 5 + Piercing: 10 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml index 293d284d526..b6054124794 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml @@ -31,6 +31,7 @@ friction: 0.2 - type: EmbeddableProjectile sound: /Audio/Weapons/star_hit.ogg + - type: LandAtCursor - type: DamageOtherOnHit damage: types: diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml index 9f4adce96ae..616e4adec34 100644 --- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml +++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml @@ -27,6 +27,8 @@ - BaseStationNanotrasen - BaseRandomStation - BaseStationMail # Nyano component, required for station mail to function + - BaseStationCaptainState # DeltaV + - BaseStationStockMarket # DeltaV categories: [ HideSpawnMenu ] components: - type: Transform diff --git a/Resources/Prototypes/Entities/Structures/Dispensers/soda.yml b/Resources/Prototypes/Entities/Structures/Dispensers/soda.yml index 72468973ee4..e3042c798cc 100644 --- a/Resources/Prototypes/Entities/Structures/Dispensers/soda.yml +++ b/Resources/Prototypes/Entities/Structures/Dispensers/soda.yml @@ -1,9 +1,9 @@ - type: entity - id: soda_dispenser - name: soda dispenser - suffix: Filled parent: ReagentDispenserBase + id: SodaDispenser + name: soda dispenser description: A beverage dispenser with a selection of soda and several other common beverages. Has a single fill slot for containers. + suffix: Filled components: - type: Rotatable - type: Sprite @@ -24,9 +24,9 @@ - Bartender - type: entity + parent: SodaDispenser id: SodaDispenserEmpty suffix: Empty - parent: soda_dispenser components: - type: ReagentDispenser storageWhitelist: diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml index 7a926d66d37..2b8a3d889d6 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml @@ -33,8 +33,6 @@ bonkDamage: types: Blunt: 4 - bonkSound: !type:SoundCollectionSpecifier - collection: TrayHit - type: Clickable - type: FootstepModifier footstepSoundCollection: diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index df1a56866ba..4e499cc381b 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -759,6 +759,7 @@ - map: ["computerLayerKeys"] state: tech_key - type: CargoOrderConsole + - type: BankClient - type: ActiveRadio channels: - Supply diff --git a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml index 4cb76ea4b92..0485b5a5178 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml @@ -85,6 +85,7 @@ whitelist: components: - FitsInDispenser + - type: ItemToggle - type: HealthAnalyzer scanDelay: 0 - type: UserInterface diff --git a/Resources/Prototypes/Entities/Structures/Machines/chem_master.yml b/Resources/Prototypes/Entities/Structures/Machines/chem_master.yml index 4e565054b46..d78ed1182cd 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/chem_master.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/chem_master.yml @@ -1,5 +1,5 @@ - type: entity - id: chem_master + id: ChemMaster parent: [ BaseMachinePowered, ConstructibleMachine ] name: ChemMaster 4000 description: An industrial grade chemical manipulator with pill and bottle production included. diff --git a/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml b/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml index 6ee454c6a98..2880f819a72 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml @@ -50,9 +50,11 @@ behaviors: - !type:DoActsBehavior acts: ["Breakage"] - - type: GravityGenerator + - type: PowerCharge + windowTitle: gravity-generator-window-title idlePower: 50 activePower: 2500 + - type: GravityGenerator lightRadiusMin: 0.75 lightRadiusMax: 2.5 spriteMap: @@ -60,10 +62,13 @@ unpowered: "off" off: "off" on: "on" + - type: ActivatableUI + key: enum.PowerChargeUiKey.Key + - type: ActivatableUIRequiresPower - type: UserInterface interfaces: - enum.GravityGeneratorUiKey.Key: - type: GravityGeneratorBoundUserInterface + enum.PowerChargeUiKey.Key: + type: PowerChargeBoundUserInterface - type: Appearance - type: PointLight radius: 2.5 @@ -124,9 +129,10 @@ board: MiniGravityGeneratorCircuitboard - type: ApcPowerReceiver powerLoad: 500 - - type: GravityGenerator + - type: PowerCharge idlePower: 15 activePower: 500 + - type: GravityGenerator lightRadiusMin: 0.75 lightRadiusMax: 2.5 - type: StaticPrice diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index af5eef0ed3b..75b593fbcc0 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -16,7 +16,7 @@ mask: - MachineMask layer: - - MachineLayer + - MachineLayer - type: Lathe - type: MaterialStorage - type: Destructible @@ -270,6 +270,7 @@ dynamicRecipes: - PowerDrill - MiningDrill + - MiningDrillDiamond - AnomalyScanner - AnomalyLocator - AnomalyLocatorWide @@ -318,6 +319,7 @@ - SynthesizerInstrument - RPED - ClothingShoesBootsMagSci + - ClothingShoesBootsMoon - ClothingShoesBootsSpeed - NodeScanner - HolofanProjector @@ -467,6 +469,7 @@ - SodaDispenserMachineCircuitboard - DeepFryerMachineCircuitboard #Nyano - Summary: adds deep fryer circuit board - SpaceHeaterMachineCircuitBoard + - StationAnchorCircuitboard dynamicRecipes: - ThermomachineFreezerMachineCircuitBoard - HellfireFreezerMachineCircuitBoard @@ -817,6 +820,7 @@ - TargetHuman - TargetSyndicate - WeaponDisablerPractice + - WeaponFlareGunSecurity - WeaponLaserCarbinePractice - Zipties - ShockCollar @@ -1304,22 +1308,27 @@ - type: Lathe idleState: icon runningState: building + defaultProductionAmount: 10 staticRecipes: - BluespaceCrystal - NormalityCrystal - - SheetSteel30 - - SheetGlass30 - - SheetRGlass30 - - SheetPlasma30 - - SheetPGlass30 - - SheetRPGlass30 - - SheetUranium30 - - IngotGold30 - - IngotSilver30 - - MaterialBananium10 + - SheetSteel + - SheetGlass1 + - SheetRGlass + - SheetPlasma1 + - SheetPGlass1 + - SheetRPGlass1 + - SheetUranium1 + - IngotGold1 + - IngotSilver1 + - MaterialBananium1 - type: MaterialStorageMagnetPickup # Delta V - Summary: Adds magnet pull from Frontier magnetEnabled: True range: 0.30 # Delta V - End Magnet Pull + - type: MiningPoints # DeltaV - Source of mining points for miners + transferSound: + path: /Audio/Effects/Cargo/ping.ogg + - type: MiningPointsLathe # DeltaV - type: entity parent: OreProcessor @@ -1337,19 +1346,20 @@ staticRecipes: - BluespaceCrystal - NormalityCrystal - - SheetSteel30 - - SheetGlass30 - - SheetRGlass30 - - SheetPlasma30 - - SheetPGlass30 - - SheetRPGlass30 - - SheetPlasteel30 - - SheetUranium30 - - SheetUGlass30 - - SheetRUGlass30 - - IngotGold30 - - IngotSilver30 - - MaterialBananium10 + - SheetSteel + - SheetGlass1 + - SheetRGlass + - SheetPlasma1 + - SheetPGlass1 + - SheetRPGlass1 + - SheetPlasteel1 + - SheetUranium1 + - SheetUGlass1 + - SheetRUGlass1 + - IngotGold1 + - IngotSilver1 + - MaterialBananium1 + - MaterialDiamond - type: entity parent: BaseLathe diff --git a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml index 994269f71b4..b43ae829064 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml @@ -105,3 +105,25 @@ - type: GuideHelp guides: - Chef + +- type: entity + id: SyndicateMicrowave + parent: KitchenMicrowave + name: donk co. microwave + description: So advanced, it can cook donk-pockets in a mere 2.5 seconds! + components: + - type: Microwave + cookTimeMultiplier: 0.5 + capacity: 10 + canMicrowaveIdsSafely: false + explosionChance: 0.3 + - type: Sprite + sprite: Structures/Machines/microwave_syndie.rsi + drawdepth: SmallObjects + snapCardinals: true + - type: Machine + board: SyndicateMicrowaveMachineCircuitboard + - type: FoodRecipeProvider + providedRecipes: + - RecipeBaguetteSword + - RecipeThrowingCroissant diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index c79cfa2641c..f9e945c8697 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -111,8 +111,6 @@ understands: - TauCetiBasic - RobotTalk - - type: VendingMachine - soundVend: /Audio/SimpleStation14/Machines/machine_vend.ogg - type: entity parent: VendingMachine @@ -1524,12 +1522,12 @@ name: Salvage Vendor description: A dwarf's best friend! components: - - type: VendingMachine - pack: SalvageEquipmentInventory - offState: off - brokenState: broken - normalState: normal-unshaded - denyState: deny-unshaded + #- type: VendingMachine # DeltaV: Use mining points instead of limited stock + # pack: SalvageEquipmentInventory + # offState: off + # brokenState: broken + # normalState: normal-unshaded + # denyState: deny-unshaded - type: Sprite sprite: Structures/Machines/VendingMachines/mining.rsi layers: @@ -1546,6 +1544,21 @@ color: "#9dc5c9" mask: /Textures/Effects/LightMasks/cone.png autoRot: true + - type: ShopVendor # DeltaV + pack: SalvageVendorInventory + offState: off + brokenState: broken + normalState: normal-unshaded + denyState: deny-unshaded + - type: PointsVendor # DeltaV + - type: UserInterface # DeltaV: Replace vending machine BUI with shop vendor + interfaces: + enum.VendingMachineUiKey.Key: + type: ShopVendorBoundUserInterface + enum.WiresUiKey.Key: + type: WiresBoundUserInterface + - type: Wires # DeltaV: Use shop vendor wires layout + layoutId: ShopVendor - type: AccessReader access: [["Salvage"]] - type: GuideHelp diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/station_anchor.yml b/Resources/Prototypes/Entities/Structures/Shuttles/station_anchor.yml new file mode 100644 index 00000000000..4e9a63f2af3 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Shuttles/station_anchor.yml @@ -0,0 +1,121 @@ +- type: entity + id: StationAnchorBase + abstract: true + name: station anchor + description: Prevents stations from moving. + suffix: On + placement: + mode: AlignTileAny + components: + - type: StationAnchor + - type: Transform + anchored: true + - type: Physics + bodyType: Static + - type: AmbientSound + enabled: false + range: 4 + volume: -4 + sound: + path: /Audio/Effects/shuttle_thruster.ogg + - type: InteractionOutline + - type: Sprite + sprite: Structures/Machines/station_anchor.rsi + layers: + - state: station_anchor + map: ["base"] + - state: station_anchor_unlit + shader: unshaded + map: ["unlit"] + - type: GenericVisualizer + visuals: + enum.PowerChargeVisuals.Active: + unlit: + True: { visible: True } + False: { visible: False } + - type: Appearance + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.7,-0.8,0.7,0.8" + density: 190 + mask: + - LargeMobMask + layer: + - WallLayer + + +- type: entity + id: StationAnchorIndestructible + parent: StationAnchorBase + suffix: Indestructible, Unpowered + +- type: entity + id: StationAnchor + parent: [StationAnchorBase, BaseMachinePowered, ConstructibleMachine] + name: station anchor + description: Prevents stations from moving + placement: + mode: AlignTileAny + components: + - type: PowerCharge + windowTitle: station-anchor-window-title + idlePower: 50 + activePower: 2500 + chargeRate: 0.5 + - type: ActivatableUI + key: enum.PowerChargeUiKey.Key + - type: ActivatableUIRequiresPower + - type: Anchorable + - type: ApcPowerReceiver + powerLoad: 2500 + - type: ExtensionCableReceiver + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: Repairable + fuelCost: 10 + doAfterDelay: 5 + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 150 + behaviors: + - !type:DoActsBehavior + acts: [ "Breakage" ] + - trigger: + !type:DamageTrigger + damage: 600 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - type: StaticPrice + price: 10000 + - type: Machine + board: StationAnchorCircuitboard + - type: ContainerContainer + containers: + machine_board: !type:Container + machine_parts: !type:Container + - type: Construction + containers: + - machine_parts + - machine_board + - type: UserInterface + interfaces: + enum.PowerChargeUiKey.Key: + type: PowerChargeBoundUserInterface + +- type: entity + parent: StationAnchor + id: StationAnchorOff + suffix: Off + components: + - type: StationAnchor + switchedOn: false diff --git a/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml b/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml index 934298b6207..df19550cdbd 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml @@ -25,8 +25,7 @@ - type: ReagentTank tankType: Fuel - type: DamageOnToolInteract - tools: - - Welding + tools: Welding weldingDamage: types: Heat: 10 @@ -217,4 +216,3 @@ fillBaseName: watertank-2- - type: ExaminableSolution solution: tank - diff --git a/Resources/Prototypes/Entities/Structures/Storage/barrels.yml b/Resources/Prototypes/Entities/Structures/Storage/barrels.yml index 0cbbb2ede8b..052c21206b5 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/barrels.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/barrels.yml @@ -192,8 +192,7 @@ - ReagentId: WeldingFuel Quantity: 500 - type: DamageOnToolInteract - tools: - - Welding + tools: Welding weldingDamage: types: Heat: 10 diff --git a/Resources/Prototypes/Entities/Structures/Storage/tanks.yml b/Resources/Prototypes/Entities/Structures/Storage/tanks.yml index 41038fb74d1..a9875172e59 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/tanks.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/tanks.yml @@ -81,8 +81,7 @@ - type: ReagentTank tankType: Fuel - type: DamageOnToolInteract - tools: - - Welding + tools: Welding weldingDamage: types: Heat: 10 diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml index ceb9f94b580..35df7765a6e 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml @@ -244,6 +244,15 @@ - type: Sprite state: ai +- type: entity + parent: BaseSign + id: SignAiUpload + name: ai upload sign + description: A sign, indicating an AI is present. + components: + - type: Sprite + state: ai_upload + - type: entity parent: BaseSign id: SignArcade @@ -274,11 +283,11 @@ - type: entity parent: BaseSign id: SignAnomaly - name: xenoarchaeology lab sign + name: xenoarcheology lab sign description: A sign indicating the xenoarchaeology lab. components: - type: Sprite - state: anomaly + state: xenoarch - type: entity parent: BaseSign @@ -287,7 +296,7 @@ description: A sign indicating the anomalous research lab. components: - type: Sprite - state: anomaly2 + state: anomaly - type: entity parent: BaseSign @@ -300,21 +309,38 @@ - type: entity parent: BaseSign - id: SignAtmosMinsky - name: atmospherics sign - description: A sign indicating the atmospherics area. + id: SignBar + name: bar sign + description: A sign indicating the bar. components: - type: Sprite - state: atmominsky + state: bar - type: entity parent: BaseSign - id: SignBar - name: bar sign - description: A sign indicating the bar. + id: SignKitchen + name: kitchen sign + description: The heart of the home. And disease. components: - type: Sprite - state: bar + state: kitchen + +- type: entity + parent: BaseSign + id: SignTheater + name: theater sign + description: Would it even be Space Station without drama? + components: + - type: Sprite + layers: + - state: drama1 + map: [ "base" ] + - type: RandomSprite + available: + - base: + drama1: null + drama2: null + drama3: null - type: entity parent: BaseSign @@ -397,24 +423,6 @@ - type: Sprite state: chem -- type: entity - parent: BaseSign - id: SignChemistry1 - name: chemistry sign - description: A sign indicating the chemistry lab. - components: - - type: Sprite - state: chemistry1 - -- type: entity - parent: BaseSign - id: SignChemistry2 - name: chemistry sign - description: A sign indicating the chemistry lab. - components: - - type: Sprite - state: chemistry2 - - type: entity parent: BaseSign id: SignCloning @@ -428,19 +436,20 @@ parent: BaseSign id: SignConference name: conference room sign - description: A sign indicating the conference room. + description: Where work happens. components: - type: Sprite state: conference_room - type: entity parent: BaseSign - id: SignCourt - name: court sign - description: A sign labelling the courtroom. + id: SignCryo + name: cryosleep sign + description: Just like that? You're gonna chicken out? components: - type: Sprite - state: court + state: cryo + - type: entity parent: BaseSign @@ -462,18 +471,27 @@ - type: entity parent: BaseSign - id: SignDrones - name: drones sign - description: A sign indicating drones. + id: SignRestroom + name: restroom sign + description: A sign indicating where you go to... What do you do here again? components: - type: Sprite - state: drones + state: restroom + +- type: entity + parent: BaseSign + id: SignMaterials + name: materials sign + description: An omen to the juicy vault of steel, glass, and plastic that lays before you. + components: + - type: Sprite + state: mats - type: entity parent: BaseSign id: SignEngine - name: engine sign - description: A sign indicating the engine room. + name: power sign + description: Where the powa happens. components: - type: Sprite state: engine @@ -547,7 +565,7 @@ parent: BaseSign id: SignHead name: head sign - description: A sign with a hat on it. + description: An official sign indicating the dwellings of a Nanotrasen-certified head of department. components: - type: Sprite state: commander @@ -559,25 +577,7 @@ description: A sign indicating a hydroponics area. components: - type: Sprite - state: hydro1 - -- type: entity - parent: BaseSign - id: SignHydro2 - name: hydro sign - description: A sign indicating a hydroponics area. - components: - - type: Sprite - state: hydro2 - -- type: entity - parent: BaseSign - id: SignHydro3 - name: hydro sign - description: A sign indicating a hydroponics area. - components: - - type: Sprite - state: hydro3 + state: hydro - type: entity parent: BaseSign @@ -609,8 +609,8 @@ - type: entity parent: BaseSign id: SignLawyer - name: lawyer sign - description: A sign labelling an area where the Lawyers work. + name: law sign + description: A sign indicating the presence of the (typically absent) rule of law. components: - type: Sprite state: law @@ -642,15 +642,6 @@ - type: Sprite state: medbay -- type: entity - parent: BaseSign - id: SignMinerDock - name: miner dock sign - description: A sign indicating the miner dock. - components: - - type: Sprite - state: miner_dock - - type: entity parent: BaseSign id: SignMorgue @@ -743,36 +734,27 @@ - type: entity parent: BaseSign - id: SignScience1 - name: science sign - description: A sign indicating the science area. + id: SignServer + name: server sign + description: Ever heard of Big Data? This is it, chump. The biggest. components: - type: Sprite - state: science1 + state: data - type: entity parent: BaseSign - id: SignScience2 - name: science sign - description: A sign indicating the science area. - components: - - type: Sprite - state: science2 - -- type: entity - parent: BaseSign - id: SignShield - name: shield sign - description: A sign with a shield. + id: SignCans + name: canisters sign + description: A sign indicating the auspicious presence of gas canisters. components: - type: Sprite - state: shield + state: cans - type: entity parent: BaseSign id: SignShipDock - name: docking sign - description: A sign indicating the ship docking area. + name: evac sign + description: A sign indicating the where the evac shuttle will (likely) arrive. components: - type: Sprite state: dock @@ -817,12 +799,12 @@ - type: entity parent: BaseSign - id: SignToxins2 - name: toxins sign - description: A sign indicating the toxin lab. + id: SignVault + name: vault sign + description: A sign indicating the vault. Who knows what secrets lie inside? components: - type: Sprite - state: toxins2 + state: vault - type: entity parent: BaseSign @@ -969,29 +951,11 @@ - type: Sprite state: xenobio -- type: entity - parent: BaseSign - id: SignXenobio2 - name: xenobio sign - description: A sign indicating the xenobiology lab. - components: - - type: Sprite - state: xenobio2 - -- type: entity - parent: BaseSign - id: SignXenolab - name: xenolab sign - description: A sign indicating the xenobiology lab. - components: - - type: Sprite - state: xenolab - - type: entity parent: BaseSign id: SignZomlab name: zombie lab sign - description: A sign indicating the zombie lab. + description: The final remains of a shut-down Nanotrasen research project that aimed to harness the powers of Romerol. I wonder how that went... components: - type: Sprite state: zomlab diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml index 6c6b8bf57e9..9a244c2c598 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml @@ -1,5 +1,5 @@ - type: entity - id: Intercom + id: BaseIntercom name: intercom description: An intercom. For when the station just needs to know something. abstract: true @@ -9,6 +9,10 @@ - type: Electrified enabled: false usesApcPower: true + - type: TelecomExempt + - type: EncryptionKeyHolder + keySlots: 3 + keysExtractionMethod: Prying - type: RadioMicrophone powerRequired: true unobstructedRequired: true @@ -27,12 +31,14 @@ - type: InteractionOutline - type: Appearance - type: WiresVisuals + - type: WiresPanelSecurity - type: ContainerFill containers: board: [ IntercomElectronics ] - type: ContainerContainer containers: board: !type:Container + key_slots: !type:Container - type: Sprite noRot: false drawdepth: SmallObjects @@ -52,7 +58,6 @@ visible: false - state: panel map: ["enum.WiresVisualLayers.MaintenancePanel"] - shader: unshaded visible: false - type: Transform noRot: false @@ -64,6 +69,7 @@ - type: ActivatableUIRequiresPower - type: ActivatableUI key: enum.IntercomUiKey.Key + singleUser: true - type: UserInterface interfaces: enum.IntercomUiKey.Key: @@ -119,7 +125,7 @@ - Wallmount - type: entity - id: IntercomAssesmbly + id: IntercomAssembly name: intercom assembly description: An intercom. It doesn't seem very helpful right now. components: @@ -129,7 +135,18 @@ - type: Sprite drawdepth: SmallObjects sprite: Structures/Wallmounts/intercom.rsi - state: build + layers: + - state: build + - state: panel + visible: false + map: [ "wires" ] + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ConstructionVisuals.Layer: + wires: + 0: { visible: false } + 1: { visible: true } - type: Construction graph: Intercom node: assembly @@ -140,97 +157,176 @@ snap: - Wallmount +# this weird inheritance BS exists for construction shitcode +- type: entity + id: IntercomConstructed + parent: BaseIntercom + suffix: Empty, Panel Open + components: + - type: Sprite + layers: + - state: base + - state: unshaded + map: ["enum.PowerDeviceVisualLayers.Powered"] + shader: unshaded + - state: broadcasting + map: ["enum.RadioDeviceVisualLayers.Broadcasting"] + shader: unshaded + visible: false + - state: speaker + map: ["enum.RadioDeviceVisualLayers.Speaker"] + shader: unshaded + visible: false + - state: panel + map: ["enum.WiresVisualLayers.MaintenancePanel"] + visible: true + - type: WiresPanel + open: true + +- type: entity + id: Intercom + parent: IntercomConstructed + suffix: "" + components: + - type: Sprite + layers: + - state: base + - state: unshaded + map: ["enum.PowerDeviceVisualLayers.Powered"] + shader: unshaded + - state: broadcasting + map: ["enum.RadioDeviceVisualLayers.Broadcasting"] + shader: unshaded + visible: false + - state: speaker + map: ["enum.RadioDeviceVisualLayers.Speaker"] + shader: unshaded + visible: false + - state: panel + map: ["enum.WiresVisualLayers.MaintenancePanel"] + - type: WiresPanel + open: false + - type: entity id: IntercomCommon parent: Intercom suffix: Common components: - - type: Intercom - supportedChannels: - - Common + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon - type: entity id: IntercomCommand parent: Intercom suffix: Command components: - - type: Intercom - supportedChannels: - - Common - - Command + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyCommand - type: entity id: IntercomEngineering parent: Intercom suffix: Engineering components: - - type: Intercom - supportedChannels: - - Common - - Engineering + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyEngineering - type: entity id: IntercomMedical parent: Intercom suffix: Medical components: - - type: Intercom - supportedChannels: - - Common - - Medical + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyMedical - type: entity id: IntercomScience parent: Intercom suffix: Epistemics # DeltaV - Epistemics Department replacing Science components: - - type: Intercom - supportedChannels: - - Common - - Science + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyScience - type: entity id: IntercomSecurity parent: Intercom suffix: Security + description: An intercom. It's been reinforced with metal from security helmets, making it a bitch-and-a-half to open. components: - - type: Intercom - supportedChannels: - - Common - - Security + - type: WiresPanel + openDelay: 5 + - type: WiresPanelSecurity + examine: wires-panel-component-on-examine-security-level2 + wiresAccessible: false + - type: Construction + node: intercomReinforced + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeySecurity - type: entity id: IntercomService parent: Intercom suffix: Service components: - - type: Intercom - supportedChannels: - - Common - - Service + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyService - type: entity id: IntercomSupply parent: Intercom suffix: Supply components: - - type: Intercom - supportedChannels: - - Common - - Supply + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyCargo - type: entity id: IntercomAll parent: Intercom suffix: All components: - - type: Intercom - supportedChannels: - - Common - - Command - - Engineering - - Medical - - Science - - Security - - Service - - Supply + - type: ContainerFill + containers: + board: + - IntercomElectronics + key_slots: + - EncryptionKeyCommon + - EncryptionKeyStationMaster diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/shelfs.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/shelfs.yml new file mode 100644 index 00000000000..af68f5898d2 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/shelfs.yml @@ -0,0 +1,475 @@ +# Parents +- type: entity + abstract: true + id: ShelfBase + parent: BaseStructure + name: shelf + description: a strange place to place, well, anything really. You feel like you shouldn't be seeing this.' + components: + - type: Sprite + drawdepth: WallMountedItems + sprite: Structures/Storage/Shelfs/wood.rsi + state: base + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.35,-0.35,0.35,0.35" + density: 35 + layer: + - BulletImpassable + - type: Transform + - type: Damageable + damageModifierSet: Wood + damageContainer: Inorganic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 60 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: WoodDestroyHeavy + - !type:DoActsBehavior + acts: ["Destruction"] + - type: WallMount + arc: 175 + - type: Storage + grid: + - 0,0,3,1 + - 0,3,3,4 + maxItemSize: Normal + - type: UserInterface + interfaces: + enum.StorageUiKey.Key: + type: StorageBoundUserInterface + - type: InteractionOutline + - type: ContainerContainer + containers: + storagebase: !type:Container + - type: Tag + tags: + - Structure + +- type: entity + abstract: true + id: ShelfBaseReinforced + parent: ShelfBase + name: reinforced shelf + description: It looks as strong as reality itself. + components: + - type: Lock + - type: LockVisuals + - type: Sprite + sprite: Structures/Storage/Shelfs/wood.rsi + state: base + layers: + - state: rbase + map: ["enum.StorageVisualLayers.Base"] + - state: unlocked + shader: unshaded + # used to keep the unlocked light visible while open. + - state: closed + map: ["enum.StorageVisualLayers.Door"] + - state: locked + map: ["enum.LockVisualLayers.Lock"] + shader: unshaded + - type: Appearance + - type: EntityStorageVisuals + stateDoorOpen: open + stateDoorClosed: closed + + - type: AccessReader + +# Normal +- type: entity + id: ShelfWood + parent: ShelfBase + name: wooden shelf + description: A convenient place to place, well, anything really. + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 60 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: WoodDestroyHeavy + - !type:SpawnEntitiesBehavior + spawn: + MaterialWoodPlank1: + min: 1 + max: 3 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Tag + tags: + - Structure + - Wooden + - type: Construction + graph: Shelf + node: ShelfWood + +- type: entity + id: ShelfMetal + parent: ShelfBase + name: metal shelf + description: A sturdy place to place, well, anything really. + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/metal.rsi + state: base + - type: Damageable + damageModifierSet: Metallic + damageContainer: Inorganic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 120 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 2 + max: 4 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Tag + tags: + - Structure + - type: Construction + graph: Shelf + node: ShelfMetal + +- type: entity + id: ShelfGlass + parent: ShelfBase + name: glass shelf + description: A fragile place to place, well, anything really. + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/glass.rsi + state: base + - type: Damageable + damageModifierSet: Glass + damageContainer: Inorganic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 50 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: WindowShatter + - !type:SpawnEntitiesBehavior + spawn: + ShardGlass: + min: 0 + max: 2 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Tag + tags: + - Structure + - type: Construction + graph: Shelf + node: ShelfGlass + +# Reinforced +- type: entity + id: ShelfRWood + parent: ShelfBaseReinforced + name: sturdy wood shelf + description: A safe place to put your favorite bottle of whiskey + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/wood.rsi + state: base + layers: + - state: rbase + map: ["enum.StorageVisualLayers.Base"] + - state: closed + map: ["enum.StorageVisualLayers.Door"] + - state: locked + map: ["enum.LockVisualLayers.Lock"] + shader: unshaded + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 215 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: WoodDestroyHeavy + - !type:SpawnEntitiesBehavior + spawn: + MaterialWoodPlank: + min: 2 + max: 5 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Construction + graph: Shelf + node: ShelfRWood + +- type: entity + id: ShelfRMetal + parent: ShelfBaseReinforced + name: sturdy metal shelf + description: A strong & shiny place to keep all your vials safe + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/metal.rsi + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 450 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetPlasteel1: + min: 2 + max: 3 + ShardGlass: + min: 1 + max: 2 + PartRodMetal1: + min: 1 + max: 2 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Construction + graph: Shelf + node: ShelfRMetal + +- type: entity + id: ShelfRGlass + parent: ShelfBaseReinforced + name: sturdy glass shelf + description: Crystal clear reinforced glass doors to show off all your fancy bottles you definitely didn't sell a co-worker's favorite mothroach for. + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/glass.rsi + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 250 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: WindowShatter + - !type:SpawnEntitiesBehavior + spawn: + SheetPlastic1: + min: 1 + max: 3 + ShardGlass: + min: 1 + max: 2 + PartRodMetal1: + min: 0 + max: 1 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Construction + graph: Shelf + node: ShelfRGlass + +# Departmental +- type: entity + id: ShelfBar + parent: ShelfBase + name: bar shelf + description: Made out of the finest synthetic wood for all alcohol holding needs. + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/Departments/Service/bar.rsi + state: base + layers: + - state: base + - state: bar-0 + - map: ["enum.StorageFillLayers.Fill"] + - type: Appearance + - type: StorageFillVisualizer + maxFillLevels: 13 + fillBaseName: bar + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 100 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: WoodDestroyHeavy + - !type:SpawnEntitiesBehavior + spawn: + MaterialWoodPlank1: + min: 1 + max: 4 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Storage + grid: + - 0,0,5,1 + - 0,3,5,4 + maxItemSize: Normal + whitelist: + tags: + - DrinkGlass + - DrinkBottle + - DrinkCan + - Beer + - type: Construction + graph: Shelf + node: ShelfBar + +- type: entity + id: ShelfKitchen + parent: ShelfBase + name: cooking shelf + description: Holds knifes, spice, and everything nice! + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/Departments/Service/kitchen.rsi + state: base + layers: + - state: base + - state: kitchen-0 + - map: ["enum.StorageFillLayers.Fill"] + - type: Appearance + - type: StorageFillVisualizer + maxFillLevels: 13 + fillBaseName: kitchen + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 150 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 4 + MaterialWoodPlank1: + min: 0 + max: 1 + PartRodMetal1: + min: 0 + max: 2 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Storage + grid: + - 0,0,5,1 + - 0,3,5,4 + maxItemSize: Normal + whitelist: + tags: + - DrinkGlass + - BoxCardboard + - MonkeyCube + - Enzyme + - Mayo + - Packet + - Cleaver + - Knife + - KitchenKnife + - RollingPin + - Ingredient + - Trash + - type: Construction + graph: Shelf + node: ShelfKitchen + +- type: entity + id: ShelfChemistry + parent: ShelfBaseReinforced + name: chemical shelf + description: Keeps all your chemicals safe and out of the clow- er, public hands! + components: + - type: Sprite + sprite: Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi + layers: + - state: base + map: ["enum.StorageVisualLayers.Base"] + - state: unlocked + shader: unshaded + - state: chem-0 + - map: ["enum.StorageFillLayers.Fill"] + - state: closed + map: ["enum.StorageVisualLayers.Door"] + - state: locked + map: ["enum.LockVisualLayers.Lock"] + shader: unshaded + - type: StorageFillVisualizer + maxFillLevels: 7 + fillBaseName: chem + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 330 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetPlasteel1: + min: 1 + max: 2 + SheetPlastic1: + min: 1 + max: 2 + ShardGlass: + min: 1 + max: 1 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Storage + grid: + - 0,0,5,1 + - 0,3,5,4 + maxItemSize: Normal + whitelist: + tags: + - ChemDispensable + - GlassBeaker + - Bottle + - type: Construction + graph: Shelf + node: ShelfChemistry + + + +# Access presets +# Try to keep alphabetical sorting if adding more + +- type: entity + parent: ShelfChemistry + id: ShelfChemistryChemistrySecure + suffix: Chemistry, Secure + components: + - type: AccessReader + access: [["Chemistry"]] + diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml index 3347b0e4b1c..954caf67aeb 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml @@ -4,6 +4,7 @@ name: camera description: A surveillance camera. It's watching you. Kinda. components: + - type: StationAiVision - type: Clickable - type: InteractionOutline - type: Construction diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/walldispenser.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/walldispenser.yml index 72ea308af08..3570264a57a 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/walldispenser.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/walldispenser.yml @@ -82,8 +82,7 @@ - type: ReagentTank tankType: Fuel - type: DamageOnToolInteract - tools: - - Welding + tools: Welding weldingDamage: types: Heat: 20 diff --git a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml index 7b8d145e624..552e3fc6a5f 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml @@ -91,6 +91,28 @@ state: rock_asteroid_west - state: rock_gold +- type: entity + id: AsteroidRockDiamond + parent: AsteroidRock + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock_asteroid + - map: [ "enum.EdgeLayer.South" ] + state: rock_asteroid_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_asteroid_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_asteroid_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_asteroid_west + - state: rock_diamond + - type: entity id: AsteroidRockPlasma parent: AsteroidRock @@ -498,6 +520,28 @@ state: rock_west - state: rock_gold +- type: entity + id: WallRockDiamond + parent: WallRock + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_diamond + - type: entity id: WallRockPlasma parent: WallRock @@ -786,6 +830,28 @@ state: rock_wall_west - state: rock_gold +- type: entity + id: WallRockBasaltDiamond + parent: WallRockBasalt + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock_wall + - map: [ "enum.EdgeLayer.South" ] + state: rock_wall_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_wall_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_wall_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_wall_west + - state: rock_diamond + - type: entity id: WallRockBasaltPlasma parent: WallRockBasalt @@ -1073,6 +1139,28 @@ state: rock_snow_west - state: rock_gold +- type: entity + id: WallRockSnowDiamond + parent: WallRockSnow + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock_snow + - map: [ "enum.EdgeLayer.South" ] + state: rock_snow_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_snow_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_snow_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_snow_west + - state: rock_diamond + - type: entity id: WallRockSnowPlasma parent: WallRockSnow @@ -1360,6 +1448,28 @@ state: rock_sand_west - state: rock_gold +- type: entity + id: WallRockSandDiamond + parent: WallRockSand + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock_sand + - map: [ "enum.EdgeLayer.South" ] + state: rock_sand_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_sand_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_sand_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_sand_west + - state: rock_diamond + - type: entity id: WallRockSandPlasma parent: WallRockSand @@ -1470,7 +1580,6 @@ state: rock_sand_west - state: rock_uranium - - type: entity id: WallRockSandBananium parent: WallRockSand @@ -1647,6 +1756,28 @@ state: rock_chromite_west - state: rock_gold +- type: entity + id: WallRockChromiteDiamond + parent: WallRockChromite + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock_chromite + - map: [ "enum.EdgeLayer.South" ] + state: rock_chromite_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_chromite_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_chromite_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_chromite_west + - state: rock_diamond + - type: entity id: WallRockChromitePlasma parent: WallRockChromite @@ -1934,6 +2065,28 @@ state: rock_andesite_west - state: rock_gold +- type: entity + id: WallRockAndesiteDiamond + parent: WallRockAndesite + description: An ore vein rich with diamonds. + suffix: Diamond + components: + - type: OreVein + oreChance: 1.0 + currentOre: OreDiamond + - type: Sprite + layers: + - state: rock_andesite + - map: [ "enum.EdgeLayer.South" ] + state: rock_andesite_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_andesite_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_andesite_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_andesite_west + - state: rock_diamond + - type: entity id: WallRockAndesitePlasma parent: WallRockAndesite diff --git a/Resources/Prototypes/Entities/Structures/gates.yml b/Resources/Prototypes/Entities/Structures/gates.yml index 22ab745a1ee..6b02840ba0b 100644 --- a/Resources/Prototypes/Entities/Structures/gates.yml +++ b/Resources/Prototypes/Entities/Structures/gates.yml @@ -15,9 +15,10 @@ - type: entity parent: BaseLogicItem - id: LogicGate + id: LogicGateOr name: logic gate description: A logic gate with two inputs and one output. Technicians can change its mode of operation using a screwdriver. + suffix: Or components: - type: Sprite layers: @@ -51,6 +52,71 @@ Nand: { state: nand } Xnor: { state: xnor } +- type: entity + parent: LogicGateOr + id: LogicGateAnd + suffix: And + components: + - type: Sprite + layers: + - state: base + - state: and + map: [ "enum.LogicGateLayers.Gate" ] + - type: LogicGate + gate: And + +- type: entity + parent: LogicGateOr + id: LogicGateXor + suffix: Xor + components: + - type: Sprite + layers: + - state: base + - state: xor + map: [ "enum.LogicGateLayers.Gate" ] + - type: LogicGate + gate: Xor + +- type: entity + parent: LogicGateOr + id: LogicGateNor + suffix: Nor + components: + - type: Sprite + layers: + - state: base + - state: nor + map: [ "enum.LogicGateLayers.Gate" ] + - type: LogicGate + gate: Nor + +- type: entity + parent: LogicGateOr + id: LogicGateNand + suffix: Nand + components: + - type: Sprite + layers: + - state: base + - state: nand + map: [ "enum.LogicGateLayers.Gate" ] + - type: LogicGate + gate: Nand + +- type: entity + parent: LogicGateOr + id: LogicGateXnor + suffix: Xnor + components: + - type: Sprite + layers: + - state: base + - state: xnor + map: [ "enum.LogicGateLayers.Gate" ] + - type: LogicGate + gate: Xnor + - type: entity parent: BaseLogicItem id: EdgeDetector diff --git a/Resources/Prototypes/Entities/Tiles/lava.yml b/Resources/Prototypes/Entities/Tiles/lava.yml index 9d61304af9d..3bc1cf1d239 100644 --- a/Resources/Prototypes/Entities/Tiles/lava.yml +++ b/Resources/Prototypes/Entities/Tiles/lava.yml @@ -16,8 +16,12 @@ triggerGroups: types: - Lava - - type: Lava - fireStacks: 0.75 + - type: TileEntityEffect + effects: + - !type:FlammableReaction + multiplier: 3.75 + multiplierOnExisting: 0.75 + - !type:Ignite - type: Transform anchored: true - type: SyncSprite diff --git a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml index 500286ead31..3de5f5c120f 100644 --- a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml +++ b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity id: FloorLiquidPlasmaEntity name: liquid plasma description: Sweet, expensive nectar. Don't consume. @@ -16,8 +16,12 @@ triggerGroups: types: - Lava - - type: Lava - fireStacks: 0.75 + - type: TileEntityEffect + effects: + - !type:FlammableReaction + multiplier: 3.75 + multiplierOnExisting: 0.75 + - !type:Ignite - type: Transform anchored: true - type: SyncSprite diff --git a/Resources/Prototypes/Entities/World/Debris/asteroids.yml b/Resources/Prototypes/Entities/World/Debris/asteroids.yml index 1541190cd21..b4ac7435c2b 100644 --- a/Resources/Prototypes/Entities/World/Debris/asteroids.yml +++ b/Resources/Prototypes/Entities/World/Debris/asteroids.yml @@ -32,6 +32,9 @@ - id: WallRockGold prob: 0.05 orGroup: rock + - id: WallRockDiamond + prob: 0.005 + orGroup: rock - id: WallRockSilver prob: 0.05 orGroup: rock diff --git a/Resources/Prototypes/Flavors/flavors.yml b/Resources/Prototypes/Flavors/flavors.yml index 26190d87912..7534a8cdc68 100644 --- a/Resources/Prototypes/Flavors/flavors.yml +++ b/Resources/Prototypes/Flavors/flavors.yml @@ -1079,6 +1079,11 @@ flavorType: Complex description: flavor-complex-fishops +#- type: flavor +# id: bluepumpkin +# flavorType: Complex +# description: flavor-complex-blue-pumpkin + - type: flavor id: violets flavorType: Complex @@ -1097,4 +1102,34 @@ - type: flavor id: paintthinner flavorType: Complex - description: flavor-complex-paint-thinner \ No newline at end of file + description: flavor-complex-paint-thinner + +- type: flavor + id: numbingtranquility + flavorType: Complex + description: flavor-complex-numbing-tranquility + +- type: flavor + id: truenature + flavorType: Complex + description: flavor-complex-true-nature + +- type: flavor + id: falsemeat + flavorType: Complex + description: flavor-complex-false-meat + +- type: flavor + id: cherry + flavorType: Complex + description: flavor-complex-cherry + +- type: flavor + id: paper + flavorType: Complex + description: flavor-complex-paper + +- type: flavor + id: compressed-meat + flavorType: Complex + description: flavor-complex-compressed-meat diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 801dcc4b859..98c29e843fc 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -1,22 +1,53 @@ - type: entity - id: AnomalySpawn + id: BaseStationEvent + parent: BaseGameRule + abstract: true + categories: [ HideSpawnMenu ] + components: + - type: GameRule + delay: + min: 10 + max: 20 + +- type: entity + id: BaseStationEventShortDelay + parent: BaseGameRule + abstract: true + categories: [ HideSpawnMenu ] + components: + - type: GameRule + delay: + min: 10 + max: 20 + +- type: entity + id: BaseStationEventLongDelay parent: BaseGameRule + abstract: true + categories: [ HideSpawnMenu ] + components: + - type: GameRule + delay: + min: 40 + max: 60 + +- type: entity + id: AnomalySpawn + parent: BaseStationEventLongDelay categories: [ HideSpawnMenu ] components: - type: StationEvent weight: 8 - startDelay: 30 duration: 35 - type: AnomalySpawnRule - type: entity id: BluespaceArtifact - parent: BaseGameRule + parent: BaseStationEventLongDelay categories: [ HideSpawnMenu ] components: - type: StationEvent weight: 8 - startDelay: 30 duration: 35 - type: BluespaceArtifactRule @@ -104,7 +135,43 @@ earliestStart: 30 reoccurrenceDelay: 20 minimumPlayers: 30 - - type: NinjaSpawnRule + - type: SpaceSpawnRule + - type: AntagLoadProfileRule + - type: AntagObjectives + objectives: + - StealResearchObjective + - DoorjackObjective + - SpiderChargeObjective + - TerrorObjective + - MassArrestObjective + - NinjaSurviveObjective + - type: AntagSelection + agentName: ninja-round-end-agent-name + definitions: + - spawnerPrototype: SpawnPointGhostSpaceNinja + min: 1 + max: 1 + pickPlayer: false + startingGear: SpaceNinjaGear + briefing: + text: ninja-role-greeting + color: Green + sound: /Audio/Misc/ninja_greeting.ogg + components: + - type: SpaceNinja + - type: NpcFactionMember + factions: + - Syndicate + - type: AutoImplant + implants: + - DeathAcidifierImplant + - type: RandomMetadata + nameSegments: + - names_ninja_title + - names_ninja + mindComponents: + - type: NinjaRole + prototype: SpaceNinja - type: entity parent: BaseGameRule @@ -212,6 +279,45 @@ - id: MobMothroach prob: 0.008 +- type: entity + id: SnailMigrationLowPop + parent: BaseGameRule + components: + - type: StationEvent + startAnnouncement: true + startDelay: 10 + weight: 6 + duration: 50 + - type: VentCrittersRule + entries: + - id: MobSnail + prob: 0.02 + - id: MobSnailSpeed + prob: 0.002 + - id: MobSnailMoth + prob: 0.002 + +- type: entity + id: SnailMigration + parent: BaseStationEventShortDelay + components: + - type: StationEvent + startAnnouncement: true + earliestStart: 15 + weight: 6 + duration: 50 + minimumPlayers: 30 + - type: VentCrittersRule + entries: + - id: MobSnail + prob: 0.02 + - id: MobSnailSpeed + prob: 0.002 + - id: MobSnailMoth + prob: 0.002 + - id: MobSnailInstantDeath + prob: 0.00001 # ~ 1:2000 snails + - type: entity id: PowerGridCheck parent: BaseGameRule @@ -352,7 +458,7 @@ blacklist: components: - ZombieImmune - - InitialInfectedExempt + - AntagImmune briefing: text: zombie-patientzero-role-greeting color: Plum @@ -368,9 +474,9 @@ prototype: InitialInfected - type: entity - id: LoneOpsSpawn - parent: BaseGameRule categories: [ HideSpawnMenu ] + parent: BaseNukeopsRule + id: LoneOpsSpawn components: - type: StationEvent earliestStart: 35 @@ -402,9 +508,9 @@ prototype: Nukeops - type: entity - id: SleeperAgentsRule - parent: BaseGameRule categories: [ HideSpawnMenu ] + parent: BaseTraitorRule + id: SleeperAgents components: - type: StationEvent earliestStart: 25 @@ -412,17 +518,20 @@ minimumPlayers: 15 reoccurrenceDelay: 30 startAnnouncement: false - - type: AlertLevelInterceptionRule - - type: TraitorRule +# - type: AlertLevelInterceptionRule - Disable setting to blue with sleeper agents. Uncomment to enable. - type: AntagSelection definitions: - - prefRoles: [ Traitor ] + - prefRoles: [ TraitorSleeper ] + fallbackRoles: [ Traitor ] min: 1 max: 2 playerRatio: 10 + blacklist: + components: + - AntagImmune mindComponents: - type: TraitorRole - prototype: Traitor + prototype: TraitorSleeper - type: entity id: MassHallucinations diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml index fca0073b4e5..50864930b0f 100644 --- a/Resources/Prototypes/GameRules/midround.yml +++ b/Resources/Prototypes/GameRules/midround.yml @@ -1,41 +1,22 @@ -# doesnt spawn a ninja or anything, just stores configuration for it -# see NinjaSpawn event for spawning -- type: entity - id: Ninja - parent: BaseGameRule - categories: [ HideSpawnMenu ] - components: - - type: GenericAntagRule - agentName: ninja-round-end-agent-name - objectives: - - StealResearchObjective - - DoorjackObjective - - SpiderChargeObjective - - TerrorObjective - - MassArrestObjective - - NinjaSurviveObjective - - type: NinjaRule - threats: NinjaThreats - -# stores configuration for dragon -- type: entity - categories: [ HideSpawnMenu ] - parent: BaseGameRule - id: Dragon - components: - - type: GenericAntagRule - agentName: dragon-round-end-agent-name - objectives: - - CarpRiftsObjective - - DragonSurviveObjective - - type: entity categories: [ HideSpawnMenu ] parent: BaseGameRule id: Thief components: - type: ThiefRule + - type: AntagObjectives + objectives: + - EscapeThiefShuttleObjective + - type: AntagRandomObjectives + sets: + - groups: ThiefBigObjectiveGroups + prob: 0.7 + maxPicks: 1 + - groups: ThiefObjectiveGroups + maxPicks: 10 + maxDifficulty: 2.5 - type: AntagSelection + agentName: thief-round-end-agent-name definitions: - prefRoles: [ Thief ] maxRange: diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 4ae53c9b37a..70ccd06f394 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -64,17 +64,25 @@ roundEndDelay: 10 - type: entity - id: Nukeops + abstract: true parent: BaseGameRule - categories: [ HideSpawnMenu ] + id: BaseNukeopsRule components: - - type: GameRule - minPlayers: 35 - type: RandomMetadata #this generates the random operation name cuz it's cool. nameSegments: - operationPrefix - operationSuffix - type: NukeopsRule + - type: AntagSelection + - type: AntagLoadProfileRule + +- type: entity + categories: [ HideSpawnMenu ] + parent: BaseNukeopsRule + id: Nukeops + components: + - type: GameRule + minPlayers: 20 - type: LoadMapRule gameMap: NukieOutpost - type: AntagSelection @@ -134,26 +142,52 @@ prototype: Nukeops - type: entity - id: Traitor + abstract: true parent: BaseGameRule + id: BaseTraitorRule + components: + - type: TraitorRule + # TODO: codewords in yml + # TODO: uplink in yml + - type: AntagRandomObjectives + sets: + - groups: TraitorObjectiveGroups + maxDifficulty: 5 + - type: AntagSelection + agentName: traitor-round-end-agent-name + +- type: entity categories: [ HideSpawnMenu ] + parent: BaseTraitorRule + id: Traitor components: - type: GameRule minPlayers: 5 delay: min: 240 max: 420 - - type: TraitorRule - type: AntagSelection definitions: - prefRoles: [ Traitor ] max: 8 playerRatio: 10 + blacklist: + components: + - AntagImmune lateJoinAdditional: true mindComponents: - type: TraitorRole prototype: Traitor +- type: entity + id: TraitorReinforcement + parent: Traitor + components: + - type: TraitorRule + giveUplink: false + giveCodewords: false # It would actually give them a different set of codewords than the regular traitors, anyway + giveBriefing: false + - type: entity id: Revolutionary parent: BaseGameRule @@ -212,7 +246,7 @@ blacklist: components: - ZombieImmune - - InitialInfectedExempt + - AntagImmune briefing: text: zombie-patientzero-role-greeting color: Plum diff --git a/Resources/Prototypes/Hydroponics/seeds.yml b/Resources/Prototypes/Hydroponics/seeds.yml index e134d24c26d..284ef764c7c 100644 --- a/Resources/Prototypes/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Hydroponics/seeds.yml @@ -7,6 +7,8 @@ packetPrototype: WheatSeeds productPrototypes: - WheatBushel + mutationPrototypes: + - meatwheat lifespan: 25 maturation: 6 production: 3 @@ -18,11 +20,37 @@ Nutriment: Min: 1 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 Flour: Min: 5 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 + +- type: seed + id: meatwheat + name: seeds-meatwheat-name + noun: seeds-noun-seeds + displayName: seeds-meatwheat-display-name + plantRsi: Objects/Specific/Hydroponics/meatwheat.rsi + packetPrototype: MeatwheatSeeds + productPrototypes: + - MeatwheatBushel + lifespan: 25 + maturation: 6 + production: 3 + yield: 3 + potency: 5 + idealLight: 8 + nutrientConsumption: 0.40 + chemicals: + Nutriment: + Min: 1 + Max: 20 + PotencyDivisor: 20 + UncookedAnimalProteins: + Min: 5 + Max: 20 + PotencyDivisor: 20 - type: seed id: oat @@ -44,11 +72,11 @@ Nutriment: Min: 1 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 Oats: Min: 5 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 - type: seed id: banana @@ -73,11 +101,11 @@ Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Nutriment: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 - type: seed id: mimana @@ -100,11 +128,11 @@ MuteToxin: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Nutriment: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 - type: seed id: carrots @@ -126,15 +154,15 @@ JuiceCarrot: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Oculine: Min: 2 Max: 6 - potencyDivisor: 20 + PotencyDivisor: 20 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: laughinPea @@ -145,6 +173,8 @@ packetPrototype: LaughinPeaSeeds productPrototypes: - FoodLaughinPeaPod + mutationPrototypes: + - worldPea lifespan: 25 growthStages: 3 maturation: 7 @@ -159,15 +189,15 @@ Nutriment: Min: 1 Max: 3 - potencyDivisor: 7 + PotencyDivisor: 7 Sugar: Min: 1 Max: 10 - potencyDivisor: 5 + PotencyDivisor: 5 Laughter: Min: 1 Max: 10 - potencyDivisor: 5 + PotencyDivisor: 5 - type: seed id: lemon @@ -180,6 +210,8 @@ - FoodLemon mutationPrototypes: - lemoon + - lime + - orange harvestRepeat: Repeat lifespan: 55 maturation: 6 @@ -191,11 +223,11 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: lemoon @@ -217,11 +249,11 @@ Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Milk: Min: 8 Max: 20 - potencyDivisor: 5 + PotencyDivisor: 5 - type: seed id: lime @@ -232,6 +264,9 @@ packetPrototype: LimeSeeds productPrototypes: - FoodLime + mutationPrototypes: + - orange + - lemon harvestRepeat: Repeat lifespan: 55 maturation: 6 @@ -243,11 +278,11 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: orange @@ -258,6 +293,10 @@ packetPrototype: OrangeSeeds productPrototypes: - FoodOrange + mutationPrototypes: + - extradimensionalOrange + - lemon + - lime harvestRepeat: Repeat lifespan: 55 maturation: 6 @@ -269,11 +308,41 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 + +- type: seed + id: extradimensionalOrange + name: seeds-extradimensionalorange-name + noun: seeds-noun-seeds + displayName: seeds-extradimensionalorange-display-name + plantRsi: Objects/Specific/Hydroponics/extradimensional_orange.rsi + packetPrototype: ExtradimensionalOrangeSeeds + productPrototypes: + - FoodExtradimensionalOrange + harvestRepeat: Repeat + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + idealLight: 8 + chemicals: + Haloperidol: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Nutriment: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Vitamin: + Min: 1 + Max: 4 + PotencyDivisor: 25 - type: seed id: pineapple @@ -296,15 +365,15 @@ Nutriment: Min: 1 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 Water: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Vitamin: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 - type: seed id: potato @@ -326,11 +395,11 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: sugarcane @@ -341,6 +410,8 @@ packetPrototype: SugarcaneSeeds productPrototypes: - Sugarcane + mutationPrototypes: + - papercane harvestRepeat: Repeat lifespan: 60 maturation: 6 @@ -353,7 +424,25 @@ Sugar: Min: 4 Max: 5 - potencyDivisor: 5 + PotencyDivisor: 5 + +- type: seed + id: papercane + name: seeds-papercane-name + noun: seeds-noun-seeds + displayName: seeds-papercane-display-name + plantRsi: Objects/Specific/Hydroponics/papercane.rsi + packetPrototype: PapercaneSeeds + productPrototypes: + - Papercane + harvestRepeat: Repeat + lifespan: 60 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + growthStages: 3 + idealHeat: 298 - type: seed id: towercap @@ -426,15 +515,15 @@ Nutriment: Min: 1 Max: 7 - potencyDivisor: 14 + PotencyDivisor: 14 Vitamin: Min: 1 Max: 3 - potencyDivisor: 33 + PotencyDivisor: 33 Water: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: blueTomato @@ -460,15 +549,15 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 SpaceLube: Min: 5 Max: 15 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: bloodTomato @@ -494,11 +583,11 @@ Blood: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: eggplant @@ -523,11 +612,11 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: cabbage @@ -548,11 +637,11 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: garlic @@ -573,15 +662,15 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Allicin: Min: 1 Max: 8 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: apple @@ -592,6 +681,34 @@ packetPrototype: AppleSeeds productPrototypes: - FoodApple + mutationPrototypes: + - goldenApple + harvestRepeat: Repeat + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + idealLight: 6 + chemicals: + Nutriment: + Min: 1 + Max: 10 + PotencyDivisor: 10 + Vitamin: + Min: 1 + Max: 4 + PotencyDivisor: 25 + +- type: seed + id: goldenApple + name: seeds-goldenapple-name + noun: seeds-noun-seeds + displayName: seeds-goldenapple-display-name + plantRsi: Objects/Specific/Hydroponics/golden_apple.rsi + packetPrototype: GoldenAppleSeeds + productPrototypes: + - FoodGoldenApple harvestRepeat: Repeat lifespan: 55 maturation: 6 @@ -599,15 +716,21 @@ yield: 3 potency: 10 idealLight: 6 + waterConsumption: 0.75 + nutrientConsumption: 0.75 chemicals: Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 + DoctorsDelight: + Min: 3 + Max: 13 + PotencyDivisor: 10 - type: seed id: corn @@ -631,11 +754,11 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 20 + PotencyDivisor: 20 Cornmeal: Min: 5 Max: 15 - potencyDivisor: 10 + PotencyDivisor: 10 - type: seed id: onion @@ -661,15 +784,15 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Allicin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: onionred @@ -693,15 +816,15 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Allicin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: chanterelle @@ -726,7 +849,7 @@ Nutriment: Min: 1 Max: 25 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: eggy @@ -750,7 +873,7 @@ Egg: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 - type: seed id: cannabis @@ -777,7 +900,7 @@ THC: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 - type: seed id: rainbowCannabis @@ -802,27 +925,27 @@ SpaceDrugs: Min: 1 Max: 15 - potencyDivisor: 10 + PotencyDivisor: 10 Lipolicide: Min: 1 Max: 15 - potencyDivisor: 10 + PotencyDivisor: 10 MindbreakerToxin: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Happiness: Min: 1 Max: 5 -# potencyDivisor: 20 +# PotencyDivisor: 20 # ColorfulReagent: # Min: 0 # Max: 5 -# potencyDivisor: 20 +# PotencyDivisor: 20 Psicodine: Min: 0 Max: 5 - potencyDivisor: 33 + PotencyDivisor: 33 - type: seed id: tobacco @@ -847,7 +970,7 @@ Nicotine: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 - type: seed id: nettle @@ -873,7 +996,7 @@ Histamine: Min: 1 Max: 25 - potencyDivisor: 4 + PotencyDivisor: 4 - type: seed id: deathNettle @@ -898,11 +1021,11 @@ SulfuricAcid: Min: 1 Max: 15 - potencyDivisor: 6 + PotencyDivisor: 6 FluorosulfuricAcid: Min: 1 Max: 15 - potencyDivisor: 6 + PotencyDivisor: 6 - type: seed id: chili @@ -927,15 +1050,15 @@ CapsaicinOil: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Nutriment: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: chilly @@ -958,15 +1081,15 @@ Frostoil: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Nutriment: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: poppy @@ -990,11 +1113,11 @@ Nutriment: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 Bicaridine: Min: 1 Max: 20 - potencyDivisor: 5 + PotencyDivisor: 5 - type: seed id: aloe @@ -1016,11 +1139,11 @@ Aloe: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Dermaline: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 - type: seed id: lily @@ -1044,11 +1167,11 @@ Nutriment: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 Bicaridine: Min: 1 Max: 20 - potencyDivisor: 5 + PotencyDivisor: 5 - type: seed id: lingzhi @@ -1070,11 +1193,11 @@ Ultravasculine: Min: 1 Max: 20 - potencyDivisor: 5 + PotencyDivisor: 5 Epinephrine: Min: 1 Max: 20 - potencyDivisor: 5 + PotencyDivisor: 5 - type: seed id: ambrosiaVulgaris @@ -1098,23 +1221,23 @@ Nutriment: Min: 1 Max: 2 - potencyDivisor: 10 + PotencyDivisor: 10 Bicaridine: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Kelotane: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Desoxyephedrine: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 - type: seed id: ambrosiaDeus @@ -1136,19 +1259,19 @@ Nutriment: Min: 1 Max: 2 - potencyDivisor: 10 + PotencyDivisor: 10 Omnizine: # Don't kill me Min: 1 Max: 3 - potencyDivisor: 35 + PotencyDivisor: 35 SpaceDrugs: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Desoxyephedrine: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 - type: seed id: galaxythistle @@ -1159,6 +1282,8 @@ packetPrototype: GalaxythistleSeeds productPrototypes: - FoodGalaxythistle + mutationPrototypes: + - glasstle lifespan: 25 maturation: 10 production: 3 @@ -1170,7 +1295,29 @@ Stellibinin: Min: 1 Max: 25 - potencyDivisor: 4 + PotencyDivisor: 4 + +- type: seed + id: glasstle + name: seeds-glasstle-name + noun: seeds-noun-seeds + displayName: seeds-glasstle-display-name + plantRsi: Objects/Specific/Hydroponics/glasstle.rsi + packetPrototype: GlasstleSeeds + productPrototypes: + - FoodGlasstle + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 + waterConsumption: 0.5 + chemicals: + Razorium: + Min: 1 + Max: 25 + PotencyDivisor: 4 - type: seed id: flyAmanita @@ -1193,11 +1340,11 @@ Amatoxin: Min: 1 Max: 25 - potencyDivisor: 4 + PotencyDivisor: 4 Nutriment: ## yumby :) Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 - type: seed id: gatfruit @@ -1208,6 +1355,35 @@ packetPrototype: GatfruitSeeds productPrototypes: - FoodGatfruit + mutationPrototypes: + - fakeCapfruit + - realCapfruit + lifespan: 65 + maturation: 25 + production: 25 + yield: 1 + potency: 10 + growthStages: 2 + idealLight: 6 + chemicals: + Nutriment: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Sulfur: + Min: 1 + Max: 5 + PotencyDivisor: 20 + +- type: seed + id: fakeCapfruit + name: seeds-capfruit-name + noun: seeds-noun-seeds + displayName: seeds-capfruit-display-name + plantRsi: Objects/Specific/Hydroponics/capfruit.rsi + packetPrototype: FakeCapfruitSeeds + productPrototypes: + - FoodFakeCapfruit lifespan: 65 maturation: 25 production: 25 @@ -1219,11 +1395,37 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Sulfur: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 + +- type: seed + id: realCapfruit + name: seeds-capfruit-name + noun: seeds-noun-seeds + displayName: seeds-capfruit-display-name + plantRsi: Objects/Specific/Hydroponics/capfruit.rsi + packetPrototype: RealCapfruitSeeds + productPrototypes: + - FoodRealCapfruit + lifespan: 65 + maturation: 25 + production: 25 + yield: 1 + potency: 10 + growthStages: 2 + idealLight: 6 + chemicals: + Nutriment: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Sulfur: + Min: 1 + Max: 5 + PotencyDivisor: 20 - type: seed id: rice @@ -1247,11 +1449,11 @@ Nutriment: Min: 1 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 Rice: Min: 5 Max: 20 - potencyDivisor: 20 + PotencyDivisor: 20 - type: seed id: soybeans @@ -1276,7 +1478,7 @@ Nutriment: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 - type: seed id: spacemansTrumpet @@ -1298,11 +1500,11 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 50 + PotencyDivisor: 50 PolypyryliumOligomers: Min: 1 Max: 15 - potencyDivisor: 5 + PotencyDivisor: 5 - type: seed id: koibean @@ -1325,11 +1527,11 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 CarpoToxin: Min: 1 Max: 4 - potencyDivisor: 30 + PotencyDivisor: 30 - type: seed id: grape @@ -1350,11 +1552,11 @@ Nutriment: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 - type: seed id: watermelon @@ -1365,6 +1567,8 @@ packetPrototype: WatermelonSeeds productPrototypes: - FoodWatermelon + mutationPrototypes: + - holymelon lifespan: 55 maturation: 12 production: 3 @@ -1375,15 +1579,44 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Water: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 Vitamin: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 + +- type: seed + id: holymelon + name: seeds-holymelon-name + noun: seeds-noun-seeds + displayName: seeds-holymelon-display-name + plantRsi: Objects/Specific/Hydroponics/holymelon.rsi + packetPrototype: HolymelonSeeds + productPrototypes: + - FoodHolymelon + lifespan: 55 + maturation: 12 + production: 3 + yield: 1 + potency: 1 + idealLight: 8 + chemicals: + Nutriment: + Min: 1 + Max: 10 + PotencyDivisor: 10 + HolyWater: + Min: 1 + Max: 10 + PotencyDivisor: 10 + Vitamin: + Min: 1 + Max: 5 + PotencyDivisor: 20 - type: seed id: cocoa @@ -1407,11 +1640,11 @@ Vitamin: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 Nutriment: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 - type: seed id: berries @@ -1433,11 +1666,11 @@ Nutriment: Min: 2 Max: 5 - potencyDivisor: 30 + PotencyDivisor: 30 Vitamin: Min: 1 Max: 4 - potencyDivisor: 40 + PotencyDivisor: 40 - type: seed id: bungo @@ -1462,11 +1695,11 @@ Nutriment: Min: 5 Max: 10 - potencyDivisor: 20 + PotencyDivisor: 20 Enzyme: Min: 5 Max: 10 - potencyDivisor: 20 + PotencyDivisor: 20 - type: seed id: pea @@ -1493,11 +1726,44 @@ Nutriment: Min: 1 Max: 3 - potencyDivisor: 33 + PotencyDivisor: 33 Vitamin: Min: 1 Max: 2 - potencyDivisor: 50 + PotencyDivisor: 50 + +- type: seed + id: worldPea + name: seeds-worldpea-name + noun: seeds-noun-seeds + displayName: seeds-worldpea-display-name + plantRsi: Objects/Specific/Hydroponics/world_pea.rsi + packetPrototype: PeaSeeds + productPrototypes: + - FoodWorldPeas + lifespan: 25 + growthStages: 3 + maturation: 20 + production: 6 + yield: 3 + potency: 25 + idealLight: 8 + harvestRepeat: Repeat + nutrientConsumption: 0.5 + waterConsumption: 0.5 + chemicals: + Happiness: + Min: 1 + Max: 3 + PotencyDivisor: 25 + Nutriment: + Min: 1 + Max: 3 + PotencyDivisor: 20 + Pax: + Min: 1 + Max: 2 + PotencyDivisor: 50 - type: seed id: pumpkin @@ -1519,11 +1785,11 @@ PumpkinFlesh: Min: 1 Max: 20 - potencyDivisor: 5 + PotencyDivisor: 5 Vitamin: Min: 1 Max: 5 - potencyDivisor: 20 + PotencyDivisor: 20 - type: seed id: cotton @@ -1548,7 +1814,7 @@ Fiber: Min: 5 Max: 10 - potencyDivisor: 20 + PotencyDivisor: 20 - type: seed id: pyrotton @@ -1571,8 +1837,34 @@ Fiber: Min: 5 Max: 10 - potencyDivisor: 20 + PotencyDivisor: 20 Phlogiston: Min: 4 Max: 8 - potencyDivisor: 30 + PotencyDivisor: 30 + +- type: seed + id: cherry + name: seeds-cherry-name + noun: seeds-noun-seeds + displayName: seeds-cherry-display-name + plantRsi: Objects/Specific/Hydroponics/cherry.rsi + packetPrototype: CherrySeeds + productPrototypes: + - FoodCherry + harvestRepeat: Repeat + lifespan: 55 + maturation: 6 + production: 6 + yield: 5 + potency: 10 + idealLight: 6 + chemicals: + Nutriment: + Min: 1 + Max: 3 + PotencyDivisor: 30 + Vitamin: + Min: 1 + Max: 3 + PotencyDivisor: 40 diff --git a/Resources/Prototypes/Loadouts/Categories/categories.yml b/Resources/Prototypes/Loadouts/Categories/categories.yml index 48de355a517..716b2ebeb62 100644 --- a/Resources/Prototypes/Loadouts/Categories/categories.yml +++ b/Resources/Prototypes/Loadouts/Categories/categories.yml @@ -51,6 +51,7 @@ - JobsCommandAUncategorized - JobsCommandCaptain - JobsCommandHeadOfPersonnel + - JobsCommandAdminAssistant - type: loadoutCategory id: JobsCommandAUncategorized @@ -61,6 +62,9 @@ - type: loadoutCategory id: JobsCommandHeadOfPersonnel +- type: loadoutCategory + id: JobsCommandAdminAssistant + # Engineering - type: loadoutCategory id: JobsEngineering diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/adminassistant.yml b/Resources/Prototypes/Loadouts/Jobs/Command/adminassistant.yml new file mode 100644 index 00000000000..9e2c638b017 --- /dev/null +++ b/Resources/Prototypes/Loadouts/Jobs/Command/adminassistant.yml @@ -0,0 +1,80 @@ +# Uncategorized +# Backpacks + +# Belt + +# Ears + +# Equipment + +# Eyes + +# Gloves +- type: loadout + id: LoadoutAdministrativeAssistantGlovesHoP + category: JobsCommandAdminAssistant + cost: 0 + exclusive: true + requirements: + - !type:CharacterItemGroupRequirement + group: LoadoutAdminAssistantGloves + - !type:CharacterJobRequirement + jobs: + - AdministrativeAssistant + items: + - ClothingHandsGlovesHop + +- type: loadout + id: LoadoutAdministrativeAssistantGlovesInspection + category: JobsCommandAdminAssistant + cost: 0 + exclusive: true + requirements: + - !type:CharacterItemGroupRequirement + group: LoadoutAdminAssistantGloves + - !type:CharacterJobRequirement + jobs: + - AdministrativeAssistant + items: + - ClothingHandsGlovesInspection + +# Head + +# Id + +# Neck + +# Mask + +# Outer + +# Shoes + +# Uniforms +- type: loadout + id: LoadoutAdminAssistantUniformJumpsuit + category: JobsCommandAdminAssistant + cost: 0 + exclusive: true + requirements: + - !type:CharacterItemGroupRequirement + group: LoadoutAdminAssistantUniforms + - !type:CharacterJobRequirement + jobs: + - AdministrativeAssistant + items: + - ClothingUniformJumpsuitAdminAssistant + +- type: loadout + id: LoadoutAdminAssistantUniformJumpskirt + category: JobsCommandAdminAssistant + cost: 0 + exclusive: true + requirements: + - !type:CharacterItemGroupRequirement + group: LoadoutAdminAssistantUniforms + - !type:CharacterJobRequirement + jobs: + - AdministrativeAssistant + items: + - ClothingUniformJumpskirtAdminAssistant diff --git a/Resources/Prototypes/NPCs/goliath.yml b/Resources/Prototypes/NPCs/goliath.yml new file mode 100644 index 00000000000..0befc5d581f --- /dev/null +++ b/Resources/Prototypes/NPCs/goliath.yml @@ -0,0 +1,75 @@ +- type: htnCompound + id: GoliathCompound + branches: + - tasks: + - !type:HTNCompoundTask + task: GoliathMeleeCombatPrecondition + - tasks: + - !type:HTNCompoundTask + task: IdleCompound + +- type: htnCompound + id: GoliathMeleeCombatPrecondition + branches: + - preconditions: + - !type:BuckledPrecondition + isBuckled: true + tasks: + - !type:HTNPrimitiveTask + operator: !type:UnbuckleOperator + shutdownState: TaskFinished + + - preconditions: + - !type:InContainerPrecondition + isInContainer: true + tasks: + - !type:HTNCompoundTask + task: EscapeCompound + + - preconditions: + - !type:PulledPrecondition + isPulled: true + tasks: + - !type:HTNPrimitiveTask + operator: !type:UnPullOperator + shutdownState: TaskFinished + + - tasks: + - !type:HTNPrimitiveTask + operator: !type:UtilityOperator + proto: NearbyMeleeTargets + - !type:HTNCompoundTask + task: GoliathAttackTargetCompound + +- type: htnCompound + id: GoliathAttackTargetCompound + branches: + - preconditions: + - !type:KeyExistsPrecondition + key: Target + tasks: + - !type:HTNPrimitiveTask + operator: !type:MoveToOperator + shutdownState: PlanFinished + pathfindInPlanning: true + removeKeyOnFinish: false + targetKey: TargetCoordinates + pathfindKey: TargetPathfind + rangeKey: MeleeRange + - !type:HTNPrimitiveTask + operator: !type:JukeOperator + jukeType: AdjacentTile + - !type:HTNPrimitiveTask + operator: !type:MeleeOperator + targetKey: Target + preconditions: + - !type:KeyExistsPrecondition + key: Target + - !type:TargetInRangePrecondition + targetKey: Target + rangeKey: MeleeRange + services: + - !type:UtilityService + id: MeleeService + proto: NearbyMeleeTargets + key: Target diff --git a/Resources/Prototypes/Nyanotrasen/Catalog/uplink_catalog.yml b/Resources/Prototypes/Nyanotrasen/Catalog/uplink_catalog.yml index 043e16054ff..77446652bbd 100644 --- a/Resources/Prototypes/Nyanotrasen/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Nyanotrasen/Catalog/uplink_catalog.yml @@ -6,7 +6,7 @@ cost: Telecrystal: 10 categories: - - UplinkWeapons + - UplinkWeaponry conditions: - !type:BuyerSpeciesCondition whitelist: @@ -34,7 +34,7 @@ cost: Telecrystal: 6 categories: - - UplinkArmor + - UplinkWearables conditions: - !type:BuyerSpeciesCondition whitelist: diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/Random/books.yml b/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/Random/books.yml index 2f790f2ce1c..6ee19cce6b3 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/Random/books.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/Random/books.yml @@ -50,7 +50,7 @@ - BookSpaceEncyclopedia - BookTheBookOfControl - BookBartendersManual - - BookChefGaming + - BookHowToCookForFortySpaceman - BookLeafLoversSecret - BookEngineersHandbook - BookScientistsGuidebook diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml index d0207e362e3..4ddfc9bcd4d 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml @@ -26,6 +26,7 @@ components: # - type: GhostRoleMobSpawner # prototype: MobHumanFugitive # Todo + - type: GhostRoleAntagSpawner - type: GhostRole name: Fugitive description: You're an escaped prisoner. Make it out alive. diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/mutants.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/mutants.yml index b3de9e01910..fa4452f911e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/mutants.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/mutants.yml @@ -195,148 +195,4 @@ # name: ghost-role-information-giant-spider-vampire-name # description: ghost-role-information-giant-spider-vampire-description # rules: No antagonist restrictions. Just don't talk in emote; you have telepathic chat. -# - type: GhostTakeoverAvailable - -- type: entity - parent: SimpleMobBase - id: MobMouseCancer - name: Cancer Mouse - description: Oh hey Civvie... - components: - - type: Sprite - drawdepth: SmallMobs - sprite: Mobs/Animals/mouse.rsi - layers: - - map: ["enum.DamageStateVisualLayers.Base"] - state: mouse-0 - color: greenyellow - - type: DamageStateVisuals - rotate: true - states: - Alive: - Base: mouse-0 - Critical: - Base: dead-0 - Dead: - Base: splat-0 - - type: PointLight - color: greenyellow - - type: RadiationSource - intensity: 5 - slope: 0.5 - - type: Damageable - damageContainer: Biological - damageModifierSet: CancerMouse - - type: GhostRole - makeSentient: true - name: ghost-role-information-cancer-mouse-name - description: ghost-role-information-cancer-mouse-description - - type: GhostTakeoverAvailable - - type: CombatMode - - type: MovementSpeedModifier - baseWalkSpeed : 3.5 - baseSprintSpeed : 3.5 - - type: InputMover - - type: MobMover - - type: Reactive - groups: - Flammable: [Touch] - Extinguish: [Touch] - - type: Physics - bodyType: KinematicController - - type: Fixtures - fixtures: - fix1: - shape: - !type:PhysShapeCircle - radius: 0.2 - density: 30 #Bulky by mouse standards... - mask: - - SmallMobMask - layer: - - SmallMobLayer - - type: MobState - - type: MobThresholds - thresholds: - 0: Alive - 40: Critical - 80: Dead - - type: Item - size: Normal - - type: Stamina - critThreshold: 80 - - type: MeleeWeapon - hidden: true - soundHit: - path: /Audio/Weapons/bladeslice.ogg - angle: 0 - animation: WeaponArcClaw - damage: - types: - Slash: 5 - Piercing: 3 - - type: Body - prototype: Rat - requiredLegs: 1 # TODO: More than 1 leg - - type: Hunger # probably should be prototyped - thresholds: - Overfed: 200 - Okay: 150 - Peckish: 100 - Starving: 50 - Dead: 0 - baseDecayRate: 0.01666666666 - - type: Thirst - thresholds: - OverHydrated: 600 - Okay: 450 - Thirsty: 300 - Parched: 150 - Dead: 0 - baseDecayRate: 0.1 - - type: Appearance - - type: Puller - needsHands: false - - type: Vocal - sounds: - Male: Mouse - Female: Mouse - Unsexed: Mouse - wilhelmProbability: 0.001 - - type: Tag - tags: - - CannotSuicide - - DoorBumpOpener - - FootstepSound - - Recyclable - - type: NoSlip - - type: MobPrice - price: 100 # rat wealth - - type: FelinidFood - - type: CanEscapeInventory - - type: Extractable - grindableSolutionName: food - - type: Bloodstream - bloodReagent: Radium - bloodMaxVolume: 70 - - type: SolutionContainerManager - solutions: - food: - reagents: - - ReagentId: Nutriment - Quantity: 2 - - ReagentId: Radium - Quantity: 70 - - type: Butcherable - spawned: - - id: FoodMeatRat - amount: 1 - - type: Grammar - attributes: - proper: true - gender: male - - type: IntrinsicRadioReceiver - - type: IntrinsicRadioTransmitter - - type: ActiveRadio - channels: - - Common +# - type: GhostTakeoverAvailable \ No newline at end of file diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/Baked/pizza.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/Baked/pizza.yml index 8c96635b06b..010a98e157e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/Baked/pizza.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/Baked/pizza.yml @@ -223,6 +223,7 @@ tags: - Pizza - Fruit + - Slice # Tastes like crust, pesto, cheese - type: entity @@ -286,4 +287,5 @@ tags: - Pizza - ClothMade + - Slice # Tastes like crust, cotton, cheese diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/meals.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/meals.yml index 624689a7ed2..9a36d379716 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/meals.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/meals.yml @@ -73,7 +73,8 @@ count: 6 slice: FoodMothGreenLasagneSlice - type: Food - trash: FoodPlate + trash: + - FoodPlate - type: Sprite sprite: Nyanotrasen/Objects/Consumable/Food/meal.rsi state: greenlasagne diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/snacks.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/snacks.yml index f49d6cb2cec..773f848a42e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/snacks.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/snacks.yml @@ -12,7 +12,8 @@ - tofu - leafy - type: Food - trash: FoodPlateSmall + trash: + - FoodPlateSmall - type: Sprite sprite: Nyanotrasen/Objects/Consumable/Food/Snacks/moth.rsi state: squeakingstirfry @@ -214,7 +215,8 @@ sprite: Nyanotrasen/Objects/Consumable/Food/Snacks/boritopie.rsi state: borito-pie - type: Food - trash: FoodPacketBoritosTrash + trash: + - FoodPacketBoritosTrash - type: SolutionContainerManager solutions: food: diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml index c85255f814e..f2a16126b96 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml @@ -39,6 +39,7 @@ - NewsReaderCartridge - CrimeAssistCartridge - SecWatchCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -73,6 +74,7 @@ - NotekeeperCartridge - NewsReaderCartridge - MailMetricsCartridge + - NanoChatCartridge - type: entity parent: BasePDA @@ -119,3 +121,4 @@ - NotekeeperCartridge - NewsReaderCartridge - GlimmerMonitorCartridge + - NanoChatCartridge diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml index dc0629a745c..efc0d2c9a1a 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml @@ -9,7 +9,6 @@ map: [ "enum.AmmoVisualLayers.Base" ] - type: CartridgeAmmo proto: PelletShotgunSoulbreaker - count: 1 - type: ChemicalAmmo - type: SolutionContainerManager solutions: diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Melee/breaching_hammer.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Melee/breaching_hammer.yml index 2c6bba1cbef..5d4b98b5fab 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Melee/breaching_hammer.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Melee/breaching_hammer.yml @@ -35,7 +35,7 @@ - type: Tool qualities: - Prying - speed: 0.8 + speedModifier: 0.8 - type: Prying pryPowered: !type:Bool true diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml index 6430e94105a..bf3450f70c0 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml @@ -46,7 +46,7 @@ acts: ["Destruction"] - type: ApcPowerReceiver powerLoad: 1000 # TODO if we keep this make it spike power draw when teleporting - powerDisabled: true + powerDisabled: false - type: ExtensionCableReceiver - type: Appearance - type: GenericVisualizer diff --git a/Resources/Prototypes/Nyanotrasen/Hydroponics/seeds.yml b/Resources/Prototypes/Nyanotrasen/Hydroponics/seeds.yml index 08f4482787f..12744b1e06a 100644 --- a/Resources/Prototypes/Nyanotrasen/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Nyanotrasen/Hydroponics/seeds.yml @@ -22,8 +22,8 @@ Nutriment: Min: 1 Max: 10 - potencyDivisor: 10 + PotencyDivisor: 10 DemonsBlood: Min: 1 Max: 4 - potencyDivisor: 25 + PotencyDivisor: 25 diff --git a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml index 6c8e1d77151..f7a31205706 100644 --- a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml +++ b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml @@ -18,15 +18,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye startingGear: ForensicMantisGear icon: "JobIconForensicMantis" supervisors: job-supervisors-rd diff --git a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Wildcards/prisoner.yml b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Wildcards/prisoner.yml index 638aaecd2cf..4f5551a2b3e 100644 --- a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Wildcards/prisoner.yml +++ b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Wildcards/prisoner.yml @@ -13,15 +13,6 @@ - !type:DepartmentTimeRequirement department: Security min: 21600 - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - inverted: true - species: - - Shadowkin - - !type:CharacterTraitRequirement - traits: - - ShadowkinBlackeye special: - !type:AddComponentSpecial components: diff --git a/Resources/Prototypes/Nyanotrasen/StatusEffects/job.yml b/Resources/Prototypes/Nyanotrasen/StatusEffects/job.yml index 943660199ba..6a48a061f23 100644 --- a/Resources/Prototypes/Nyanotrasen/StatusEffects/job.yml +++ b/Resources/Prototypes/Nyanotrasen/StatusEffects/job.yml @@ -1,32 +1,32 @@ -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconGladiator icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: nyanoGladiator -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconPrisonGuard icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: nyanoPrisonGuard -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMailCarrier icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: nyanoMailCarrier -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMartialArtist icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi state: nyanoMartialArtist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconForensicMantis icon: diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index fb4ce6f4a16..16dc7c6d310 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -22,11 +22,11 @@ CaptainGunStealObjective: 0.5 CaptainJetpackStealObjective: 0.5 HandTeleporterStealObjective: 0.5 - SecretDocumentsStealObjective: 0.5 LOLuckyBillStealObjective: 0.5 # DeltaV - LO steal objective, see Resources/Prototypes/DeltaV/Objectives/traitor.yml HoPBookIanDossierStealObjective: 1 # DeltaV - HoP steal objective, see Resources/Prototypes/DeltaV/Objectives/traitor.yml HoSGunStealObjective: 0.5 StealSupermatterSliverObjective: 0.5 + EnergyShotgunStealObjective: 0.5 - type: weightedRandom id: TraitorObjectiveGroupKill @@ -61,11 +61,6 @@ ThiefObjectiveGroupStructure: 0 #Temporarily disabled until obvious ways to steal structures are added ThiefObjectiveGroupAnimal: 2 -- type: weightedRandom - id: ThiefEscapeObjectiveGroups - weights: - ThiefObjectiveGroupEscape: 1 - - type: weightedRandom id: ThiefObjectiveGroupCollection weights: diff --git a/Resources/Prototypes/Objectives/stealTargetGroups.yml b/Resources/Prototypes/Objectives/stealTargetGroups.yml index 03d0e84c717..9ba05f14c32 100644 --- a/Resources/Prototypes/Objectives/stealTargetGroups.yml +++ b/Resources/Prototypes/Objectives/stealTargetGroups.yml @@ -35,13 +35,6 @@ sprite: Objects/Devices/hand_teleporter.rsi state: icon -- type: stealTargetGroup - id: BookSecretDocuments - name: "emergency security orders" - sprite: - sprite: Objects/Misc/bureaucracy.rsi - state: folder-sec-doc - - type: stealTargetGroup id: ClothingShoesBootsMagAdv name: advanced magboots @@ -408,3 +401,5 @@ sprite: sprite: Mobs/Animals/crab.rsi state: crab + + diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml index f9dfe5c3109..eecff55cd63 100644 --- a/Resources/Prototypes/Objectives/traitor.yml +++ b/Resources/Prototypes/Objectives/traitor.yml @@ -222,23 +222,6 @@ stealGroup: HandTeleporter verifyMapExistence: true -## hos - -- type: entity - categories: [HideSpawnMenu] - parent: BaseTraitorStealObjective - id: SecretDocumentsStealObjective - components: - - type: Objective - # hos has a gun ce does not, higher difficulty than most - difficulty: 3 - - type: NotJobRequirement - job: HeadOfSecurity - - type: StealCondition - stealGroup: BookSecretDocuments - verifyMapExistence: true - owner: job-name-hos - ## ce - type: entity diff --git a/Resources/Prototypes/Procedural/Magnet/asteroid.yml b/Resources/Prototypes/Procedural/Magnet/asteroid.yml index 8fcd265297d..6fc762a67dc 100644 --- a/Resources/Prototypes/Procedural/Magnet/asteroid.yml +++ b/Resources/Prototypes/Procedural/Magnet/asteroid.yml @@ -6,6 +6,7 @@ OreCoal: 1.0 OreSalt: 1.0 OreGold: 0.25 + OreDiamond: 0.05 OreSilver: 0.25 OrePlasma: 0.15 OreUranium: 0.15 diff --git a/Resources/Prototypes/Procedural/biome_ore_templates.yml b/Resources/Prototypes/Procedural/biome_ore_templates.yml index c0001367d5c..158f58a2670 100644 --- a/Resources/Prototypes/Procedural/biome_ore_templates.yml +++ b/Resources/Prototypes/Procedural/biome_ore_templates.yml @@ -130,6 +130,20 @@ maxGroupSize: 10 radius: 4 +- type: biomeMarkerLayer + id: OreDiamond + entityMask: + AsteroidRock: AsteroidRockDiamond + WallRock: WallRockDiamond + WallRockBasalt: WallRockBasaltDiamond + WallRockChromite: WallRockChromiteDiamond + WallRockSand: WallRockSandDiamond + WallRockSnow: WallRockSnowDiamond + maxCount: 6 + minGroupSize: 1 + maxGroupSize: 2 + radius: 4 + # Artifact Fragment - type: biomeMarkerLayer id: OreArtifactFragment diff --git a/Resources/Prototypes/Procedural/salvage_loot.yml b/Resources/Prototypes/Procedural/salvage_loot.yml index b256c73dfd9..7e7ddf6ff9f 100644 --- a/Resources/Prototypes/Procedural/salvage_loot.yml +++ b/Resources/Prototypes/Procedural/salvage_loot.yml @@ -179,6 +179,13 @@ - !type:BiomeMarkerLoot proto: OreBananium +- type: salvageLoot + id: OreDiamond + guaranteed: true + loots: + - !type:BiomeMarkerLoot + proto: OreDiamond + - type: salvageLoot id: OreArtifactFragment guaranteed: true diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml index f374a3debf1..2bafbbc0f0b 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml @@ -2086,6 +2086,10 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove fizziness: 0.25 - type: reagent @@ -2113,6 +2117,10 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove fizziness: 0.15 - type: reagent @@ -2140,6 +2148,10 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove fizziness: 0.15 - type: reagent @@ -2167,6 +2179,10 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove fizziness: 0.25 - type: reagent @@ -2194,6 +2210,10 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove fizziness: 0.15 - type: reagent @@ -2221,4 +2241,8 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove fizziness: 0.25 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 52a01d973f6..3f9fb7b53d2 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -12,6 +12,10 @@ effects: - !type:SatiateThirst factor: 2 + - !type:GenericStatusEffect + key: Drowsiness + time: 2.0 + type: Remove - !type:AdjustReagent reagent: Theobromine amount: 0.05 @@ -96,6 +100,15 @@ metamorphicMaxFillLevels: 1 metamorphicFillBaseName: fill- metamorphicChangeColor: false + metabolisms: + Drink: + effects: + - !type:SatiateThirst + factor: 2 + - !type:GenericStatusEffect + key: Drowsiness + time: 2.0 + type: Remove - type: reagent id: GreenTea @@ -145,6 +158,15 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: false + metabolisms: + Drink: + effects: + - !type:SatiateThirst + factor: 2 + - !type:GenericStatusEffect + key: Drowsiness + time: 2.0 + type: Remove - type: reagent id: IcedGreenTea @@ -322,6 +344,10 @@ effects: - !type:SatiateThirst factor: 6 + - !type:GenericStatusEffect + key: Drowsiness + time: 3.0 + type: Remove Poison: effects: - !type:HealthChange @@ -354,6 +380,15 @@ metamorphicMaxFillLevels: 1 metamorphicFillBaseName: fill- metamorphicChangeColor: false + metabolisms: + Drink: + effects: + - !type:SatiateThirst + factor: 2 + - !type:GenericStatusEffect + key: Drowsiness + time: 2.0 + type: Remove - type: reagent id: Tea diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml b/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml index c42791fa8fe..59d6063e78e 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml @@ -143,3 +143,12 @@ metamorphicMaxFillLevels: 4 metamorphicFillBaseName: fill- metamorphicChangeColor: false + +- type: reagent + id: JuiceCherry + name: reagent-name-juice-cherry + parent: BaseJuice + desc: reagent-desc-juice-cherry + physicalDesc: reagent-physical-desc-sweet + flavor: cherry + color: "#84031a" diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml index d78b0351cee..49290677a1e 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml @@ -13,6 +13,15 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: false + metabolisms: + Drink: + effects: + - !type:SatiateThirst + factor: 2 + - !type:GenericStatusEffect + key: Drowsiness + time: 1.0 + type: Remove - type: reagent id: RoyRogers @@ -68,6 +77,10 @@ effects: - !type:SatiateThirst factor: 2 + - !type:GenericStatusEffect + key: Drowsiness + time: 2.0 + type: Remove - !type:AdjustReagent reagent: Theobromine amount: 0.1 diff --git a/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml b/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml index 84d00fa0c14..b74fd7eb232 100644 --- a/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml @@ -258,4 +258,4 @@ effects: - !type:AdjustReagent reagent: Nutriment - amount: 0.75 + amount: 0.75 \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/Materials/materials.yml b/Resources/Prototypes/Reagents/Materials/materials.yml index f56a712cd8e..9fe532233cc 100644 --- a/Resources/Prototypes/Reagents/Materials/materials.yml +++ b/Resources/Prototypes/Reagents/Materials/materials.yml @@ -134,3 +134,19 @@ icon: { sprite: Objects/Materials/materials.rsi, state: normality } color: "#53a9ff" price: 7.5 + +- type: material + id: Gunpowder + name: materials-gunpowder + unit: materials-unit-piece + icon: { sprite: Objects/Misc/reagent_fillings.rsi, state: powderpile } + color: "#A9A9A9" + price: 0 + +- type: material + id: Diamond + name: materials-diamond + unit: materials-unit-piece + icon: { sprite: Objects/Materials/materials.rsi, state: diamond } + color: "#80ffff" + price: 20 # big diamond gaslit us so hard diamonds actually became extremely rare diff --git a/Resources/Prototypes/Reagents/Materials/ores.yml b/Resources/Prototypes/Reagents/Materials/ores.yml index 7b887b2f43c..44f13faac1d 100644 --- a/Resources/Prototypes/Reagents/Materials/ores.yml +++ b/Resources/Prototypes/Reagents/Materials/ores.yml @@ -24,6 +24,15 @@ color: "#FFD700" price: 0.2 +- type: material + id: RawDiamond + stackEntity: DiamondOre1 + name: materials-raw-diamond + unit: materials-unit-piece + icon: { sprite: Objects/Materials/ore.rsi, state: diamond } + color: "#C9D8F2" + price: 0.5 + - type: material id: RawSilver stackEntity: SilverOre1 diff --git a/Resources/Prototypes/Reagents/botany.yml b/Resources/Prototypes/Reagents/botany.yml index cdd19dc308d..99e9182103d 100644 --- a/Resources/Prototypes/Reagents/botany.yml +++ b/Resources/Prototypes/Reagents/botany.yml @@ -31,7 +31,7 @@ - !type:PlantAdjustHealth amount: -0.5 - !type:PlantAdjustMutationMod - prob: 0.3 + probability: 0.3 amount: 0.4 metabolisms: Medicine: @@ -115,10 +115,10 @@ - !type:PlantAdjustNutrition amount: 0.05 - !type:PlantAdjustWeeds - prob: 0.025 + probability: 0.025 amount: 1 - !type:PlantAdjustPests - prob: 0.025 + probability: 0.025 amount: 1 - !type:RobustHarvest {} metabolisms: @@ -273,12 +273,12 @@ - !type:PlantAdjustNutrition amount: 0.1 - !type:PlantAdjustPests - prob: 0.1 + probability: 0.1 amount: -1 - !type:PlantAdjustHealth amount: 0.1 - !type:PlantAffectGrowth - prob: 0.2 + probability: 0.2 amount: 1 - !type:PlantDiethylamine {} metabolisms: diff --git a/Resources/Prototypes/Reagents/elements.yml b/Resources/Prototypes/Reagents/elements.yml index e47335b1b4a..4f05568ad2b 100644 --- a/Resources/Prototypes/Reagents/elements.yml +++ b/Resources/Prototypes/Reagents/elements.yml @@ -334,7 +334,7 @@ - !type:PlantAdjustHealth amount: -1.5 - !type:PlantAdjustMutationMod - prob: 0.2 + probability: 0.2 amount: 0.1 metabolisms: Poison: diff --git a/Resources/Prototypes/Reagents/fun.yml b/Resources/Prototypes/Reagents/fun.yml index befa8d82968..1df2636c8ce 100644 --- a/Resources/Prototypes/Reagents/fun.yml +++ b/Resources/Prototypes/Reagents/fun.yml @@ -333,6 +333,7 @@ - !type:Emote emote: Weh showInChat: true + force: true probability: 0.5 - !type:Polymorph prototype: ArtifactLizard # Does the same thing as the original YML I made for this reagent. diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index 0c261b1dbf6..445a5344104 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -907,6 +907,10 @@ - !type:GenericStatusEffect key: Stutter component: StutteringAccent + - !type:GenericStatusEffect + key: Drowsiness + time: 10 + type: Remove - !type:ResetNarcolepsy conditions: - !type:ReagentThreshold @@ -928,6 +932,10 @@ metabolisms: Medicine: effects: + - !type:GenericStatusEffect + key: Drowsiness + time: 10 + type: Remove - !type:ResetNarcolepsy conditions: - !type:ReagentThreshold @@ -1265,6 +1273,85 @@ - "psicodine-effect-at-peace" probability: 0.2 +- type: reagent + id: PotassiumIodide + name: reagent-name-potassium-iodide + group: Medicine + desc: reagent-desc-potassium-iodide + physicalDesc: reagent-physical-desc-grainy + flavor: medicine + color: "#baa15d" + metabolisms: + Medicine: + effects: + - !type:GenericStatusEffect + key: RadiationProtection + component: RadiationProtection + time: 2 + type: Add + refresh: false + - !type:HealthChange + conditions: + - !type:ReagentThreshold + min: 20 + damage: + types: + Poison: 1 + +- type: reagent + id: Haloperidol + name: reagent-name-haloperidol + group: Medicine + desc: reagent-desc-haloperidol + physicalDesc: reagent-physical-desc-crystalline + flavor: medicine + color: "#27870a" + metabolisms: + Medicine: + effects: + - !type:Emote + emote: Yawn + showInChat: true + probability: 0.1 + - !type:GenericStatusEffect + key: Drowsiness + component: Drowsiness + time: 4 + type: Add + refresh: false + - !type:GenericStatusEffect + key: Jitter + time: 4.0 + type: Remove + - !type:GenericStatusEffect + key: SeeingRainbows + time: 10.0 + type: Remove + - !type:AdjustReagent + reagent: Desoxyephedrine + amount: -3.0 + - !type:AdjustReagent + reagent: Ephedrine + amount: -3.0 + - !type:AdjustReagent + reagent: Stimulants + amount: -3.0 + - !type:AdjustReagent + reagent: THC + amount: -3.0 + - !type:AdjustReagent + reagent: SpaceDrugs + amount: -3.0 + - !type:AdjustReagent + reagent: Bananadine + amount: -3.0 + - !type:AdjustReagent + reagent: SpaceGlue + amount: -3.0 + - !type:AdjustReagent + reagent: MindbreakerToxin + amount: -3.0 + - type: reagent id: HolyWater name: reagent-name-holywater diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index ebb8b0fc43b..08912357a34 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -40,6 +40,14 @@ key: KnockedDown time: 3 type: Remove + - !type:GenericStatusEffect + conditions: + - !type:ReagentThreshold + reagent: Haloperidol + max: 0.01 + key: Drowsiness + time: 10 + type: Remove Medicine: effects: - !type:ResetNarcolepsy @@ -80,6 +88,14 @@ key: KnockedDown time: 1 type: Remove + - !type:GenericStatusEffect + conditions: + - !type:ReagentThreshold + reagent: Haloperidol + max: 0.01 + key: Drowsiness + time: 10 + type: Remove - !type:PopupMessage visualType: Medium messages: ["ephedrine-effect-tight-pain", "ephedrine-effect-heart-pounds"] @@ -140,6 +156,14 @@ key: ForcedSleep time: 3 type: Remove + - !type:GenericStatusEffect + conditions: + - !type:ReagentThreshold + reagent: Haloperidol + max: 0.01 + key: Drowsiness + time: 10 + type: Remove Medicine: metabolismRate: 1.0 effects: diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml index abb33f832a4..199c8ae109a 100644 --- a/Resources/Prototypes/Reagents/toxins.yml +++ b/Resources/Prototypes/Reagents/toxins.yml @@ -56,18 +56,19 @@ metabolisms: Poison: effects: + - !type:Emote + emote: Yawn + showInChat: true + probability: 0.1 - !type:MovespeedModifier walkSpeedModifier: 0.65 sprintSpeedModifier: 0.65 - !type:GenericStatusEffect - conditions: - - !type:ReagentThreshold - reagent: ChloralHydrate - min: 10 - key: ForcedSleep - component: ForcedSleeping - refresh: false + key: Drowsiness + component: Drowsiness + time: 4 type: Add + refresh: false - !type:HealthChange conditions: - !type:ReagentThreshold @@ -571,6 +572,7 @@ - !type:Emote emote: Honk showInChat: true + force: true probability: 0.2 - !type:HealthChange conditions: diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/food/steak.yml b/Resources/Prototypes/Recipes/Construction/Graphs/food/steak.yml index bbf8f5ef54d..4c104003319 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/food/steak.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/food/steak.yml @@ -173,3 +173,125 @@ - minTemperature: 345 - node: bacon entity: FoodMeatBaconCooked + +# cutlets + +- type: constructionGraph + id: Cutlet + start: start + graph: + - node: start + edges: + - to: cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 335 + - node: cutlet + entity: FoodMeatCutletCooked + +- type: constructionGraph + id: BearCutlet + start: start + graph: + - node: start + edges: + - to: bear cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 345 + - node: bear cutlet + entity: FoodMeatBearCutletCooked + +- type: constructionGraph + id: PenguinCutlet + start: start + graph: + - node: start + edges: + - to: penguin cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 345 + - node: penguin cutlet + entity: FoodMeatPenguinCutletCooked + +- type: constructionGraph + id: ChickenCutlet + start: start + graph: + - node: start + edges: + - to: chicken cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 345 + - node: chicken cutlet + entity: FoodMeatChickenCutletCooked + +- type: constructionGraph + id: DuckCutlet + start: start + graph: + - node: start + edges: + - to: duck cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 335 + - node: duck cutlet + entity: FoodMeatDuckCutletCooked + +- type: constructionGraph + id: LizardCutlet + start: start + graph: + - node: start + edges: + - to: lizard cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 345 + - node: lizard cutlet + entity: FoodMeatLizardCutletCooked + +- type: constructionGraph + id: SpiderCutlet + start: start + graph: + - node: start + edges: + - to: spider cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 345 + - node: spider cutlet + entity: FoodMeatSpiderCutletCooked + +- type: constructionGraph + id: XenoCutlet + start: start + graph: + - node: start + edges: + - to: xeno cutlet + completed: + - !type:PlaySound + sound: /Audio/Effects/sizzle.ogg + steps: + - minTemperature: 345 + - node: xeno cutlet + entity: FoodMeatXenoCutletCooked \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/furniture/shelfs.yml b/Resources/Prototypes/Recipes/Construction/Graphs/furniture/shelfs.yml new file mode 100644 index 00000000000..61a903f7a0a --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/furniture/shelfs.yml @@ -0,0 +1,268 @@ +- type: constructionGraph + id: Shelf + start: start + graph: + - node: start + actions: + - !type:DeleteEntity {} + edges: +# Normal + - to: ShelfWood + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: WoodPlank + amount: 4 + doAfter: 2 + - to: ShelfMetal + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: Steel + amount: 5 + doAfter: 3 + - to: ShelfGlass + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: Glass + amount: 4 + doAfter: 2 +# Reinforced + - to: ShelfRWood + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: WoodPlank + amount: 8 + doAfter: 3 + - material: Cable + amount: 2 + doAfter: 1 + - to: ShelfRMetal + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: Plasteel + amount: 5 + doAfter: 3 + - material: ReinforcedGlass + amount: 5 + doAfter: 2 + - material: Cable + amount: 3 + doAfter: 1 + - to: ShelfRGlass + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: Plastic + amount: 5 + doAfter: 2 + - material: ReinforcedGlass + amount: 5 + doAfter: 3 + - material: Cable + amount: 2 + doAfter: 1 +# Departmental + - to: ShelfBar + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: WoodPlank + amount: 6 + doAfter: 2 + - to: ShelfKitchen + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: MetalRod + amount: 2 + doAfter: 1 + - material: Steel + amount: 5 + - material: WoodPlank + amount: 3 + doAfter: 2 + - to: ShelfChemistry + completed: + - !type:SnapToGrid + southRotation: true + steps: + - material: Plasteel + amount: 2 + doAfter: 2 + - material: ReinforcedGlass + amount: 5 + doAfter: 2 + - material: Plastic + amount: 5 + doAfter: 2 + - material: Cable + amount: 2 + doAfter: 1 + +# Normal deconstructs + - node: ShelfWood + entity: ShelfWood + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: MaterialWoodPlank1 + amount: 4 + steps: + - tool: Prying + doAfter: 2 + + - node: ShelfMetal + entity: ShelfMetal + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetSteel1 + amount: 5 + steps: + - tool: Screwing + doAfter: 5 + + - node: ShelfGlass + entity: ShelfGlass + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetGlass1 + amount: 4 + steps: + - tool: Screwing + doAfter: 2 +# Reinforced deconstructs + - node: ShelfRWood + entity: ShelfRWood + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: MaterialWoodPlank1 + amount: 8 + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 2 + steps: + - tool: Screwing + doAfter: 5 + - tool: Prying + doAfter: 2 + + - node: ShelfRMetal + entity: ShelfRMetal + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetPlasteel1 + amount: 5 + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 5 + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 3 + steps: + - tool: Screwing + doAfter: 2 + - tool: Welding + doAfter: 5 + + - node: ShelfRGlass + entity: ShelfRGlass + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetPlastic1 + amount: 5 + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 5 + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 2 + steps: + - tool: Welding + doAfter: 2 + - tool: Screwing + doAfter: 4 + +# Departmental deconstructs + - node: ShelfBar + entity: ShelfBar + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: MaterialWoodPlank1 + amount: 6 + steps: + - tool: Prying + doAfter: 3 + + - node: ShelfKitchen + entity: ShelfKitchen + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: PartRodMetal + amount: 2 + - !type:SpawnPrototype + prototype: SheetSteel1 + amount: 5 + - !type:SpawnPrototype + prototype: MaterialWoodPlank1 + amount: 3 + steps: + - tool: Screwing + doAfter: 2 + - tool: Welding + doAfter: 2 + - tool: Prying + doAfter: 1 + + - node: ShelfChemistry + entity: ShelfChemistry + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetPlasteel1 + amount: 2 + - !type:SpawnPrototype + prototype: SheetPlastic1 + amount: 5 + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 5 + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 2 + steps: + - tool: Welding + doAfter: 2 + - tool: Screwing + doAfter: 1 + - tool: Anchoring + doAfter: 2 + - tool: Prying + doAfter: 4 diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/tools/logic_gate.yml b/Resources/Prototypes/Recipes/Construction/Graphs/tools/logic_gate.yml index 6e64f061eb9..d366a2ea59a 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/tools/logic_gate.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/tools/logic_gate.yml @@ -34,7 +34,7 @@ state: icon name: a multitool - node: logic_gate - entity: LogicGate + entity: LogicGateOr - node: edge_detector entity: EdgeDetector - node: power_sensor diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/utilities/intercom.yml b/Resources/Prototypes/Recipes/Construction/Graphs/utilities/intercom.yml index 2247860f892..ba29d72539a 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/utilities/intercom.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/utilities/intercom.yml @@ -11,13 +11,17 @@ doAfter: 2.0 - node: assembly - entity: IntercomAssesmbly + entity: IntercomAssembly edges: - to: wired steps: - material: Cable amount: 2 doAfter: 1 + completed: + - !type:VisualizerDataInt + key: "enum.ConstructionVisuals.Layer" + data: 1 - to: start completed: - !type:GivePrototype @@ -29,7 +33,7 @@ doAfter: 2 - node: wired - entity: IntercomAssesmbly + entity: IntercomAssembly edges: - to: electronics steps: @@ -45,6 +49,9 @@ - !type:GivePrototype prototype: CableApcStack1 amount: 2 + - !type:VisualizerDataInt + key: "enum.ConstructionVisuals.Layer" + data: 0 steps: - tool: Cutting doAfter: 1 @@ -57,7 +64,11 @@ doAfter: 2 - node: intercom - entity: IntercomCommon #TODO: make this work with encryption keys + entity: IntercomConstructed + doNotReplaceInheritingEntities: true + actions: + - !type:SetWiresPanelSecurity + wiresAccessible: true edges: - to: wired conditions: @@ -72,3 +83,27 @@ steps: - tool: Prying doAfter: 1 + - to: intercomReinforced + conditions: + - !type:WirePanel + steps: + - material: Steel + amount: 1 + - tool: Welding + doAfter: 1 + + - node: intercomReinforced + actions: + - !type:SetWiresPanelSecurity + examine: wires-panel-component-on-examine-security-level2 + wiresAccessible: false + edges: + - to: intercom + conditions: + - !type:WirePanel + completed: + - !type:GivePrototype + prototype: SheetSteel1 + steps: + - tool: Welding + doAfter: 5 diff --git a/Resources/Prototypes/Recipes/Construction/storage.yml b/Resources/Prototypes/Recipes/Construction/storage.yml index c8edebc5096..7128c79eee8 100644 --- a/Resources/Prototypes/Recipes/Construction/storage.yml +++ b/Resources/Prototypes/Recipes/Construction/storage.yml @@ -67,3 +67,151 @@ canBuildInImpassable: false conditions: - !type:TileNotBlocked + +# Shelfs +# Normals +- type: construction + id: ShelfWood + name: wooden shelf + description: A convenient place to place, well, anything really. + graph: Shelf + startNode: start + targetNode: ShelfWood + icon: + sprite: Structures/Storage/Shelfs/wood.rsi + state: base + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +- type: construction + id: ShelfMetal + name: metal shelf + description: A sturdy place to place, well, anything really. + graph: Shelf + startNode: start + targetNode: ShelfMetal + icon: + sprite: Structures/Storage/Shelfs/metal.rsi + state: base + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +- type: construction + id: ShelfGlass + name: glass shelf + description: Just like a normal shelf! But fragile and without the walls! + graph: Shelf + startNode: start + targetNode: ShelfGlass + icon: + sprite: Structures/Storage/Shelfs/glass.rsi + state: base + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +# Reinforced +- type: construction + id: ShelfRWood + name: sturdy wooden shelf + description: The perfect place to store all your vintage records. + graph: Shelf + startNode: start + targetNode: ShelfRWood + icon: + sprite: Structures/Storage/Shelfs/wood.rsi + state: rbase + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +- type: construction + id: ShelfRMetal + name: sturdy metal shelf + description: Nice and strong, and keeps your maints loot secure. + graph: Shelf + startNode: start + targetNode: ShelfRMetal + icon: + sprite: Structures/Storage/Shelfs/metal.rsi + state: rbase + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +- type: construction + id: ShelfRGlass + name: sturdy glass shelf + description: See through, decent strength, shiny plastic case. Whats not to love? + graph: Shelf + startNode: start + targetNode: ShelfRGlass + icon: + sprite: Structures/Storage/Shelfs/glass.rsi + state: rbase + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +# Departmental +- type: construction + id: ShelfBar + name: bar shelf + description: A convenient place for all your extra booze, specifically designed to hold more bottles! + graph: Shelf + startNode: start + targetNode: ShelfBar + icon: + sprite: Structures/Storage/Shelfs/Departments/Service/bar.rsi + state: base + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +- type: construction + id: ShelfKitchen + name: cooking shelf + description: Holds your knifes, spice, and everything nice! + graph: Shelf + startNode: start + targetNode: ShelfKitchen + icon: + sprite: Structures/Storage/Shelfs/Departments/Service/kitchen.rsi + state: base + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition + +- type: construction + id: ShelfChemistry + name: chemical shelf + description: Perfect for keeping the most important chemicals safe, and out of the clumsy clowns hands! + graph: Shelf + startNode: start + targetNode: ShelfChemistry + icon: + sprite: Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi + state: base + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: true + conditions: + - !type:WallmountCondition diff --git a/Resources/Prototypes/Recipes/Construction/utilities.yml b/Resources/Prototypes/Recipes/Construction/utilities.yml index 19f2fee1837..82c16de7b6a 100644 --- a/Resources/Prototypes/Recipes/Construction/utilities.yml +++ b/Resources/Prototypes/Recipes/Construction/utilities.yml @@ -790,7 +790,7 @@ # INTERCOM - type: construction name: intercom - id: IntercomAssesmbly + id: IntercomAssembly graph: Intercom startNode: start targetNode: intercom diff --git a/Resources/Prototypes/Recipes/Cooking/food_sequence_element.yml b/Resources/Prototypes/Recipes/Cooking/food_sequence_element.yml new file mode 100644 index 00000000000..68605e08d80 --- /dev/null +++ b/Resources/Prototypes/Recipes/Cooking/food_sequence_element.yml @@ -0,0 +1,1154 @@ +# Bun bottom + +- type: foodSequenceElement + id: BunTopBurger + final: true + sprites: + - sprite: Objects/Consumable/Food/burger_sequence.rsi + state: bun_top + tags: + - Bun + +# Mice + +- type: foodSequenceElement + id: RatBurger + name: food-sequence-burger-content-rat + sprites: + - sprite: Mobs/Animals/mouse.rsi + state: dead-0 + - sprite: Mobs/Animals/mouse.rsi + state: dead-1 + - sprite: Mobs/Animals/mouse.rsi + state: dead-2 + +- type: foodSequenceElement + id: RatTaco + name: food-sequence-content-rat + sprites: + - sprite: Objects/Consumable/Food/taco_sequence.rsi + state: rat + +- type: foodSequenceElement + id: RatSkewer + name: food-sequence-content-rat + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-rat + +# Cheese + +- type: foodSequenceElement + id: CheeseBurger + name: food-sequence-content-cheese + sprites: + - sprite: Objects/Consumable/Food/burger_sequence.rsi + state: cheese + tags: + - Cheese + +- type: foodSequenceElement + id: CheeseTaco + name: food-sequence-content-cheese + sprites: + - sprite: Objects/Consumable/Food/taco_sequence.rsi + state: cheese + tags: + - Cheese + +# Steak + +- type: foodSequenceElement + id: MeatSteak + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: plain-cooked + tags: + - Cooked + - Meat + +# Becon + +- type: foodSequenceElement + id: MeatBecon + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: bacon-cooked + - sprite: Objects/Consumable/Food/meat.rsi + state: bacon2-cooked + tags: + - Cooked + - Meat + +# Bear meat + +- type: foodSequenceElement + id: MeatBear + name: food-sequence-content-bear + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: product-cooked + tags: + - Cooked + - Meat + +- type: foodSequenceElement + id: MeatBearBurger + name: food-sequence-burger-content-bear + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: product-cooked + tags: + - Cooked + - Meat + +# Penguin meat + +- type: foodSequenceElement + id: MeatPenguin + name: food-sequence-content-penguin + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: bird-cooked + tags: + - Cooked + - Meat + +- type: foodSequenceElement + id: MeatPenguinBurger + name: food-sequence-burger-content-penguin + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: bird-cooked + tags: + - Cooked + - Meat + +# Chicken meat + +- type: foodSequenceElement + id: MeatChicken + name: food-sequence-content-chicken + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: chicken-fried + tags: + - Cooked + - Meat + - Chicken + +# Fried Chicken meat + +- type: foodSequenceElement + id: MeatFriedChicken + name: food-sequence-content-chicken + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: chicken-fried + - sprite: Objects/Consumable/Food/meat.rsi + state: chicken2-fried + tags: + - Cooked + - Meat + - Chicken + +# Duck meat + +- type: foodSequenceElement + id: MeatDuck + name: food-sequence-content-duck + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: bird-cooked + tags: + - Cooked + - Meat + +# Crab meat + +- type: foodSequenceElement + id: MeatCrab + name: food-sequence-content-crab + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: crab-cooked + tags: + - Cooked + - Meat + - Crab + +- type: foodSequenceElement + id: MeatCrabBurger + name: food-sequence-burger-content-crab + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: crab-cooked + tags: + - Cooked + - Meat + - Crab + +# Meat goliath + +- type: foodSequenceElement + id: MeatGoliath + name: food-sequence-content-goliath + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: goliath-cooked + tags: + - Cooked + - Meat + +- type: foodSequenceElement + id: MeatGoliathBurger + name: food-sequence-burger-content-goliath + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: goliath-cooked + tags: + - Cooked + - Meat + +# Xeno meat + +- type: foodSequenceElement + id: MeatXeno + name: food-sequence-content-xeno + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: rouny-cooked + tags: + - Cooked + - Meat + +# Meat lizard + +- type: foodSequenceElement + id: MeatLizard + name: food-sequence-content-lizard + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: lizard-cooked + tags: + - Cooked + - Meat + +- type: foodSequenceElement + id: MeatLizardBurger + name: food-sequence-burger-content-lizard + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: lizard-cooked + tags: + - Cooked + - Meat + +# Meat spider + +- type: foodSequenceElement + id: MeatSpider + name: food-sequence-content-spider + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: spiderleg-cooked + tags: + - Cooked + - Meat + +- type: foodSequenceElement + id: MeatSpiderBurger + name: food-sequence-burger-content-spider + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: spiderleg-cooked + tags: + - Cooked + - Meat + +# Meatball + +- type: foodSequenceElement + id: MeatBall + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: meatball-cooked + tags: + - Cooked + - Meat + +# Snail meat + +- type: foodSequenceElement + id: MeatSnail + name: food-sequence-content-snail + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: snail-cooked + tags: + - Cooked + - Meat + +# Meat cutlet + +- type: foodSequenceElement + id: MeatCutlet + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Bear cutlet + +- type: foodSequenceElement + id: BearCutlet + name: food-sequence-content-bear + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +- type: foodSequenceElement + id: BearCutletBurger + name: food-sequence-burger-content-bear + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Penguin cutlet + +- type: foodSequenceElement + id: PenguinCutlet + name: food-sequence-content-penguin + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +- type: foodSequenceElement + id: PenguinCutletBurger + name: food-sequence-burger-content-penguin + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Chicken cutlet + +- type: foodSequenceElement + id: ChickenCutlet + name: food-sequence-content-chicken + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + - Chicken + +# Duck cutlet + +- type: foodSequenceElement + id: DuckCutlet + name: food-sequence-content-duck + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Spider cutlet + +- type: foodSequenceElement + id: LizardCutlet + name: food-sequence-content-lizard + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +- type: foodSequenceElement + id: LizardCutletBurger + name: food-sequence-burger-content-lizard + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: cutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Spider cutlet + +- type: foodSequenceElement + id: SpiderCutlet + name: food-sequence-content-spider + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: spidercutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +- type: foodSequenceElement + id: SpiderCutletBurger + name: food-sequence-burger-content-spider + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: spidercutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Xeno cutlet + +- type: foodSequenceElement + id: XenoCutlet + name: food-sequence-content-xeno + sprites: + - sprite: Objects/Consumable/Food/meat.rsi + state: xenocutlet-cooked + tags: + - Cooked + - Cutlet + - Meat + +# Brain + +- type: foodSequenceElement + id: Brain + name: food-sequence-content-brain + sprites: + - sprite: Mobs/Species/Human/organs.rsi + state: brain + tags: + - Brain + - Raw + +# Banana + +- type: foodSequenceElement + id: Banana + name: food-sequence-content-banana + sprites: + - sprite: Objects/Specific/Hydroponics/banana.rsi + state: produce + tags: + - Fruit + +# Mimana + +- type: foodSequenceElement + id: Mimana + name: food-sequence-content-mimana + sprites: + - sprite: Objects/Specific/Hydroponics/mimana.rsi + state: produce + tags: + - Fruit + +# Carrot + +- type: foodSequenceElement + id: Carrot + name: food-sequence-content-carrot + sprites: + - sprite: Objects/Specific/Hydroponics/carrot.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: CarrotBurger + name: food-sequence-burger-content-carrot + sprites: + - sprite: Objects/Specific/Hydroponics/carrot.rsi + state: produce + tags: + - Vegetable + +# Cabbage + +- type: foodSequenceElement + id: Cabbage + name: food-sequence-content-cabbage + sprites: + - sprite: Objects/Specific/Hydroponics/carrot.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: CabbageBurger + name: food-sequence-burger-content-cabbage + sprites: + - sprite: Objects/Specific/Hydroponics/cabbage.rsi + state: produce + tags: + - Vegetable + +# Garlic + +- type: foodSequenceElement + id: Garlic + name: food-sequence-content-garlic + sprites: + - sprite: Objects/Specific/Hydroponics/garlic.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: GarlicBurger + name: food-sequence-burger-content-garlic + sprites: + - sprite: Objects/Specific/Hydroponics/garlic.rsi + state: produce + tags: + - Vegetable + +# Lemon + +- type: foodSequenceElement + id: Lemon + name: food-sequence-content-lemon + sprites: + - sprite: Objects/Specific/Hydroponics/lemon.rsi + state: produce + tags: + - Fruit + +# Lemoon + +- type: foodSequenceElement + id: Lemoon + name: food-sequence-content-lemoon + sprites: + - sprite: Objects/Specific/Hydroponics/lemoon.rsi + state: produce + tags: + - Fruit + +# Lime + +- type: foodSequenceElement + id: Lime + name: food-sequence-content-lime + sprites: + - sprite: Objects/Specific/Hydroponics/lime.rsi + state: produce + tags: + - Fruit + +# Orange + +- type: foodSequenceElement + id: Orange + name: food-sequence-content-orange + sprites: + - sprite: Objects/Specific/Hydroponics/orange.rsi + state: produce + tags: + - Fruit + +# Extradimensional Orange + +- type: foodSequenceElement + id: ExtradimensionalOrange + name: food-sequence-content-orange + sprites: + - sprite: Objects/Specific/Hydroponics/extradimensional_orange.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: ExtradimensionalOrangeBurger + name: food-sequence-burger-content-extradimensional-orange + sprites: + - sprite: Objects/Specific/Hydroponics/extradimensional_orange.rsi + state: produce + tags: + - Fruit + +# Potato + +- type: foodSequenceElement + id: Potato + name: food-sequence-content-potato + sprites: + - sprite: Objects/Specific/Hydroponics/potato.rsi + state: produce + tags: + - Vegetable + +# Tomato + +- type: foodSequenceElement + id: Tomato + name: food-sequence-content-tomato + sprites: + - sprite: Objects/Specific/Hydroponics/tomato.rsi + state: produce + tags: + - Fruit + - Vegetable + +- type: foodSequenceElement + id: TomatoSkewer + name: food-sequence-content-tomato + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-tomato + tags: + - Fruit + - Vegetable + +# Blue Tomato + +- type: foodSequenceElement + id: BlueTomato + name: food-sequence-content-tomato + sprites: + - sprite: Objects/Specific/Hydroponics/blue_tomato.rsi + state: produce + tags: + - Fruit + - Vegetable + +# Blood Tomato + +- type: foodSequenceElement + id: BloodTomato + name: food-sequence-content-tomato + sprites: + - sprite: Objects/Specific/Hydroponics/blood_tomato.rsi + state: produce + tags: + - Fruit + - Vegetable + +# Apple + +- type: foodSequenceElement + id: Apple + name: food-sequence-content-apple + sprites: + - sprite: Objects/Specific/Hydroponics/apple.rsi + state: produce + tags: + - Fruit + +# Golden Apple + +- type: foodSequenceElement + id: GoldenApple + name: food-sequence-content-apple + sprites: + - sprite: Objects/Specific/Hydroponics/golden_apple.rsi + state: produce + tags: + - Fruit + +# Pineapple + +- type: foodSequenceElement + id: PineappleSliceBurger + name: food-sequence-burger-content-pineapple + sprites: + - sprite: Objects/Specific/Hydroponics/pineapple.rsi + state: slice + tags: + - Fruit + - Slice + +- type: foodSequenceElement + id: PineappleSlice + name: food-sequence-content-pineapple + sprites: + - sprite: Objects/Specific/Hydroponics/pineapple.rsi + state: slice + tags: + - Fruit + - Slice + +# Onion + +- type: foodSequenceElement + id: OnionSliceBurger + name: food-sequence-burger-content-onion + sprites: + - sprite: Objects/Specific/Hydroponics/onion.rsi + state: slice + tags: + - Vegetable + - Slice + +- type: foodSequenceElement + id: OnionSlice + name: food-sequence-content-onion + sprites: + - sprite: Objects/Specific/Hydroponics/onion.rsi + state: slice + tags: + - Vegetable + - Slice + +# Onion red + +- type: foodSequenceElement + id: OnionRedSliceBurger + name: food-sequence-burger-content-onion + sprites: + - sprite: Objects/Specific/Hydroponics/onion_red.rsi + state: slice + tags: + - Vegetable + - Slice + +- type: foodSequenceElement + id: OnionRedSlice + name: food-sequence-content-onion + sprites: + - sprite: Objects/Specific/Hydroponics/onion_red.rsi + state: slice + tags: + - Vegetable + - Slice + +# Watermelon + +- type: foodSequenceElement + id: WatermelonSliceBurger + name: food-sequence-burger-content-watermelon + sprites: + - sprite: Objects/Specific/Hydroponics/watermelon.rsi + state: slice + tags: + - Fruit + - Slice + +- type: foodSequenceElement + id: WatermelonSlice + name: food-sequence-content-watermelon + sprites: + - sprite: Objects/Specific/Hydroponics/watermelon.rsi + state: slice + tags: + - Fruit + - Slice + +- type: foodSequenceElement + id: WatermelonSliceSkewer + name: food-sequence-content-watermelon + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-watermelon + tags: + - Fruit + - Slice + +- type: foodSequenceElement + id: HolymelonSliceBurger + name: food-sequence-burger-content-holymelon + sprites: + - sprite: Objects/Specific/Hydroponics/holymelon.rsi + state: slice + tags: + - Fruit + - Slice + +- type: foodSequenceElement + id: HolymelonSlice + name: food-sequence-content-holymelon + sprites: + - sprite: Objects/Specific/Hydroponics/holymelon.rsi + state: slice + tags: + - Fruit + - Slice + +- type: foodSequenceElement + id: HolymelonSliceSkewer + name: food-sequence-content-holymelon + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-holymelon + tags: + - Fruit + - Slice + +# Chili pepper + +- type: foodSequenceElement + id: ChiliPepper + name: food-sequence-content-chili + sprites: + - sprite: Objects/Specific/Hydroponics/chili.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: ChiliPepperSkewer + name: food-sequence-content-chili + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-pepper + tags: + - Vegetable + +# Chilly pepper + +- type: foodSequenceElement + id: ChillyPepper + name: food-sequence-content-chilly + sprites: + - sprite: Objects/Specific/Hydroponics/chilly.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: ChillyPepperSkewer + name: food-sequence-content-chilly + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-bluepepper + tags: + - Vegetable + +# Corn +- type: foodSequenceElement + id: Corn + name: food-sequence-content-corn + sprites: + - sprite: Objects/Specific/Hydroponics/corn.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: CornSkewer + name: food-sequence-content-corn + sprites: + - sprite: Objects/Consumable/Food/skewer.rsi + state: skewer-corn + tags: + - Vegetable + +# Aloe + +- type: foodSequenceElement + id: Aloe + name: food-sequence-content-aloe + sprites: + - sprite: Objects/Specific/Hydroponics/aloe.rsi + state: produce + tags: + - Vegetable + +# Poppy + +- type: foodSequenceElement + id: Poppy + name: food-sequence-content-poppy + sprites: + - sprite: Objects/Specific/Hydroponics/poppy.rsi + state: produce + tags: + - Flower + +# lily + +- type: foodSequenceElement + id: Lily + name: food-sequence-content-lily + sprites: + - sprite: Objects/Specific/Hydroponics/lily.rsi + state: produce + tags: + - Flower + +# lingzhi + +- type: foodSequenceElement + id: Lingzhi + name: food-sequence-content-mushroom + sprites: + - sprite: Objects/Specific/Hydroponics/lingzhi.rsi + state: produce + +# AmbrosiaVulgaris + +- type: foodSequenceElement + id: AmbrosiaVulgaris + name: food-sequence-content-ambrosia + sprites: + - sprite: Objects/Specific/Hydroponics/ambrosia_vulgaris.rsi + state: produce + +- type: foodSequenceElement + id: AmbrosiaVulgarisBurger + name: food-sequence-burger-content-ambrosia + sprites: + - sprite: Objects/Specific/Hydroponics/ambrosia_vulgaris.rsi + state: produce + +# AmbrosiaDeus + +- type: foodSequenceElement + id: AmbrosiaDeus + name: food-sequence-content-ambrosia + sprites: + - sprite: Objects/Specific/Hydroponics/ambrosia_deus.rsi + state: produce + +- type: foodSequenceElement + id: AmbrosiaDeusBurger + name: food-sequence-burger-content-ambrosia + sprites: + - sprite: Objects/Specific/Hydroponics/ambrosia_deus.rsi + state: produce + +# Glasstle + +- type: foodSequenceElement + id: Glasstle + name: food-sequence-content-glasstle + sprites: + - sprite: Objects/Specific/Hydroponics/glasstle.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: GlasstleBurger + name: food-sequence-burger-content-glasstle + sprites: + - sprite: Objects/Specific/Hydroponics/glasstle.rsi + state: produce + tags: + - Fruit + +# FlyAmanita + +- type: foodSequenceElement + id: FlyAmanita + name: food-sequence-content-mushroom + sprites: + - sprite: Objects/Specific/Hydroponics/fly_amanita.rsi + state: produce + +# Gatfruit + +- type: foodSequenceElement + id: Gatfruit + name: food-sequence-content-gatfruit + sprites: + - sprite: Objects/Specific/Hydroponics/gatfruit.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: GatfruitBurger + name: food-sequence-burger-content-gatfruit + sprites: + - sprite: Objects/Specific/Hydroponics/gatfruit.rsi + state: produce + tags: + - Fruit + +# Capfruit + +- type: foodSequenceElement + id: Capfruit + name: food-sequence-content-capfruit + sprites: + - sprite: Objects/Specific/Hydroponics/capfruit.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: CapfruitBurger + name: food-sequence-burger-content-capfruit + sprites: + - sprite: Objects/Specific/Hydroponics/capfruit.rsi + state: produce + tags: + - Fruit + +# Soybeans + +- type: foodSequenceElement + id: Soybeans + name: food-sequence-content-soy + sprites: + - sprite: Objects/Specific/Hydroponics/soybeans.rsi + state: produce + tags: + - Vegetable + +- type: foodSequenceElement + id: SoybeansBurger + name: food-sequence-burger-content-soy + sprites: + - sprite: Objects/Specific/Hydroponics/soybeans.rsi + state: produce + tags: + - Vegetable + +# SpacemansTrumpet + +- type: foodSequenceElement + id: SpacemansTrumpet + name: food-sequence-content-spacemans-trumpet + sprites: + - sprite: Objects/Specific/Hydroponics/spacemans_trumpet.rsi + state: produce + tags: + - Flower + +- type: foodSequenceElement + id: SpacemansTrumpetBurger + name: food-sequence-burger-content-spacemans-trumpet + sprites: + - sprite: Objects/Specific/Hydroponics/spacemans_trumpet.rsi + state: produce + tags: + - Flower + +# Koibean + +- type: foodSequenceElement + id: Koibean + name: food-sequence-content-koibean + sprites: + - sprite: Objects/Specific/Hydroponics/koibean.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: KoibeanBurger + name: food-sequence-burger-content-koibean + sprites: + - sprite: Objects/Specific/Hydroponics/koibean.rsi + state: produce + tags: + - Fruit + +# Galaxythistle + +- type: foodSequenceElement + id: Galaxythistle + name: food-sequence-content-galaxy + sprites: + - sprite: Objects/Specific/Hydroponics/galaxythistle.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: GalaxythistleBurger + name: food-sequence-burger-content-galaxy + sprites: + - sprite: Objects/Specific/Hydroponics/galaxythistle.rsi + state: produce + tags: + - Fruit + +# bungo + +- type: foodSequenceElement + id: Bungo + name: food-sequence-content-bungo + sprites: + - sprite: Objects/Specific/Hydroponics/bungo.rsi + state: produce + tags: + - Fruit + +# Pea + +- type: foodSequenceElement + id: Pea + name: food-sequence-content-pea + sprites: + - sprite: Objects/Specific/Hydroponics/pea.rsi + state: produce + tags: + - Vegetable + +# Cherry + +- type: foodSequenceElement + id: Cherry + name: food-sequence-content-cherry + sprites: + - sprite: Objects/Specific/Hydroponics/cherry.rsi + state: produce + tags: + - Fruit + +# Berries + +- type: foodSequenceElement + id: Berries + name: food-sequence-content-berries + sprites: + - sprite: Objects/Specific/Hydroponics/berries.rsi + state: produce + tags: + - Fruit + +- type: foodSequenceElement + id: BerriesBurger + name: food-sequence-burger-content-berries + sprites: + - sprite: Objects/Specific/Hydroponics/berries.rsi + state: produce + tags: + - Fruit + +# Suppermatter + +- type: foodSequenceElement + id: Suppermatter + name: food-sequence-content-suppermatter + sprites: + - sprite: Objects/Consumable/Food/Baked/cake.rsi + state: suppermatter-shard + +- type: foodSequenceElement + id: SuppermatterBurger + name: food-sequence-burger-content-suppermatter + sprites: + - sprite: Objects/Consumable/Food/Baked/cake.rsi + state: suppermatter-shard diff --git a/Resources/Prototypes/Recipes/Cooking/meal_recipes.yml b/Resources/Prototypes/Recipes/Cooking/meal_recipes.yml index f3176cc5e7a..de939ed3fc1 100644 --- a/Resources/Prototypes/Recipes/Cooking/meal_recipes.yml +++ b/Resources/Prototypes/Recipes/Cooking/meal_recipes.yml @@ -57,7 +57,7 @@ FoodTomato: 1 FoodOnionSlice: 2 -- type: microwaveMealRecipe +- type: microwaveMealRecipe #Added to metamorph recipes id: RecipeBrainBurger name: brain burger recipe result: FoodBurgerBrain @@ -76,7 +76,7 @@ FoodMeat: 1 ClothingHeadHatCatEars: 1 -- type: microwaveMealRecipe +- type: microwaveMealRecipe #Added to metamorph recipes id: RecipeCheeseburger name: cheeseburger recipe result: FoodBurgerCheese @@ -86,7 +86,7 @@ FoodMeat: 1 FoodCheeseSlice: 1 -- type: microwaveMealRecipe +- type: microwaveMealRecipe #Added to metamorph recipes id: RecipeChickenSandwich name: chicken sandwich recipe result: FoodBurgerChicken @@ -115,7 +115,7 @@ FoodBreadBun: 1 FoodMeatCorgi: 1 -- type: microwaveMealRecipe +- type: microwaveMealRecipe #Added to metamorph recipes id: RecipeCrabBurger name: crab burger recipe result: FoodBurgerCrab @@ -140,7 +140,7 @@ CrayonGreen: 1 Flare: 1 -- type: microwaveMealRecipe +- type: microwaveMealRecipe #Added to metamorph recipes id: RecipeDuckBurger name: duck sandwich recipe result: FoodBurgerDuck @@ -432,6 +432,19 @@ solids: FoodDough: 1 +- type: microwaveMealRecipe + id: RecipeBaguetteSword + name: baguette sword recipe + result: WeaponBaguette + secretRecipe: true + time: 15 + reagents: + TableSalt: 5 + Blackpepper: 5 + solids: + FoodDough: 1 + PartRodMetal1: 1 + - type: microwaveMealRecipe id: RecipeButteredToast name: buttered toast recipe @@ -439,7 +452,7 @@ time: 5 solids: FoodBreadPlainSlice: 1 - FoodButter: 1 + FoodButterSlice: 1 - type: microwaveMealRecipe id: RecipeFrenchToast @@ -460,7 +473,7 @@ solids: FoodBreadPlainSlice: 1 FoodGarlic: 1 - FoodButter: 1 + FoodButterSlice: 1 - type: microwaveMealRecipe id: RecipeJellyToast @@ -896,6 +909,29 @@ FoodBungo: 2 FoodChiliPepper: 1 +- type: microwaveMealRecipe + id: RecipeBoiledSnail + name: boiled snail recipe + result: FoodMeatSnailCooked + time: 5 + reagents: + Water: 10 + solids: + FoodMeatSnail: 1 + +- type: microwaveMealRecipe + id: RecipeEscargotSoup + name: escargot recipe + result: FoodSoupEscargot + time: 10 + reagents: + Water: 5 + solids: + FoodBowlBig: 1 + FoodOnionSlice: 1 + FoodButter: 1 + FoodMeatSnailCooked: 1 + #Pies - type: microwaveMealRecipe @@ -948,15 +984,15 @@ FoodBerries: 3 FoodPlateTin: 1 -#- type: microwaveMealRecipe -# id: RecipeCherryPie -# name: cherry pie recipe -# result: FoodPieCherry -# time: 15 -# solids: -# FoodDoughPie: 1 -# FoodCherry: 3 #cherries don't exist yet -# FoodPlateTin: 1 +- type: microwaveMealRecipe + id: RecipeCherryPie + name: cherry pie recipe + result: FoodPieCherry + time: 15 + solids: + FoodDoughPie: 1 + FoodCherry: 5 + FoodPlateTin: 1 - type: microwaveMealRecipe id: RecipeFrostyPie @@ -1187,7 +1223,7 @@ Flour: 5 Sugar: 5 solids: - FoodButter: 1 + FoodButterSlice: 1 FoodSnackChocolateBar: 1 - type: microwaveMealRecipe @@ -1199,7 +1235,7 @@ Flour: 5 Sugar: 10 solids: - FoodButter: 1 + FoodButterSlice: 1 - type: microwaveMealRecipe id: RecipeRaisinCookie @@ -1221,7 +1257,7 @@ Oats: 5 Sugar: 5 solids: - FoodButter: 1 + FoodButterSlice: 1 - type: microwaveMealRecipe id: RecipeChocolateChipPancake @@ -1741,98 +1777,6 @@ Nitrogen: 10 Plasma: 10 -# Kebabs -- type: microwaveMealRecipe - id: RecipeMeatKebab - name: meat kebab recipe - result: FoodMeatKebab - time: 5 - solids: - FoodMeatCutlet: 3 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeHawaiianKebab - name: Hawaiian kebab recipe - result: FoodMeatHawaiianKebab - time: 5 - solids: - FoodChiliPepper: 1 - FoodMeatCutlet: 1 - FoodPineappleSlice: 1 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeFiestaKebab - name: fiesta kebab recipe - result: FoodMeatFiestaKebab - time: 5 - solids: - FoodChiliPepper: 1 - FoodCorn: 1 - FoodMeatCutlet: 1 - FoodTomato: 1 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeRatKebab - name: rat kebab recipe - result: FoodMeatRatKebab - time: 10 - solids: - FoodMeatRat: 1 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeDoubleRatKebab - name: double rat kebab recipe - result: FoodMeatRatdoubleKebab - time: 20 - solids: - FoodMeatRat: 2 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeHumanKebab - name: human kebab recipe - result: FoodMeatHumanKebab - time: 15 - solids: - TorsoHuman: 1 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeLizardKebab - name: lizard kebab recipe - result: FoodMeatLizardtailKebab - time: 15 - solids: - FoodMeatLizard: 1 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeSnakeKebab - name: snake kebab recipe - result: FoodMeatSnakeKebab - time: 15 - reagents: - Wine: 5 - solids: - FoodMeatSnake: 1 - FoodKebabSkewer: 1 - -- type: microwaveMealRecipe - id: RecipeFoodMealSoftTaco - name: soft taco recipe - result: FoodMealSoftTaco - time: 10 - solids: - FoodDoughSlice: 1 - FoodTomato: 1 - FoodOnionSlice: 2 - FoodCheeseSlice: 1 - FoodMeatCutlet: 1 - - type: microwaveMealRecipe id: RecipeFoodBakedChevreChaud name: chevre chaud recipe @@ -1968,3 +1912,23 @@ FoodTomato: 1 FoodCabbage: 1 FoodOnionSlice: 2 + +- type: microwaveMealRecipe + id: RecipeCroissant + name: croissant recipe + result: FoodBakedCroissant + time: 5 + solids: + FoodCroissantRaw: 1 + FoodButterSlice: 1 + +- type: microwaveMealRecipe + id: RecipeThrowingCroissant + name: throwing croissant recipe + result: WeaponCroissant + secretRecipe: true + time: 5 + solids: + FoodCroissantRaw: 1 + FoodButterSlice: 1 + ShardGlass: 1 \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Cooking/sequence_metamorph.yml b/Resources/Prototypes/Recipes/Cooking/sequence_metamorph.yml new file mode 100644 index 00000000000..95f1c97a229 --- /dev/null +++ b/Resources/Prototypes/Recipes/Cooking/sequence_metamorph.yml @@ -0,0 +1,125 @@ +# rules for transferring recipes from microwaveMealRecipe +# 1) leave room for variation. If the original recipe calls for 2 pieces of meat, allow players to put in 2-3 pieces. +# 2) max SequenceLength must be 1 element greater than the minimum ingredient set requires. This will allow you to put 1 poison fly or 1 other ingredient in different recipes and the recipes will still be valid. + +- type: metamorphRecipe + id: FoodBurgerCheese + key: Burger + result: FoodBurgerCheese + rules: + - !type:SequenceLength + range: + min: 3 + max: 4 + - !type:IngredientsWithTags # 1 meat cutlet + tags: + - Cooked + - Cutlet + - Meat + count: + min: 1 + max: 2 + - !type:IngredientsWithTags # 1 cheese + tags: + - Cheese + count: + min: 1 + max: 2 + - !type:LastElementHasTags # last bun + tags: + - Bun + +- type: metamorphRecipe + id: FoodBurgerChicken + key: Burger + result: FoodBurgerChicken + rules: + - !type:SequenceLength + range: + min: 2 + max: 3 + - !type:IngredientsWithTags # 1 chicken meat + tags: + - Cooked + - Cutlet + - Meat + - Chicken + count: + min: 1 + max: 2 + - !type:FoodHasReagent # 5 +- 2 mayo + reagent: Mayo + count: + min: 3 + max: 7 + - !type:LastElementHasTags # last bun + tags: + - Bun + +- type: metamorphRecipe + id: FoodBurgerCrab + key: Burger + result: FoodBurgerCrab + rules: + - !type:SequenceLength + range: + min: 3 + max: 4 + - !type:IngredientsWithTags # 2 crab meat + tags: + - Cooked + - Meat + - Crab + count: + min: 2 + max: 3 + - !type:LastElementHasTags # last bun + tags: + - Bun + +- type: metamorphRecipe + id: FoodBurgerDuck + key: Burger + result: FoodBurgerDuck + rules: + - !type:SequenceLength + range: + min: 3 + max: 4 + - !type:IngredientsWithTags # 1 duck meat + tags: + - Cooked + - Cutlet + - Meat + - Duck + count: + min: 1 + max: 2 + - !type:IngredientsWithTags # 1 cheese + tags: + - Cheese + count: + min: 1 + max: 2 + - !type:LastElementHasTags # last bun + tags: + - Bun + +- type: metamorphRecipe + id: FoodBurgerBrain + key: Burger + result: FoodBurgerBrain + rules: + - !type:SequenceLength + range: + min: 2 + max: 3 + - !type:IngredientsWithTags # 1 brain + tags: + - Brain + count: + min: 1 + max: 2 + - !type:LastElementHasTags # last bun + tags: + - Bun \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/ied.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/firebomb.yml similarity index 90% rename from Resources/Prototypes/Recipes/Crafting/Graphs/improvised/ied.yml rename to Resources/Prototypes/Recipes/Crafting/Graphs/improvised/firebomb.yml index bdf06e558f1..529585583ca 100644 --- a/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/ied.yml +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/firebomb.yml @@ -1,5 +1,5 @@ - type: constructionGraph - id: ImprovisedExplosive + id: FireBomb start: start graph: - node: start @@ -19,7 +19,7 @@ state: icon doAfter: 1 - node: empty - entity: ImprovisedExplosiveEmpty + entity: FireBombEmpty edges: - to: start completed: @@ -42,7 +42,7 @@ - tool: Screwing doAfter: 1 - node: fuel - entity: ImprovisedExplosiveFuel + entity: FireBombFuel edges: - to: empty conditions: @@ -51,7 +51,7 @@ steps: - tool: Screwing doAfter: 1 - - to: ied + - to: firebomb conditions: # no dumping out 29u of the fuel then adding wires :) - !type:MinSolution solution: drink @@ -62,8 +62,8 @@ - material: Cable amount: 5 doAfter: 2 - - node: ied - entity: ImprovisedExplosive + - node: firebomb + entity: FireBomb edges: - to: fuel completed: diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/pipebomb.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/pipebomb.yml new file mode 100644 index 00000000000..bc661b47d93 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/improvised/pipebomb.yml @@ -0,0 +1,43 @@ +- type: constructionGraph + id: PipeBomb + start: start + graph: + - node: start + edges: + - to: gunpowder + steps: + - tag: Pipe + icon: + sprite: Structures/Piping/Atmospherics/pipe.rsi + state: pipeStraight + name: pipe + - material: Steel + amount: 1 + doAfter: 3 + - node: gunpowder + entity: PipeBombGunpowder + edges: + - to: cable + steps: + - material: Gunpowder + amount: 5 + doAfter: 3 + - node: cable + entity: PipeBombCable + edges: + - to: pipebomb + steps: + - material: Cable + amount: 5 + doAfter: 2 + - node: pipebomb + entity: PipeBomb + edges: + - to: cable + completed: + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 5 + steps: + - tool: Cutting + doAfter: 2 diff --git a/Resources/Prototypes/Recipes/Crafting/improvised.yml b/Resources/Prototypes/Recipes/Crafting/improvised.yml index 3f8458f58d1..cfb90e7d457 100644 --- a/Resources/Prototypes/Recipes/Crafting/improvised.yml +++ b/Resources/Prototypes/Recipes/Crafting/improvised.yml @@ -164,11 +164,11 @@ state: icon - type: construction - name: improvised explosive device - id: improvisedexplosive - graph: ImprovisedExplosive + name: fire bomb + id: firebomb + graph: FireBomb startNode: start - targetNode: ied + targetNode: firebomb category: construction-category-weapons objectType: Item description: A weak, improvised incendiary device. @@ -201,3 +201,16 @@ sprite: Clothing/Head/Hats/straw_hat.rsi state: icon objectType: Item + +- type: construction + name: pipe bomb + id: pipebomb + graph: PipeBomb + startNode: start + targetNode: pipebomb + category: construction-category-weapons + objectType: Item + description: An improvised explosive made from pipes and wire. + icon: + sprite: Objects/Weapons/Bombs/pipebomb.rsi + state: icon \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml index d0e6f370948..dbb4cd68df2 100644 --- a/Resources/Prototypes/Recipes/Lathes/electronics.yml +++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml @@ -939,7 +939,6 @@ completetime: 6 materials: Steel: 100 - Glass: 500 - type: latheRecipe @@ -977,6 +976,16 @@ Glass: 500 Gold: 100 +- type: latheRecipe + id: StationAnchorCircuitboard + result: StationAnchorCircuitboard + category: Circuitry + completetime: 8 + materials: + Steel: 100 + Glass: 900 + Gold: 100 + - type: latheRecipe id: ReagentGrinderIndustrialMachineCircuitboard result: ReagentGrinderIndustrialMachineCircuitboard diff --git a/Resources/Prototypes/Recipes/Lathes/medical.yml b/Resources/Prototypes/Recipes/Lathes/medical.yml index 0591a3b1215..ba8f596d215 100644 --- a/Resources/Prototypes/Recipes/Lathes/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/medical.yml @@ -144,7 +144,7 @@ - type: latheRecipe id: Medkit result: Medkit - name: first aid kit (empty) + name: lathe-recipe-Medkit-name completetime: 2 materials: Plastic: 300 @@ -152,7 +152,7 @@ - type: latheRecipe id: MedkitBurn result: MedkitBurn - name: burn treatment kit (empty) + name: lathe-recipe-MedkitBurn-name completetime: 2 materials: Plastic: 300 @@ -160,7 +160,7 @@ - type: latheRecipe id: MedkitToxin result: MedkitToxin - name: toxin treatment kit (empty) + name: lathe-recipe-MedkitToxin-name completetime: 2 materials: Plastic: 300 @@ -168,7 +168,7 @@ - type: latheRecipe id: MedkitO2 result: MedkitO2 - name: oxygen deprivation treatment kit (empty) + name: lathe-recipe-MedkitO2-name completetime: 2 materials: Plastic: 300 @@ -176,7 +176,7 @@ - type: latheRecipe id: MedkitBrute result: MedkitBrute - name: brute trauma treatment kit (empty) + name: lathe-recipe-MedkitBrute-name completetime: 2 materials: Plastic: 300 @@ -184,7 +184,7 @@ - type: latheRecipe id: MedkitAdvanced result: MedkitAdvanced - name: advanced first aid kit (empty) + name: lathe-recipe-MedkitAdvanced-name completetime: 2 materials: Plastic: 300 @@ -192,7 +192,7 @@ - type: latheRecipe id: MedkitRadiation result: MedkitRadiation - name: radiation treatment kit (empty) + name: lathe-recipe-MedkitRadiation-name completetime: 2 materials: Plastic: 300 @@ -200,7 +200,7 @@ - type: latheRecipe id: MedkitCombat result: MedkitCombat - name: combat medical kit (empty) + name: lathe-recipe-MedkitCombat-name completetime: 2 materials: Plastic: 300 @@ -242,4 +242,12 @@ completetime: 1 materials: Steel: 600 - Plastic: 300 \ No newline at end of file + Plastic: 300 + +- type: latheRecipe + id: WhiteCane + result: WhiteCane + completetime: 2 + materials: + Steel: 100 + Plastic: 100 \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Lathes/misc.yml b/Resources/Prototypes/Recipes/Lathes/misc.yml index 2c0e1eeec38..571e9bc97b9 100644 --- a/Resources/Prototypes/Recipes/Lathes/misc.yml +++ b/Resources/Prototypes/Recipes/Lathes/misc.yml @@ -126,6 +126,13 @@ Steel: 1000 Plastic: 500 +- type: latheRecipe + id: ClothingShoesBootsMoon + result: ClothingShoesBootsMoon + completetime: 2 + materials: + Steel: 600 + - type: latheRecipe id: ClothingShoesBootsSpeed result: ClothingShoesBootsSpeed diff --git a/Resources/Prototypes/Recipes/Lathes/salvage.yml b/Resources/Prototypes/Recipes/Lathes/salvage.yml index 9b3cb086433..84047ae75d8 100644 --- a/Resources/Prototypes/Recipes/Lathes/salvage.yml +++ b/Resources/Prototypes/Recipes/Lathes/salvage.yml @@ -14,3 +14,23 @@ Steel: 1000 Glass: 500 # If they get spammed make it cost silver. + +- type: latheRecipe + id: MiningDrill + result: MiningDrill + category: Tools + completetime: 3 + materials: + Steel: 500 + Plastic: 100 + +- type: latheRecipe + id: MiningDrillDiamond + result: MiningDrillDiamond + category: Tools + completetime: 3 + materials: + Steel: 600 + Plastic: 200 + Silver: 200 + Diamond: 100 diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index 0fcdb3e0c19..5b126f76973 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -724,6 +724,15 @@ Plastic: 320 Uranium: 240 +- type: latheRecipe + id: WeaponFlareGunSecurity + result: WeaponFlareGunSecurity + category: Weapons + completetime: 6 + materials: + Plastic: 100 + Steel: 400 + - type: latheRecipe id: WeaponDisabler result: WeaponDisabler diff --git a/Resources/Prototypes/Recipes/Lathes/sheet.yml b/Resources/Prototypes/Recipes/Lathes/sheet.yml index 9f83c58e66c..3f2c8f11c2a 100644 --- a/Resources/Prototypes/Recipes/Lathes/sheet.yml +++ b/Resources/Prototypes/Recipes/Lathes/sheet.yml @@ -2,9 +2,11 @@ id: SheetSteel result: SheetSteel1 applyMaterialDiscount: false - completetime: 2 + miningPoints: 1 + completetime: 0 materials: RawIron: 100 + Coal: 30 - type: latheRecipe id: SheetSteel30 @@ -18,7 +20,8 @@ id: SheetGlass1 result: SheetGlass1 applyMaterialDiscount: false - completetime: 2 + miningPoints: 1 + completetime: 0 materials: RawQuartz: 100 @@ -33,10 +36,12 @@ id: SheetRGlass result: SheetRGlass1 applyMaterialDiscount: false - completetime: 2 + miningPoints: 1 + completetime: 0 materials: Glass: 100 Steel: 50 + Coal: 15 - type: latheRecipe id: SheetRGlass30 @@ -47,6 +52,15 @@ RawIron: 1500 Coal: 500 +- type: latheRecipe + id: SheetPGlass1 + result: SheetPGlass1 + completetime: 0 + miningPoints: 16 + materials: + RawQuartz: 100 + RawPlasma: 100 + - type: latheRecipe id: SheetPGlass30 result: SheetPGlass @@ -55,6 +69,17 @@ RawQuartz: 3000 RawPlasma: 3000 +- type: latheRecipe + id: SheetRPGlass1 + result: SheetRPGlass1 + completetime: 0 + miningPoints: 16 + materials: + RawQuartz: 100 + RawPlasma: 100 + RawIron: 50 + Coal: 15 + - type: latheRecipe id: SheetRPGlass30 result: SheetRPGlass @@ -65,6 +90,14 @@ RawIron: 1500 Coal: 500 +- type: latheRecipe + id: SheetPlasma1 + result: SheetPlasma1 + completetime: 0 + miningPoints: 15 + materials: + RawPlasma: 100 + - type: latheRecipe id: SheetPlasma30 result: SheetPlasma @@ -72,6 +105,16 @@ materials: RawPlasma: 3000 +- type: latheRecipe + id: SheetPlasteel1 + result: SheetPlasteel1 + completetime: 0 + miningPoints: 17 + materials: + RawPlasma: 100 + RawIron: 200 #Twice as durable as steel, Twice the material cost + Coal: 30 + - type: latheRecipe id: SheetPlasteel30 result: SheetPlasteel @@ -88,6 +131,15 @@ materials: RawUranium: 3000 +- type: latheRecipe + id: SheetUGlass1 + result: SheetUGlass1 + completetime: 0 + miningPoints: 31 + materials: + RawUranium: 100 + RawQuartz: 100 + - type: latheRecipe id: SheetUGlass30 result: SheetUGlass @@ -96,6 +148,17 @@ RawUranium: 3000 RawQuartz: 3000 +- type: latheRecipe + id: SheetRUGlass1 + result: SheetRUGlass1 + completetime: 0 + miningPoints: 31 + materials: + RawUranium: 100 + RawQuartz: 100 + RawIron: 50 + Coal: 15 + - type: latheRecipe id: SheetRUGlass30 result: SheetRUGlass @@ -127,41 +190,53 @@ materials: RawBananium: 3000 +- type: latheRecipe + id: MaterialDiamond + result: MaterialDiamond1 + completetime: 0 + miningPoints: 50 + materials: + RawDiamond: 100 + - type: latheRecipe id: SheetUranium1 result: SheetUranium1 - completetime: 2 + completetime: 0 + miningPoints: 30 materials: - RawUranium: 500 + RawUranium: 100 - type: latheRecipe id: IngotGold1 result: IngotGold1 - completetime: 2 + completetime: 0 + miningPoints: 18 materials: - RawGold: 500 + RawGold: 100 - type: latheRecipe id: IngotSilver1 result: IngotSilver1 - completetime: 2 + completetime: 0 + miningPoints: 16 materials: - RawSilver: 500 + RawSilver: 100 - type: latheRecipe id: SheetPlastic result: SheetPlastic1 applyMaterialDiscount: false - completetime: 2 + completetime: 0 materials: Plastic: 100 - type: latheRecipe id: MaterialBananium1 result: MaterialBananium1 - completetime: 2 + completetime: 0 + miningPoints: 60 materials: - RawBananium: 500 + RawBananium: 100 - type: latheRecipe id: MaterialSheetMeat diff --git a/Resources/Prototypes/Recipes/Lathes/tools.yml b/Resources/Prototypes/Recipes/Lathes/tools.yml index 03d8a8511e3..79d47728a4f 100644 --- a/Resources/Prototypes/Recipes/Lathes/tools.yml +++ b/Resources/Prototypes/Recipes/Lathes/tools.yml @@ -207,15 +207,6 @@ Plastic: 150 Gold: 50 -- type: latheRecipe - id: MiningDrill - result: MiningDrill - category: Tools - completetime: 3 - materials: - Steel: 500 - Plastic: 100 - - type: latheRecipe id: WelderExperimental result: WelderExperimental diff --git a/Resources/Prototypes/Recipes/Reactions/fun.yml b/Resources/Prototypes/Recipes/Reactions/fun.yml index 4e838816f20..5a5dfa986f4 100644 --- a/Resources/Prototypes/Recipes/Reactions/fun.yml +++ b/Resources/Prototypes/Recipes/Reactions/fun.yml @@ -185,3 +185,19 @@ effects: - !type:CreateEntityReactionEffect entity: ShardCrystalRandom + +- type: reaction + id: Gunpowder + impact: Low + quantized: true + minTemp: 374 + reactants: + Potassium: + amount: 6 + Sulfur: + amount: 2 + Charcoal: + amount: 2 + effects: + - !type:CreateEntityReactionEffect + entity: MaterialGunpowder \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Reactions/medicine.yml b/Resources/Prototypes/Recipes/Reactions/medicine.yml index 2e9b1d4f854..a99015fe90b 100644 --- a/Resources/Prototypes/Recipes/Reactions/medicine.yml +++ b/Resources/Prototypes/Recipes/Reactions/medicine.yml @@ -636,3 +636,29 @@ catalyst: true products: Happiness: 4 + +- type: reaction + id: PotassiumIodide + reactants: + Potassium: + amount: 1 + Iodine: + amount: 1 + products: + PotassiumIodide: 2 + +- type: reaction + id: Haloperidol + reactants: + Aluminium: + amount: 1 + Chlorine: + amount: 1 + Fluorine: + amount: 1 + Oil: + amount: 1 + PotassiumIodide: + amount: 1 + products: + Haloperidol: 5 diff --git a/Resources/Prototypes/Research/experimental.yml b/Resources/Prototypes/Research/experimental.yml index 60017fc98d1..423ec0f84da 100644 --- a/Resources/Prototypes/Research/experimental.yml +++ b/Resources/Prototypes/Research/experimental.yml @@ -69,6 +69,7 @@ cost: 7500 recipeUnlocks: - ClothingShoesBootsMagSci + - ClothingShoesBootsMoon - type: technology id: AnomalyCoreHarnessing diff --git a/Resources/Prototypes/Research/industrial.yml b/Resources/Prototypes/Research/industrial.yml index c2b05a18abe..99e000d37fc 100644 --- a/Resources/Prototypes/Research/industrial.yml +++ b/Resources/Prototypes/Research/industrial.yml @@ -11,9 +11,10 @@ cost: 7500 recipeUnlocks: - MiningDrill + - WeaponGrapplingGun - BorgModuleMining + - BorgModuleGrapplingGun - OreProcessorIndustrialMachineCircuitboard - - OreBagOfHolding - ClothingMaskWeldingGas - SalvageExpeditionsComputerCircuitboard @@ -108,19 +109,6 @@ - RipleyPeripheralsElectronics - MechEquipmentGrabber -- type: technology - id: Grappling - name: research-technology-grappling - icon: - sprite: Objects/Weapons/Guns/Launchers/grappling_gun.rsi - state: base - discipline: Industrial - tier: 1 - cost: 5000 - recipeUnlocks: - - WeaponGrapplingGun - - BorgModuleGrapplingGun - # Tier 2 - type: technology @@ -182,6 +170,19 @@ - BorgModuleAdvancedTool - BorgModuleRCD +- type: technology + id: MassExcavation + name: research-technology-excavation + icon: + sprite: Objects/Tools/handdrilldiamond.rsi + state: handdrill + discipline: Industrial + tier: 2 + cost: 12500 + recipeUnlocks: + - OreBagOfHolding + - MiningDrillDiamond + - type: technology id: MechanizedSalvaging name: research-technology-mechanized-salvaging diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml index 05b0553c78f..504b6384835 100644 --- a/Resources/Prototypes/Roles/Antags/traitor.yml +++ b/Resources/Prototypes/Roles/Antags/traitor.yml @@ -4,6 +4,43 @@ antagonist: true setPreference: true objective: roles-antag-syndicate-agent-objective - requirements: - - !type:CharacterOverallTimeRequirement # DeltaV - Playtime requirement - min: 86400 # DeltaV - 24 hours \ No newline at end of file + +- type: antag + id: TraitorSleeper + name: roles-antag-syndicate-agent-sleeper-name + antagonist: true + setPreference: true + objective: roles-antag-syndicate-agent-sleeper-objective + +# Syndicate Operative Outfit - Monkey +- type: startingGear + id: SyndicateOperativeGearMonkey + equipment: + head: ClothingHeadHatOutlawHat + jumpsuit: ClothingUniformJumpsuitOperative + mask: CigaretteSyndicate + +# Syndicate Operative Outfit - Barratry +- type: startingGear + id: SyndicateOperativeGearExtremelyBasic + equipment: + jumpsuit: ClothingUniformJumpsuitOperative + back: ClothingBackpackSyndicate + shoes: ClothingShoesBootsCombatFilled + gloves: ClothingHandsGlovesColorBlack + storage: + back: + - BoxSurvivalSyndicate + - WeaponPistolViper + - PinpointerSyndicateNuclear + - DeathAcidifierImplanter + +#Syndicate Operative Outfit - Basic +- type: startingGear + id: SyndicateOperativeGearBasic + parent: SyndicateOperativeGearExtremelyBasic + equipment: + ears: ClothingHeadsetAltSyndicate + gloves: ClothingHandsGlovesCombat + pocket1: BaseUplinkRadio40TC + id: SyndiPDA diff --git a/Resources/Prototypes/Roles/Jobs/CentComm/cburn.yml b/Resources/Prototypes/Roles/Jobs/CentComm/cburn.yml new file mode 100644 index 00000000000..5f5e3138d15 --- /dev/null +++ b/Resources/Prototypes/Roles/Jobs/CentComm/cburn.yml @@ -0,0 +1,14 @@ +- type: job + id: CBURN + name: job-name-cburn + description: job-description-cburn + playTimeTracker: JobCBURN + setPreference: false + startingGear: CBURNGear + icon: "JobIconNanotrasen" + supervisors: job-supervisors-centcom + canBeAntag: false + accessGroups: + - AllAccess + access: + - CentralCommand \ No newline at end of file diff --git a/Resources/Prototypes/Roles/Jobs/CentComm/deathsquad.yml b/Resources/Prototypes/Roles/Jobs/CentComm/deathsquad.yml new file mode 100644 index 00000000000..f400806ad4e --- /dev/null +++ b/Resources/Prototypes/Roles/Jobs/CentComm/deathsquad.yml @@ -0,0 +1,14 @@ +- type: job + id: DeathSquad + name: job-name-deathsquad + description: job-description-deathsquad + playTimeTracker: JobDeathSquad + setPreference: false + startingGear: DeathSquadGear + icon: "JobIconNanotrasen" + supervisors: job-supervisors-centcom + canBeAntag: false + accessGroups: + - AllAccess + access: + - CentralCommand \ No newline at end of file diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index 22b8cfd48ac..8cc8f5c6a72 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -17,7 +17,7 @@ - !type:AddComponentSpecial components: - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: #literally just picked semi random valus. i tested this once and tweaked it. Blunt: 5 Piercing: 4 diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index 7e4d4e19a9e..30b9a69d12f 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -27,7 +27,7 @@ - !type:CharacterWhitelistRequirement weight: 20 startingGear: CaptainGear - icon: "JobIconCaptain" + icon: JobIconCaptain requireAdminNotify: true joinNotifyCrew: true supervisors: job-supervisors-centcom diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index 7dd3e6823ae..b2cf3b87930 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -17,6 +17,17 @@ satchel: ClothingBackpackSatchelFilled duffelbag: ClothingBackpackDuffelFilled +# Syndicate Operative Outfit - Civilian +- type: startingGear + id: SyndicateOperativeGearCivilian + equipment: + jumpsuit: ClothingUniformJumpsuitSyndieFormal + back: ClothingBackpackDuffelSyndicate + shoes: ClothingShoesBootsCombat + gloves: ClothingHandsGlovesColorBlack + id: SyndiPDA + ears: ClothingHeadsetAltSyndicate + #Space Ninja Outfit - type: startingGear id: SpaceNinjaGear @@ -58,51 +69,6 @@ belt: ClothingBeltMilitaryWebbingMedFilled innerClothingSkirt: ClothingUniformJumpskirtColorBlack -# Syndicate Operative Outfit - Monkey -- type: startingGear - id: SyndicateOperativeGearMonkey - equipment: - head: ClothingHeadHatOutlawHat - jumpsuit: ClothingUniformJumpsuitOperative - mask: CigaretteSyndicate - innerClothingSkirt: ClothingUniformJumpsuitOperative - -# Syndicate Operative Outfit - Barratry -- type: startingGear - id: SyndicateOperativeGearExtremelyBasic - equipment: - jumpsuit: ClothingUniformJumpsuitOperative - back: ClothingBackpackDuffelSyndicateOperative - shoes: ClothingShoesBootsCombatFilled - gloves: ClothingHandsGlovesColorBlack - innerClothingSkirt: ClothingUniformJumpsuitOperative - satchel: ClothingBackpackDuffelSyndicateOperative - duffelbag: ClothingBackpackDuffelSyndicateOperative - -# Syndicate Operative Outfit - Civilian -- type: startingGear - id: SyndicateOperativeGearCivilian - equipment: - jumpsuit: ClothingUniformJumpsuitSyndieFormal - back: ClothingBackpackDuffelSyndicate - shoes: ClothingShoesBootsCombat - gloves: ClothingHandsGlovesColorBlack - -#Syndicate Operative Outfit - Basic -- type: startingGear - id: SyndicateOperativeGearBasic - equipment: - jumpsuit: ClothingUniformJumpsuitOperative - back: ClothingBackpackDuffelSyndicateOperative - ears: ClothingHeadsetAltSyndicate - gloves: ClothingHandsGlovesCombat - shoes: ClothingShoesBootsCombatFilled - pocket1: BaseUplinkRadio40TC - id: SyndiPDA - innerClothingSkirt: ClothingUniformJumpsuitOperative - satchel: ClothingBackpackDuffelSyndicateOperative - duffelbag: ClothingBackpackDuffelSyndicateOperative - #Syndicate Operative Outfit - Full Kit - type: startingGear id: SyndicateOperativeGearFull diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index 39a9ec5046b..ee57445cdf3 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -18,15 +18,6 @@ min: 43200 # DeltaV - 12 hours - !type:CharacterOverallTimeRequirement min: 72000 # DeltaV - 20 hours - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - inverted: true - species: - - Shadowkin - - !type:CharacterTraitRequirement - traits: - - ShadowkinBlackeye weight: 10 startingGear: CMOGear icon: "JobIconChiefMedicalOfficer" diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index bf8a83c7e8d..46d91ee00ee 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -19,15 +19,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye weight: 10 startingGear: ResearchDirectorGear icon: "JobIconResearchDirector" diff --git a/Resources/Prototypes/Roles/Jobs/departments.yml b/Resources/Prototypes/Roles/Jobs/departments.yml index 55aeafde6d8..9dca079c504 100644 --- a/Resources/Prototypes/Roles/Jobs/departments.yml +++ b/Resources/Prototypes/Roles/Jobs/departments.yml @@ -48,6 +48,17 @@ - HeadOfSecurity - ResearchDirector - Quartermaster + - ChiefJustice # DeltaV - chief justice is in command staff + - CentralCommandOfficial + - CBURN + - ERTLeader + - ERTChaplain + - ERTJanitor + - ERTMedical + - ERTSecurity + - ERTEngineer + - DeathSquad + - AdministrativeAssistant # Delta V - Administrative Assistant, see Resources/Prototypes/Nyanotrasen/Roles/Jobs/Command/admin_assistant.yml primary: false weight: 100 diff --git a/Resources/Prototypes/Roles/play_time_trackers.yml b/Resources/Prototypes/Roles/play_time_trackers.yml index a0abb1a1070..fd99b2c2288 100644 --- a/Resources/Prototypes/Roles/play_time_trackers.yml +++ b/Resources/Prototypes/Roles/play_time_trackers.yml @@ -168,3 +168,9 @@ - type: playTimeTracker id: JobZookeeper + +- type: playTimeTracker + id: JobDeathSquad + +- type: playTimeTracker + id: JobCBURN \ No newline at end of file diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index 41d04137bbe..108b9f778b5 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -57,6 +57,11 @@ kind: source path: "/Textures/Shaders/drunk.swsl" +- type: shader + id: Drowsiness + kind: source + path: "/Textures/Shaders/radial_blur.swsl" + - type: shader id: Texture kind: source diff --git a/Resources/Prototypes/Stacks/Materials/materials.yml b/Resources/Prototypes/Stacks/Materials/materials.yml index 0a05a899648..95afb0cd1ab 100644 --- a/Resources/Prototypes/Stacks/Materials/materials.yml +++ b/Resources/Prototypes/Stacks/Materials/materials.yml @@ -92,4 +92,17 @@ icon: { sprite: /Textures/Objects/Materials/materials.rsi, state: bones} spawn: MaterialBones1 maxCount: 30 - itemSize: 1 + +- type: stack + id: Gunpowder + name: gunpowder + icon: { sprite: /Textures/Objects/Misc/reagent_fillings.rsi, state: powderpile } + spawn: MaterialGunpowder + maxCount: 60 + +- type: stack + id: GoliathHide + name: goliath hide + icon: { sprite: /Textures/Objects/Materials/hide.rsi, state: goliath_hide } + spawn: MaterialGoliathHide1 + maxCount: 30 \ No newline at end of file diff --git a/Resources/Prototypes/Stacks/Materials/ore.yml b/Resources/Prototypes/Stacks/Materials/ore.yml index cf7fcb04836..cf01903aca0 100644 --- a/Resources/Prototypes/Stacks/Materials/ore.yml +++ b/Resources/Prototypes/Stacks/Materials/ore.yml @@ -4,7 +4,13 @@ icon: { sprite: /Textures/Objects/Materials/ore.rsi, state: gold } spawn: GoldOre1 maxCount: 30 - itemSize: 2 + +- type: stack + id: DiamondOre + name: rough diamond + icon: { sprite: /Textures/Objects/Materials/ore.rsi, state: diamond } + spawn: DiamondOre1 + maxCount: 30 - type: stack id: SteelOre diff --git a/Resources/Prototypes/StatusEffects/health.yml b/Resources/Prototypes/StatusIcon/StatusEffects/health.yml similarity index 86% rename from Resources/Prototypes/StatusEffects/health.yml rename to Resources/Prototypes/StatusIcon/StatusEffects/health.yml index 073b03a2aeb..de1b6684809 100644 --- a/Resources/Prototypes/StatusEffects/health.yml +++ b/Resources/Prototypes/StatusIcon/StatusEffects/health.yml @@ -1,32 +1,32 @@ -- type: statusIcon +- type: healthIcon id: HealthIcon abstract: true priority: 3 locationPreference: Left isShaded: true -- type: statusIcon +- type: healthIcon parent: HealthIcon id: HealthIconFine icon: sprite: /Textures/Interface/Misc/health_icons.rsi state: Fine -- type: statusIcon +- type: healthIcon id: HealthIconCritical parent: HealthIcon icon: sprite: /Textures/Interface/Misc/health_icons.rsi state: Critical -- type: statusIcon +- type: healthIcon id: HealthIconDead parent: HealthIcon icon: sprite: /Textures/Interface/Misc/health_icons.rsi state: Dead -- type: statusIcon +- type: healthIcon id: HealthIconRotting parent: HealthIcon icon: diff --git a/Resources/Prototypes/StatusEffects/hunger.yml b/Resources/Prototypes/StatusIcon/StatusEffects/satiation.yml similarity index 84% rename from Resources/Prototypes/StatusEffects/hunger.yml rename to Resources/Prototypes/StatusIcon/StatusEffects/satiation.yml index 9af246e06ee..749d1f88f18 100644 --- a/Resources/Prototypes/StatusEffects/hunger.yml +++ b/Resources/Prototypes/StatusIcon/StatusEffects/satiation.yml @@ -1,26 +1,26 @@ #Hunger -- type: statusIcon +- type: satiationIcon id: HungerIcon abstract: true priority: 5 locationPreference: Right isShaded: true -- type: statusIcon +- type: satiationIcon id: HungerIconOverfed parent: HungerIcon icon: sprite: /Textures/Interface/Misc/food_icons.rsi state: overfed -- type: statusIcon +- type: satiationIcon id: HungerIconPeckish parent: HungerIcon icon: sprite: /Textures/Interface/Misc/food_icons.rsi state: peckish -- type: statusIcon +- type: satiationIcon id: HungerIconStarving parent: HungerIcon icon: @@ -28,28 +28,28 @@ state: starving #Thirst -- type: statusIcon +- type: satiationIcon id: ThirstIcon abstract: true priority: 5 locationPreference: Left isShaded: true -- type: statusIcon +- type: satiationIcon id: ThirstIconOverhydrated parent: ThirstIcon icon: sprite: /Textures/Interface/Misc/food_icons.rsi state: overhydrated -- type: statusIcon +- type: satiationIcon id: ThirstIconThirsty parent: ThirstIcon icon: sprite: /Textures/Interface/Misc/food_icons.rsi state: thirsty -- type: statusIcon +- type: satiationIcon id: ThirstIconParched parent: ThirstIcon icon: diff --git a/Resources/Prototypes/StatusEffects/ssd.yml b/Resources/Prototypes/StatusIcon/StatusEffects/ssd.yml similarity index 84% rename from Resources/Prototypes/StatusEffects/ssd.yml rename to Resources/Prototypes/StatusIcon/StatusEffects/ssd.yml index 70253cc6b19..1d04a5349ef 100644 --- a/Resources/Prototypes/StatusEffects/ssd.yml +++ b/Resources/Prototypes/StatusIcon/StatusEffects/ssd.yml @@ -1,4 +1,4 @@ -- type: statusIcon +- type: ssdIcon id: SSDIcon icon: sprite: /Textures/Effects/ssd.rsi diff --git a/Resources/Prototypes/StatusIcon/debug.yml b/Resources/Prototypes/StatusIcon/debug.yml index 2011c6ceeae..24981746e7e 100644 --- a/Resources/Prototypes/StatusIcon/debug.yml +++ b/Resources/Prototypes/StatusIcon/debug.yml @@ -1,17 +1,17 @@ -- type: statusIcon +- type: debugIcon id: DebugStatus icon: sprite: /Textures/Interface/Misc/research_disciplines.rsi state: civilianservices -- type: statusIcon +- type: debugIcon id: DebugStatus2 priority: 1 icon: sprite: /Textures/Interface/Misc/research_disciplines.rsi state: arsenal -- type: statusIcon +- type: debugIcon id: DebugStatus3 priority: 5 icon: diff --git a/Resources/Prototypes/StatusIcon/antag.yml b/Resources/Prototypes/StatusIcon/faction.yml similarity index 62% rename from Resources/Prototypes/StatusIcon/antag.yml rename to Resources/Prototypes/StatusIcon/faction.yml index 0dbdfce4f97..016dec5de3c 100644 --- a/Resources/Prototypes/StatusIcon/antag.yml +++ b/Resources/Prototypes/StatusIcon/faction.yml @@ -1,42 +1,49 @@ -- type: statusIcon +- type: factionIcon id: ZombieFaction priority: 11 + showTo: + components: + - ShowAntagIcons + - Zombie + - InitialInfected icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Zombie -- type: statusIcon +- type: factionIcon id: InitialInfectedFaction priority: 11 + showTo: + components: + - ShowAntagIcons + - InitialInfected icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: InitialInfected -- type: statusIcon +- type: factionIcon id: RevolutionaryFaction priority: 11 + showTo: + components: + - ShowAntagIcons + - Revolutionary icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Revolutionary -- type: statusIcon +- type: factionIcon id: HeadRevolutionaryFaction priority: 11 + showTo: + components: + - ShowAntagIcons + - Revolutionary icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: HeadRevolutionary -- type: statusIcon - id: MindShieldIcon - priority: 2 - locationPreference: Right - layer: Mod - isShaded: true - icon: - sprite: /Textures/Interface/Misc/job_icons.rsi - state: MindShield - -- type: statusIcon +- type: factionIcon id: SyndicateFaction priority: 0 locationPreference: Left diff --git a/Resources/Prototypes/StatusEffects/job.yml b/Resources/Prototypes/StatusIcon/job.yml similarity index 72% rename from Resources/Prototypes/StatusEffects/job.yml rename to Resources/Prototypes/StatusIcon/job.yml index 96ad930bd51..17c9cee8045 100644 --- a/Resources/Prototypes/StatusEffects/job.yml +++ b/Resources/Prototypes/StatusIcon/job.yml @@ -1,377 +1,438 @@ -- type: statusIcon +- type: jobIcon id: JobIcon abstract: true priority: 1 locationPreference: Right isShaded: true -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconDetective icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Detective + jobName: job-name-detective -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconQuarterMaster icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: QuarterMaster + jobName: job-name-qm -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconBorg icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Borg + jobName: job-name-borg -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconBotanist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Botanist + jobName: job-name-botanist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconBoxer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Boxer + jobName: job-name-boxer -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconAtmosphericTechnician icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: AtmosphericTechnician + jobName: job-name-atmostech -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconNanotrasen icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Nanotrasen + jobName: job-name-centcomoff -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconPrisoner icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Prisoner + jobName: job-name-prisoner -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconJanitor icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Janitor + jobName: job-name-janitor -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconChemist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Chemist + jobName: job-name-chemist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconStationEngineer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: StationEngineer + jobName: job-name-engineer -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconSecurityOfficer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: SecurityOfficer + jobName: job-name-security -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconNoId icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: NoId + jobName: job-name-no-id -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconChiefMedicalOfficer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: ChiefMedicalOfficer + jobName: job-name-cmo -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconRoboticist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Roboticist + jobName: job-name-roboticist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconChaplain icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi # DeltaV - Move Chaplain into Epistemics state: Chaplain # DeltaV - Move Chaplain into Epistemics + jobName: job-name-chaplain -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconLawyer icon: sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi # DeltaV - Move Lawyer into Justice state: Lawyer # DeltaV - Move Lawyer into Justice + jobName: job-name-lawyer -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconUnknown icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Unknown + jobName: job-name-unknown -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconLibrarian icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Librarian + jobName: job-name-librarian -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconCargoTechnician icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: CargoTechnician + jobName: job-name-cargotech -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconScientist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Scientist + jobName: job-name-scientist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconResearchAssistant icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: ResearchAssistant + jobName: job-name-research-assistant -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconGeneticist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Geneticist + jobName: job-name-geneticist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconClown icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Clown + jobName: job-name-clown -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconCaptain icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Captain + jobName: job-name-captain -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconHeadOfPersonnel icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: HeadOfPersonnel + jobName: job-name-hop -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconVirologist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Virologist + jobName: job-name-virologist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconShaftMiner icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: ShaftMiner + jobName: job-name-salvagespec -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconPassenger icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Passenger + jobName: job-name-passenger -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconChiefEngineer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: ChiefEngineer + jobName: job-name-ce -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconBartender icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Bartender + jobName: job-name-bartender -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconHeadOfSecurity icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: HeadOfSecurity + jobName: job-name-hos -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconBrigmedic icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Brigmedic + jobName: job-name-brigmedic -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMedicalDoctor icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: MedicalDoctor + jobName: job-name-doctor -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconParamedic icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Paramedic + jobName: job-name-paramedic -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconChef icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Chef + jobName: job-name-chef -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconWarden icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Warden + jobName: job-name-warden -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconResearchDirector icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: ResearchDirector + jobName: job-name-rd -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMime icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Mime + jobName: job-name-mime -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMusician icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Musician + jobName: job-name-musician -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconReporter icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Reporter + jobName: job-name-reporter -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconPsychologist icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Psychologist + jobName: job-name-psychologist -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconMedicalIntern icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: MedicalIntern + jobName: job-name-intern -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconTechnicalAssistant icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: TechnicalAssistant + jobName: job-name-technical-assistant -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconServiceWorker icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: ServiceWorker + jobName: job-name-serviceworker -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconSecurityCadet icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: SecurityCadet + jobName: job-name-cadet -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconZombie # This is a perfectly legitimate profession to pursue icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Zombie + jobName: job-name-zombie -- type: statusIcon +- type: jobIcon + parent: JobIcon + id: JobIconSyndicate # Just in case you want to make it official which side you are on + icon: + sprite: /Textures/Interface/Misc/job_icons.rsi + state: Syndicate + jobName: job-name-syndicate + +- type: jobIcon parent: JobIcon id: JobIconZookeeper icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Zookeeper + jobName: job-name-zookeeper -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconSeniorPhysician icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorPhysician + allowSelection: false -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconSeniorOfficer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorOfficer + allowSelection: false -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconSeniorEngineer icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorEngineer + allowSelection: false -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconSeniorResearcher icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorResearcher + allowSelection: false -- type: statusIcon +- type: jobIcon parent: JobIcon id: JobIconVisitor icon: sprite: /Textures/Interface/Misc/job_icons.rsi state: Visitor + jobName: job-name-visitor diff --git a/Resources/Prototypes/StatusEffects/security.yml b/Resources/Prototypes/StatusIcon/security.yml similarity index 71% rename from Resources/Prototypes/StatusEffects/security.yml rename to Resources/Prototypes/StatusIcon/security.yml index 31a22e5df36..e360ec11eb6 100644 --- a/Resources/Prototypes/StatusEffects/security.yml +++ b/Resources/Prototypes/StatusIcon/security.yml @@ -1,41 +1,51 @@ -- type: statusIcon +- type: securityIcon id: SecurityIcon abstract: true offset: 1 locationPreference: Left isShaded: true -- type: statusIcon +- type: securityIcon parent: SecurityIcon id: SecurityIconDischarged icon: sprite: /Textures/Interface/Misc/security_icons.rsi state: hud_discharged -- type: statusIcon +- type: securityIcon parent: SecurityIcon id: SecurityIconIncarcerated icon: sprite: /Textures/Interface/Misc/security_icons.rsi state: hud_incarcerated -- type: statusIcon +- type: securityIcon parent: SecurityIcon id: SecurityIconParoled icon: sprite: /Textures/Interface/Misc/security_icons.rsi state: hud_paroled -- type: statusIcon +- type: securityIcon parent: SecurityIcon id: SecurityIconSuspected icon: sprite: /Textures/Interface/Misc/security_icons.rsi state: hud_suspected -- type: statusIcon +- type: securityIcon parent: SecurityIcon id: SecurityIconWanted icon: sprite: /Textures/Interface/Misc/security_icons.rsi state: hud_wanted + +- type: securityIcon + id: MindShieldIcon + priority: 2 + locationPreference: Right + layer: Mod + isShaded: true + icon: + sprite: /Textures/Interface/Misc/job_icons.rsi + state: MindShield diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml index 489145813e2..4b2976200f4 100644 --- a/Resources/Prototypes/Store/categories.yml +++ b/Resources/Prototypes/Store/categories.yml @@ -35,7 +35,7 @@ #uplink categoires - type: storeCategory - id: UplinkWeapons + id: UplinkWeaponry name: store-category-weapons priority: 0 @@ -50,67 +50,56 @@ priority: 2 - type: storeCategory - id: UplinkMisc - name: store-category-misc + id: UplinkWearables + name: store-category-wearables priority: 3 - type: storeCategory - id: UplinkBundles - name: store-category-bundles + id: UplinkChemicals + name: store-category-chemicals priority: 4 - type: storeCategory - id: UplinkTools - name: store-category-tools + id: UplinkDeception + name: store-category-deception priority: 5 - type: storeCategory - id: UplinkUtility - name: store-category-utility + id: UplinkDisruption + name: store-category-disruption priority: 6 - type: storeCategory id: UplinkImplants name: store-category-implants - priority: 6 - -- type: storeCategory - id: UplinkJob - name: store-category-job priority: 7 - type: storeCategory - id: UplinkArmor - name: store-category-armor + id: UplinkAllies + name: store-category-allies priority: 8 - type: storeCategory - id: UplinkPointless - name: store-category-pointless + id: UplinkJob + name: store-category-job priority: 9 - type: storeCategory - id: UplinkDeception - name: store-category-deception + id: UplinkPointless + name: store-category-pointless priority: 10 -- type: storeCategory - id: UplinkChemicals - name: store-category-chemicals - priority: 11 - -- type: storeCategory - id: UplinkDisruption - name: store-category-disruption - priority: 12 - - type: storeCategory id: UplinkSales name: Sales - priority: 10 + priority: 11 #revenant - type: storeCategory id: RevenantAbilities name: store-category-abilities +- type: storeCategory + id: DiscountedItems + name: store-discounted-items + priority: 200 diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index 47768b68ec3..1e9f495530b 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -1,37 +1,38 @@ -- type: storePreset +- type: entity id: StorePresetUplink - storeName: Uplink - categories: - - UplinkWeapons - - UplinkAmmo - - UplinkExplosives - - UplinkMisc - - UplinkBundles - - UplinkTools - - UplinkUtility - - UplinkImplants - - UplinkJob - - UplinkArmor - - UplinkPointless - - UplinkSales - currencyWhitelist: - - Telecrystal - sales: - enabled: true - minMultiplier: 0.2 - maxMultiplier: 0.8 - minItems: 3 - maxItems: 8 - salesCategory: UplinkSales + abstract: true + components: + - type: Store + name: store-preset-name-uplink + categories: + - UplinkWeaponry + - UplinkAmmo + - UplinkExplosives + - UplinkWearables + - UplinkChemicals + - UplinkDeception + - UplinkDisruption + - UplinkImplants + - UplinkJob + - UplinkAllies + - UplinkPointless + - UplinkSales + currencyWhitelist: + - Telecrystal + balance: + Telecrystal: 0 -- type: storePreset +- type: entity id: StorePresetSpellbook - storeName: Spellbook - categories: - - SpellbookOffensive #Fireball, Rod Form - - SpellbookDefensive #Magic Missile, Wall of Force - - SpellbookUtility #Body Swap, Lich, Teleport, Knock, Polymorph - - SpellbookEquipment #Battlemage Robes, Staff of Locker - - SpellbookEvents #Summon Weapons, Summon Ghosts - currencyWhitelist: - - WizCoin + abstract: true + components: + - type: Store + name: store-preset-name-spellbook + categories: + - SpellbookOffensive #Fireball, Rod Form + - SpellbookDefensive #Magic Missile, Wall of Force + - SpellbookUtility #Body Swap, Lich, Teleport, Knock, Polymorph + - SpellbookEquipment #Battlemage Robes, Staff of Locker + - SpellbookEvents #Summon Weapons, Summon Ghosts + currencyWhitelist: + - WizCoin \ No newline at end of file diff --git a/Resources/Prototypes/Traits/disabilities.yml b/Resources/Prototypes/Traits/disabilities.yml index 83645e3a820..68877b01d27 100644 --- a/Resources/Prototypes/Traits/disabilities.yml +++ b/Resources/Prototypes/Traits/disabilities.yml @@ -270,7 +270,7 @@ - !type:TraitAddComponent components: - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 5 Piercing: 4 diff --git a/Resources/Prototypes/Traits/mental.yml b/Resources/Prototypes/Traits/mental.yml index 4dec0350f45..172872a6cdc 100644 --- a/Resources/Prototypes/Traits/mental.yml +++ b/Resources/Prototypes/Traits/mental.yml @@ -23,15 +23,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - !type:CharacterSpeciesRequirement inverted: true species: @@ -71,15 +62,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - !type:CharacterSpeciesRequirement inverted: true species: @@ -119,15 +101,10 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Shadowkin - !type:CharacterTraitRequirement inverted: true traits: @@ -163,15 +140,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - !type:CharacterTraitRequirement inverted: true traits: @@ -207,15 +175,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - !type:CharacterTraitRequirement inverted: true traits: @@ -251,15 +210,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - !type:CharacterTraitRequirement inverted: true traits: @@ -294,15 +244,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - !type:CharacterTraitRequirement inverted: true traits: diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml index 949b12a7aec..25603c7347d 100644 --- a/Resources/Prototypes/Traits/skills.yml +++ b/Resources/Prototypes/Traits/skills.yml @@ -288,15 +288,6 @@ - !type:CharacterTraitRequirement traits: - AnomalousPositronics - - !type:CharacterLogicOrRequirement - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin - - !type:CharacterTraitRequirement - inverted: true - traits: - - ShadowkinBlackeye - type: trait id: TrapAvoider diff --git a/Resources/Prototypes/Traits/species.yml b/Resources/Prototypes/Traits/species.yml index a3e29b37285..771a796eefa 100644 --- a/Resources/Prototypes/Traits/species.yml +++ b/Resources/Prototypes/Traits/species.yml @@ -67,16 +67,16 @@ - Swashbuckler - Spearmaster -- type: trait - id: ShadowkinBlackeye - category: Mental - points: 4 - functions: - - !type:TraitReplaceComponent - components: - - type: Shadowkin - blackeyeSpawn: true - requirements: - - !type:CharacterSpeciesRequirement - species: - - Shadowkin +# - type: trait +# id: ShadowkinBlackeye +# category: Mental +# points: 4 +# functions: +# - !type:TraitReplaceComponent +# components: +# - type: Shadowkin +# blackeyeSpawn: true +# requirements: +# - !type:CharacterSpeciesRequirement +# species: +# - Shadowkin diff --git a/Resources/Prototypes/WhiteDream/Entities/Actions/cultists.yml b/Resources/Prototypes/WhiteDream/Entities/Actions/cultists.yml index 45e6217c602..2c20947fc90 100644 --- a/Resources/Prototypes/WhiteDream/Entities/Actions/cultists.yml +++ b/Resources/Prototypes/WhiteDream/Entities/Actions/cultists.yml @@ -368,5 +368,4 @@ state: gauntlet_echo event: !type:ProjectileSpellEvent prototype: ProjectileGauntlet - projectileSpeed: 5 - type: BaseCultSpell diff --git a/Resources/Prototypes/WhiteDream/Entities/Objects/Weapons/Projectiles/cult.yml b/Resources/Prototypes/WhiteDream/Entities/Objects/Weapons/Projectiles/cult.yml index 69a007b90e3..08008a3c5fc 100644 --- a/Resources/Prototypes/WhiteDream/Entities/Objects/Weapons/Projectiles/cult.yml +++ b/Resources/Prototypes/WhiteDream/Entities/Objects/Weapons/Projectiles/cult.yml @@ -1,12 +1,12 @@ - type: entity + parent: BaseBulletTrigger id: ProjectileGauntlet name: gauntlet description: Oh no. - parent: BaseBulletTrigger categories: [ HideSpawnMenu ] components: - type: PointLight - color: Red + color: "#FF0000" radius: 2.0 energy: 5.0 - type: Projectile @@ -16,5 +16,4 @@ Blunt: 50 - type: Sprite sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi - layers: - - state: gauntlet_echo + state: gauntlet_echo \ No newline at end of file diff --git a/Resources/Prototypes/WhiteDream/StatusIcon/antag.yml b/Resources/Prototypes/WhiteDream/StatusIcon/antag.yml index 9910ed6f4c0..102a9e8787f 100644 --- a/Resources/Prototypes/WhiteDream/StatusIcon/antag.yml +++ b/Resources/Prototypes/WhiteDream/StatusIcon/antag.yml @@ -1,15 +1,27 @@ -- type: statusIcon +- type: factionIcon id: BloodCultMember priority: 11 locationPreference: left + showTo: + components: + - ShowAntagIcons + - Construct + - BloodCultist + - BloodCultLeader icon: sprite: /Textures/WhiteDream/BloodCult/cult_hud.rsi state: cult_member -- type: statusIcon +- type: factionIcon id: BloodCultLeader priority: 11 locationPreference: left + showTo: + components: + - ShowAntagIcons + - Construct + - BloodCultist + - BloodCultLeader icon: sprite: /Textures/WhiteDream/BloodCult/cult_hud.rsi - state: cult_leader + state: cult_leader \ No newline at end of file diff --git a/Resources/Prototypes/Wires/layouts.yml b/Resources/Prototypes/Wires/layouts.yml index cf76e0ea610..9fd6bddc37f 100644 --- a/Resources/Prototypes/Wires/layouts.yml +++ b/Resources/Prototypes/Wires/layouts.yml @@ -149,3 +149,11 @@ - !type:DelayWireAction - !type:ProceedWireAction - !type:BoomWireAction + +# TODO: AI +- type: wireLayout + id: ShopVendor + wires: +# - !type:AiInteractWireAction + - !type:PowerWireAction + - !type:AccessWireAction \ 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 84df09af33d..a8967080579 100644 --- a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml +++ b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml @@ -220,7 +220,7 @@ - type: Tool qualities: - Screwing - speed: 2 # Very powerful multitool to balance out the desire to sell or scrap for points + speedModifier: 2 # Very powerful multitool to balance out the desire to sell or scrap for points useSound: /Audio/Items/drill_use.ogg - type: Tag tags: diff --git a/Resources/Prototypes/_Shitmed/Entities/Surgery/surgeries.yml b/Resources/Prototypes/_Shitmed/Entities/Surgery/surgeries.yml index cd20e14a2fd..775716ecff4 100644 --- a/Resources/Prototypes/_Shitmed/Entities/Surgery/surgeries.yml +++ b/Resources/Prototypes/_Shitmed/Entities/Surgery/surgeries.yml @@ -608,7 +608,7 @@ - type: OhioAccent - type: RatvarianLanguage - type: Clumsy - clumsyDamage: # Placeholder values to be able to initialize the component + gunShootFailDamage: # Placeholder values to be able to initialize the component types: Blunt: 0 inverse: true @@ -635,7 +635,7 @@ - type: OhioAccent - type: RatvarianLanguage - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 0 - type: SurgeryPartCondition diff --git a/Resources/Prototypes/_Shitmed/Entities/Surgery/surgery_steps.yml b/Resources/Prototypes/_Shitmed/Entities/Surgery/surgery_steps.yml index 97a693445ac..af6f603ea6b 100644 --- a/Resources/Prototypes/_Shitmed/Entities/Surgery/surgery_steps.yml +++ b/Resources/Prototypes/_Shitmed/Entities/Surgery/surgery_steps.yml @@ -526,7 +526,7 @@ - type: OhioAccent - type: RatvarianLanguage - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 5 Piercing: 4 @@ -557,7 +557,7 @@ - type: OhioAccent - type: RatvarianLanguage - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 0 - type: Sprite diff --git a/Resources/Prototypes/explosion.yml b/Resources/Prototypes/explosion.yml index 7ef2e774a71..492b7525914 100644 --- a/Resources/Prototypes/explosion.yml +++ b/Resources/Prototypes/explosion.yml @@ -133,3 +133,19 @@ fireColor: Green texturePath: /Textures/Effects/fire_greyscale.rsi fireStates: 3 + +- type: explosion + id: FireBomb + damagePerIntensity: + types: + Heat: 1 + Blunt: 2 + Piercing: 3 + lightColor: Orange + texturePath: /Textures/Effects/fire.rsi + fireStates: 6 + fireStacks: 2 + +# STOP +# BEFORE YOU ADD MORE EXPLOSION TYPES CONSIDER IF AN EXISTING ONE IS SUITABLE +# ADDING NEW ONES IS PROHIBITIVELY EXPENSIVE diff --git a/Resources/Prototypes/ore.yml b/Resources/Prototypes/ore.yml index 41eeedcb5f3..8845e993ac5 100644 --- a/Resources/Prototypes/ore.yml +++ b/Resources/Prototypes/ore.yml @@ -58,6 +58,12 @@ minOreYield: 1 maxOreYield: 3 +- type: ore + id: OreDiamond + oreEntity: DiamondOre1 + minOreYield: 1 + maxOreYield: 2 + - type: ore id: OreQuartzCrab oreEntity: MobSpawnCrabQuartz diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index bed635c7026..2bfa7ef7a0f 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -62,3 +62,9 @@ - type: statusEffect id: PhaseShifted + +- type: statusEffect + id: RadiationProtection + +- type: statusEffect + id: Drowsiness #blurs your vision and makes you randomly fall asleep diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index e7f20f8f47b..f0fab89f6a9 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -31,9 +31,6 @@ - type: Tag id: ATVKeys -- type: Tag - id: Baguette - - type: Tag id: Balloon @@ -227,7 +224,7 @@ id: BoxHug - type: Tag - id: Burnt + id: Brain - type: Tag id: BrassInstrument @@ -250,9 +247,21 @@ - type: Tag id: Bucket +- type: Tag + id: Burger + - type: Tag id: BulletFoam +- type: Tag + id: BulletRubber + +- type: Tag + id: Burnt + +- type: Tag + id: Bun + - type: Tag id: BypassDropChecks @@ -262,6 +271,9 @@ - type: Tag id: CableCoil +- type: Tag + id: Cake + - type: Tag id: CaneBlade @@ -371,6 +383,13 @@ - type: Tag id: Chicken +- type: Tag + id: Cheese + +# Allowed to control someone wearing a Chef's hat if inside their hat. +- type: Tag + id: ChefPilot + - type: Tag id: ChemDispensable # container that can go into the chem dispenser @@ -431,6 +450,9 @@ - type: Tag id: Cow +- type: Tag + id: Crab + - type: Tag id: Crayon @@ -542,6 +564,9 @@ - type: Tag id: DrinkCan +- type: Tag + id: DrinkGlass + - type: Tag id: DrinkSpaceGlue @@ -647,6 +672,9 @@ - type: Tag id: Goat +- type: Tag + id: Goliath + - type: Tag id: GPS @@ -767,6 +795,9 @@ - type: Tag id: Igniter +- type: Tag + id: Ingredient + - type: Tag #Drop this innate tool instead of deleting it. id: InnateDontDelete @@ -952,6 +983,9 @@ - type: Tag id: NozzleBackTank +- type: Tag + id: Nugget # for chicken nuggets + - type: Tag id: NukeOpsUplink @@ -1139,6 +1173,9 @@ - type: Tag id: ShellShotgun +- type: Tag + id: ShellShotgunLight # shotgun shells that are compatible with the flare gun. + - type: Tag id: Shiv @@ -1157,6 +1194,9 @@ - type: Tag id: Skewer +- type: Tag + id: Slice # sliced fruit, vegetables, pizza etc. + - type: Tag id: SmallAIChip @@ -1244,6 +1284,9 @@ - type: Tag id: TabletopBoard +- type: Tag + id: Taco + - type: Tag id: TabletopPiece diff --git a/Resources/Prototypes/whitelists.yml b/Resources/Prototypes/whitelists.yml new file mode 100644 index 00000000000..8a99aa30eab --- /dev/null +++ b/Resources/Prototypes/whitelists.yml @@ -0,0 +1,7 @@ +- type: playerConnectionWhitelist + id: basicWhitelist # Basic whitelist using only the ManualWhitelist condition + conditions: + - !type:ConditionManualWhitelistMembership + action: Allow # Allow connection if matched + - !type:ConditionAlwaysMatch # Condition that always matches + action: Deny # Deny connection if matched diff --git a/Resources/Prototypes/wizardsDenWhitelists.yml b/Resources/Prototypes/wizardsDenWhitelists.yml new file mode 100644 index 00000000000..c21d01e5a89 --- /dev/null +++ b/Resources/Prototypes/wizardsDenWhitelists.yml @@ -0,0 +1,39 @@ +# This is the whitelist used for Wizard's Den Salamander + +- type: playerConnectionWhitelist + id: salamanderMrpWhitelist + conditions: + - !type:ConditionManualBlacklistMembership # Deny blacklisted (MRP ban) + action: Deny + - !type:ConditionNotesPlaytimeRange # Deny for high severity notes in the last 30 days + includeExpired: false + minimumSeverity: 3 # High + minimumNotes: 1 + range: 30 # 30 days + action: Deny + includeSecret: false + - !type:ConditionNotesPlaytimeRange # Deny for >=2 medium severity notes in the last 14 days + includeExpired: false + minimumSeverity: 2 # Medium + minimumNotes: 1 + range: 14 # 14 Days + action: Deny + includeSecret: false + - !type:ConditionNotesPlaytimeRange # Deny for >=3 low severity notes in the last 14 days + includeExpired: false + minimumSeverity: 1 # Low + minimumNotes: 3 + range: 14 # 14 Days + action: Deny + includeSecret: false + - !type:ConditionManualWhitelistMembership # Allow whitelisted players + action: Allow + - !type:ConditionPlayerCount # Allow when <= 15 players are online + minimumPlayers: 0 + maximumPlayers: 15 + action: Allow + #- !type:ConditionPlaytime + # minimumPlaytime: 1200 # 20 hours to be whitelisted + # action: Deny + - !type:ConditionAlwaysMatch + action: Deny diff --git a/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml b/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml index 45d41631c2e..bb4d0eb4432 100644 --- a/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml +++ b/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml @@ -14,47 +14,29 @@ <Box> <GuideEntityEmbed Entity="Telecrystal" Caption="Telecrystals"/> </Box> - - [color=#a4885c]Telecrystals[/color] are given at the start to give traitors an edge on the station. Other traitors can trade their [color=#a4885c]telecrystals[/color] to each other exceeding the normal given amount. - - Using your [color=#a4885c]PDA[/color] and setting the ringtone as your uplink code gives you a variety of options to use at your disposal against the station and its crew. - <Box> - <GuideEntityEmbed Entity="PassengerPDA" Caption="PDA"/> - </Box> - Make sure to relock your [color=#a4885c]PDA[/color] to prevent anyone else from seeing it! - - Various gear include: - <Box> - <GuideEntityEmbed Entity="WeaponPistolViper" Caption="Guns"/> - <GuideEntityEmbed Entity="FireAxe" State="icon" Caption="Melee"/> - <GuideEntityEmbed Entity="C4" Caption=" Explosives"/> - <GuideEntityEmbed Entity="Syringe" Caption="Implants"/> - <GuideEntityEmbed Entity="ClothingBackpackDuffelSyndicate" Caption="Bundles"/> - </Box> + Traitors are antagonists employed by the [color=#ff0000]Syndicate.[/color] You are a sleeper agent who has access to various tools and weapons through your [bold]uplink[/bold]. + You also receive [bold]codewords[/bold] to identify other agents, and a coordinated team of traitors can have [italic]brutal results.[/italic] - ## Objectives + Anyone besides [textlink="department heads" link="Command"] or members of [textlink="Security" link="Security"] can be a traitor. - - When becoming a Traitor, you will have a list of objectives, ranging from escape alive, stealing something, and killing someone. Using the [color=#a4885c]Uplink[/color] will help you with most of these tasks. + ## Uplink & Activation + The [color=cyan]uplink[/color] is your most important tool as a traitor. You can exchange the 20 [color=red]telecrystals[/color] (TC) you start with for items that will help you with your objectives. - ## List of Possible Tasks + By pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color], you'll see your personal uplink code. [bold]Setting your PDA's ringtone as this code will open the uplink.[/bold] + Pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color] also lets you view your objectives and the codewords. - - Kill or maroon a randomly selected non-traitor. - - Kill or maroon a randomly selected traitor. - - Kill or maroon a randomly selected department head. - - Keep a randomly selected traitor alive. - - Escape on the evacuation shuttle alive and uncuffed. - - Help a randomly selected traitor finish 2/3 of their objectives. - - Die a glorious death. - - Steal the Captain's [color=#a4885c]ID Card[/color]. - <Box> - <GuideEntityEmbed Entity="CaptainIDCard" Caption="Captain's ID Card"/> - </Box> - - Steal the Captain's [color=#a4885c]Antique Laser Pistol[/color]. - <Box> - <GuideEntityEmbed Entity="WeaponAntiqueLaser" Caption="Captain's Antique Laser Pistol"/> - </Box> - - Steal the Captain's [color=#a4885c]Jetpack[/color]. + If you do not have a PDA when you are activated, an [color=cyan]uplink implant[/color] is provided [bold]for the full [color=red]TC[/color] price of the implant.[/bold] + It can be accessed from your hotbar. <Box> - <GuideEntityEmbed Entity="JetpackCaptainFilled" Caption="Captain's Jetpack"/> + <GuideEntityEmbed Entity="PassengerPDA" Caption="PDA"/> + <GuideEntityEmbed Entity="Telecrystal" Caption="Telecrystals"/> + <GuideEntityEmbed Entity="BaseUplinkRadio" Caption="Uplink Implant"/> </Box> - - Steal the Chief Medical Officer's [color=#a4885c]Hypospray[/color]. + + [bold]Make sure to close your PDA uplink to prevent anyone else from seeing it.[/bold] You don't want [color=#cb0000]Security[/color] to get their hands on this premium selection of contraband! + + Implanted uplinks are not normally accessible to other people, so they do not have any security measures. They can, however, be removed from you with an empty implanter. + <Box> <GuideEntityEmbed Entity="Hypospray" Caption="CMO's Hypospray"/> </Box> @@ -66,10 +48,6 @@ <Box> <GuideEntityEmbed Entity="HandTeleporter" Caption="Hand Teleporter"/> </Box> - - Steal the Head of Security's [color=#a4885c]Secret Documents[/color]. - <Box> - <GuideEntityEmbed Entity="BookSecretDocuments" Caption="Secret Documents"/> - </Box> - Steal the Head of Security's [color=#a4885c]X-01 MultiPhase Energy Gun[/color]. <Box> <GuideEntityEmbed Entity="WeaponEnergyGunMultiphase" Caption="Head of Security's personal Energy Gun"/> @@ -78,6 +56,10 @@ <Box> <GuideEntityEmbed Entity="ClothingShoesBootsMagAdv" Caption="Advanced Magboots"/> </Box> + - Stealing the [color=#cb0000]Head of Security[/color]'s [bold]energy shotgun[/bold]. + <Box> + <GuideEntityEmbed Entity="WeaponEnergyShotgun" Caption=""/> + </Box> - Steal the [color=#a4885c]Nuke Disk[/color]. <Box> <GuideEntityEmbed Entity="NukeDisk" Caption="Nuke Disk"/> diff --git a/Resources/ServerInfo/Guidebook/Medical/Chemist.xml b/Resources/ServerInfo/Guidebook/Medical/Chemist.xml index 05b853d1880..caced42bdf9 100644 --- a/Resources/ServerInfo/Guidebook/Medical/Chemist.xml +++ b/Resources/ServerInfo/Guidebook/Medical/Chemist.xml @@ -10,7 +10,7 @@ While chemists primarily make medications, there's a very wide variety of chemic <GuideEntityEmbed Entity="ChemDispenser"/> <GuideEntityEmbed Entity="ChemBag"/> <GuideEntityEmbed Entity="KitchenReagentGrinder"/> -<GuideEntityEmbed Entity="chem_master"/> +<GuideEntityEmbed Entity="ChemMaster"/> </Box> <Box> <GuideEntityEmbed Entity="LargeBeaker"/> diff --git a/Resources/ServerInfo/Guidebook/Medical/Medical.xml b/Resources/ServerInfo/Guidebook/Medical/Medical.xml index 22a4149833b..5924a569eb5 100644 --- a/Resources/ServerInfo/Guidebook/Medical/Medical.xml +++ b/Resources/ServerInfo/Guidebook/Medical/Medical.xml @@ -17,6 +17,6 @@ Right now, medbay is divided into two different sets of care: <GuideEntityEmbed Entity="ChemDispenser"/> <GuideEntityEmbed Entity="LargeBeaker"/> <GuideEntityEmbed Entity="PillCanister"/> -<GuideEntityEmbed Entity="chem_master"/> +<GuideEntityEmbed Entity="ChemMaster"/> </Box> </Document> diff --git a/Resources/ServerInfo/Guidebook/Service/Bartender.xml b/Resources/ServerInfo/Guidebook/Service/Bartender.xml index b7599fc0d1c..fd54bdeef0d 100644 --- a/Resources/ServerInfo/Guidebook/Service/Bartender.xml +++ b/Resources/ServerInfo/Guidebook/Service/Bartender.xml @@ -7,7 +7,7 @@ The materials for these drinks can be found in various places: </Box> <Box> <GuideEntityEmbed Entity="BoozeDispenser"/> -<GuideEntityEmbed Entity="soda_dispenser"/> +<GuideEntityEmbed Entity="SodaDispenser"/> <GuideEntityEmbed Entity="VendingMachineBooze"/> </Box> diff --git a/Resources/ServerInfo/Guidebook/Service/Chef.xml b/Resources/ServerInfo/Guidebook/Service/Chef.xml index 7d077ca269b..370c1db5c3f 100644 --- a/Resources/ServerInfo/Guidebook/Service/Chef.xml +++ b/Resources/ServerInfo/Guidebook/Service/Chef.xml @@ -51,7 +51,6 @@ Ask Botany for what you need, without a botanist, you may need to grow more plan <GuideEntityEmbed Entity="SignHydro1" Caption=" "/> <GuideEntityEmbed Entity="hydroponicsTray"/> <GuideEntityEmbed Entity="WheatBushel"/> -<GuideEntityEmbed Entity="SignHydro2" Caption=" "/> </Box> ## Gathering Milk: Alt-Click on a Cow with a container in your hand. (Beakers, Buckets, Milk Jugs, ect.) diff --git a/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/equipped-HELMET.png new file mode 100644 index 00000000000..bfd047d7cf6 Binary files /dev/null and b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/equipped-HELMET.png differ diff --git a/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/icon.png b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/icon.png new file mode 100644 index 00000000000..6161c05f120 Binary files /dev/null and b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/inhand-left.png new file mode 100644 index 00000000000..6f8c72da91e Binary files /dev/null and b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/inhand-right.png new file mode 100644 index 00000000000..6a4eb8bbe3a Binary files /dev/null and b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/meta.json b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/meta.json new file mode 100644 index 00000000000..302f4371e8c --- /dev/null +++ b/Resources/Textures/Clothing/Head/Hats/holyhatmelon.rsi/meta.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/b459ea3fdee965bdc3e93e7983ad7fa610d05c12 and https://github.com/tgstation/tgstation/commit/ead6d8d59753ef033efdfad17f337df268038ff3 and modified by ravage", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-HELMET", + "directions": 4, + "delays": [ + [ + 0.5, + 0.5, + 0.5, + 0.5 + ], + [ + 0.5, + 0.5, + 0.5, + 0.5 + ], + [ + 0.5, + 0.5, + 0.5, + 0.5 + ], + [ + 0.5, + 0.5, + 0.5, + 0.5 + ] + ] + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png new file mode 100644 index 00000000000..1b4db3d093f Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png new file mode 100644 index 00000000000..d1353c49800 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png new file mode 100644 index 00000000000..5e002cadcfe Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png new file mode 100644 index 00000000000..71bdd72fc6f Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png new file mode 100644 index 00000000000..93d0e26ddd9 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png new file mode 100644 index 00000000000..1da86e3c821 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png new file mode 100644 index 00000000000..3ce2895a397 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png new file mode 100644 index 00000000000..ba713302762 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json b/Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json new file mode 100644 index 00000000000..62072e7107e --- /dev/null +++ b/Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/vgstation-coders/vgstation13/blob/HEAD/icons/obj/clothing/masks.dmi. Vox and Reptilian edits by EmoGarbage404 (Github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "icon-joy" + }, + { + "name": "icon-despair" + }, + { + "name": "icon-anger" + }, + { + "name": "equipped-MASK", + "directions": 4 + }, + { + "name": "equipped-MASK-vox", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/equipped-FEET-vox.png b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/equipped-FEET-vox.png new file mode 100644 index 00000000000..a65deca7604 Binary files /dev/null and b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/equipped-FEET-vox.png differ diff --git a/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/equipped-FEET.png b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/equipped-FEET.png new file mode 100644 index 00000000000..2b4cdf400dd Binary files /dev/null and b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/equipped-FEET.png differ diff --git a/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/icon.png b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/icon.png new file mode 100644 index 00000000000..04f662d6d31 Binary files /dev/null and b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/inhand-left.png b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/inhand-left.png new file mode 100644 index 00000000000..03bdacf9fb5 Binary files /dev/null and b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/inhand-right.png b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/inhand-right.png new file mode 100644 index 00000000000..f00d861ca54 Binary files /dev/null and b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/meta.json b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/meta.json new file mode 100644 index 00000000000..2c2d49e0310 --- /dev/null +++ b/Resources/Textures/Clothing/Shoes/Boots/moonboots.rsi/meta.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Tau Ceti Station at commit https://github.com/TauCetiStation/TauCetiClassic/blob/HEAD/icons/obj/clothing/shoes.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-FEET", + "directions": 4 + }, + { + "name": "equipped-FEET-vox", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/alt-equipped-EARS.png b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/alt-equipped-EARS.png new file mode 100644 index 00000000000..61bdec48727 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/alt-equipped-EARS.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/equipped-EARS.png b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/equipped-EARS.png new file mode 100644 index 00000000000..0000ae7a0d1 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/equipped-EARS.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/icon.png b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/icon.png new file mode 100644 index 00000000000..286be81e6e0 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/icon.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/icon_alt.png b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/icon_alt.png new file mode 100644 index 00000000000..200d2b62e46 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/icon_alt.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/meta.json b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/meta.json new file mode 100644 index 00000000000..d91acee23ca --- /dev/null +++ b/Resources/Textures/DeltaV/Clothing/Ears/Headsets/adminassistant.rsi/meta.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "made by Radezolid, alt headset is a modified version of the alt service headset (Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/f8f4aeda930fcd0805ca4cc76d9bc9412a5b3428. Modified by TJohnson.)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "icon_alt" + }, + { + "name": "equipped-EARS", + "directions": 4 + }, + { + "name": "alt-equipped-EARS", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/equipped-INNERCLOTHING-monkey.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/equipped-INNERCLOTHING-monkey.png new file mode 100644 index 00000000000..59791ed5fad Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/equipped-INNERCLOTHING-monkey.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/equipped-INNERCLOTHING.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/equipped-INNERCLOTHING.png new file mode 100644 index 00000000000..ff304f00fd3 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/equipped-INNERCLOTHING.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/icon.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/icon.png new file mode 100644 index 00000000000..f085ca55dbd Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/icon.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/inhand-left.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/inhand-left.png new file mode 100644 index 00000000000..15c30a04914 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/inhand-left.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/inhand-right.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/inhand-right.png new file mode 100644 index 00000000000..53512e6969e Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/inhand-right.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/meta.json b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/meta.json new file mode 100644 index 00000000000..29605f8216a --- /dev/null +++ b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpskirt/admin_assistant.rsi/meta.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039. In hand sprite scaled down by potato1234_x, monkey made by brainfood1183 (github) for ss14, tie changed by noctyrnal (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-INNERCLOTHING", + "directions": 4 + }, + { + "name": "equipped-INNERCLOTHING-monkey", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/equipped-INNERCLOTHING-monkey.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/equipped-INNERCLOTHING-monkey.png new file mode 100644 index 00000000000..59791ed5fad Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/equipped-INNERCLOTHING-monkey.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/equipped-INNERCLOTHING.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/equipped-INNERCLOTHING.png new file mode 100644 index 00000000000..5b3a50b9744 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/equipped-INNERCLOTHING.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/icon.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/icon.png new file mode 100644 index 00000000000..f085ca55dbd Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/icon.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/inhand-left.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/inhand-left.png new file mode 100644 index 00000000000..15c30a04914 Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/inhand-left.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/inhand-right.png b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/inhand-right.png new file mode 100644 index 00000000000..53512e6969e Binary files /dev/null and b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/inhand-right.png differ diff --git a/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/meta.json b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/meta.json new file mode 100644 index 00000000000..29605f8216a --- /dev/null +++ b/Resources/Textures/DeltaV/Clothing/Uniforms/Jumpsuit/admin_assistant.rsi/meta.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039. In hand sprite scaled down by potato1234_x, monkey made by brainfood1183 (github) for ss14, tie changed by noctyrnal (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-INNERCLOTHING", + "directions": 4 + }, + { + "name": "equipped-INNERCLOTHING-monkey", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/AdminAssistant.png b/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/AdminAssistant.png new file mode 100644 index 00000000000..1f45dff992a Binary files /dev/null and b/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/AdminAssistant.png differ diff --git a/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/meta.json b/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/meta.json index 09d18e3ab7c..53dfda149f8 100644 --- a/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/meta.json +++ b/Resources/Textures/DeltaV/Interface/Misc/job_icons.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/d917f4c2a088419d5c3aec7656b7ff8cebd1822e | nyanoPrisonGuard, nyanoMartialArtist, nyanoGladiator made by Floofers | ChiefJustice, Clerk by leonardo_dabepis (Discord)", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/d917f4c2a088419d5c3aec7656b7ff8cebd1822e | nyanoPrisonGuard, nyanoMartialArtist, nyanoGladiator made by Floofers | ChiefJustice, Clerk by leonardo_dabepis (Discord), SecurityBorg recoloured from MedicalBorg by DangerRevolution(github), CargoAssistant recoloured from MedicalIntern by Radezolid, AdminAssistant made by noctyrnal (github)", "size": { "x": 8, "y": 8 @@ -39,6 +39,9 @@ }, { "name": "Lawyer" + }, + { + "name": "AdminAssistant" } ] } diff --git a/Resources/Textures/DeltaV/Interface/VerbIcons/ATTRIBUTION.txt b/Resources/Textures/DeltaV/Interface/VerbIcons/ATTRIBUTION.txt new file mode 100644 index 00000000000..c4cc0324d56 --- /dev/null +++ b/Resources/Textures/DeltaV/Interface/VerbIcons/ATTRIBUTION.txt @@ -0,0 +1,2 @@ +bell.svg taken from https://coreui.io/icons/ +Licensed under CC BY 4.0 diff --git a/Resources/Textures/DeltaV/Interface/VerbIcons/bell.svg b/Resources/Textures/DeltaV/Interface/VerbIcons/bell.svg new file mode 100644 index 00000000000..4274db2113f --- /dev/null +++ b/Resources/Textures/DeltaV/Interface/VerbIcons/bell.svg @@ -0,0 +1,5 @@ +<!-- Generated by IcoMoon.io --> +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> +<title>bell</title> +<path d="M21.106 16.339l-2.047-3.779v-3.935c0-3.929-3.196-7.125-7.125-7.125s-7.125 3.196-7.125 7.125v3.935l-2.047 3.779c-0.085 0.155-0.136 0.339-0.136 0.536 0 0.621 0.504 1.125 1.125 1.125h4.075c-0.011 0.124-0.017 0.249-0.017 0.375 0 2.278 1.847 4.125 4.125 4.125s4.125-1.847 4.125-4.125v0c0-0.126-0.006-0.251-0.017-0.375h4.075c0 0 0 0 0 0 0.621 0 1.125-0.504 1.125-1.125 0-0.196-0.050-0.381-0.139-0.542l0.003 0.006zM14.559 18.375c0 0.001 0 0.001 0 0.002 0 1.45-1.175 2.625-2.625 2.625s-2.625-1.175-2.625-2.625c0-0.133 0.010-0.264 0.029-0.392l-0.002 0.014h5.196c0.017 0.113 0.027 0.243 0.027 0.375v0zM4.381 16.5l1.928-3.56v-4.315c0-3.107 2.518-5.625 5.625-5.625s5.625 2.518 5.625 5.625v0 4.315l1.928 3.56z"></path> +</svg> diff --git a/Resources/Textures/DeltaV/Interface/VerbIcons/bell.svg.png b/Resources/Textures/DeltaV/Interface/VerbIcons/bell.svg.png new file mode 100644 index 00000000000..7fada9e5afd Binary files /dev/null and b/Resources/Textures/DeltaV/Interface/VerbIcons/bell.svg.png differ diff --git a/Resources/Textures/DeltaV/Interface/VerbIcons/bell_muted.png b/Resources/Textures/DeltaV/Interface/VerbIcons/bell_muted.png new file mode 100644 index 00000000000..0d0ba804285 Binary files /dev/null and b/Resources/Textures/DeltaV/Interface/VerbIcons/bell_muted.png differ diff --git a/Resources/Textures/DeltaV/Markers/jobs.rsi/adminassistant.png b/Resources/Textures/DeltaV/Markers/jobs.rsi/adminassistant.png new file mode 100644 index 00000000000..cf335a8a10d Binary files /dev/null and b/Resources/Textures/DeltaV/Markers/jobs.rsi/adminassistant.png differ diff --git a/Resources/Textures/DeltaV/Markers/jobs.rsi/meta.json b/Resources/Textures/DeltaV/Markers/jobs.rsi/meta.json index a7534b9ee5b..c6f636f987a 100644 --- a/Resources/Textures/DeltaV/Markers/jobs.rsi/meta.json +++ b/Resources/Textures/DeltaV/Markers/jobs.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "made by Floofers", + "copyright": "all unless stated made by Floofers, adminassistant made by noctyrnal (github)", "size": { "x": 32, "y": 32 @@ -45,6 +45,9 @@ }, { "name": "mobster" + }, + { + "name": "adminassistant" } ] } diff --git a/Resources/Textures/DeltaV/Misc/program_icons.rsi/meta.json b/Resources/Textures/DeltaV/Misc/program_icons.rsi/meta.json new file mode 100644 index 00000000000..96c74d48aa2 --- /dev/null +++ b/Resources/Textures/DeltaV/Misc/program_icons.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC0-1.0", + "copyright": "stock_trading made by Malice, nanochat made by kushbreth (discord)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "stock_trading" + }, + { + "name": "nanochat" + } + ] +} diff --git a/Resources/Textures/DeltaV/Misc/program_icons.rsi/nanochat.png b/Resources/Textures/DeltaV/Misc/program_icons.rsi/nanochat.png new file mode 100644 index 00000000000..2126d34cfa1 Binary files /dev/null and b/Resources/Textures/DeltaV/Misc/program_icons.rsi/nanochat.png differ diff --git a/Resources/Textures/DeltaV/Misc/program_icons.rsi/stock_trading.png b/Resources/Textures/DeltaV/Misc/program_icons.rsi/stock_trading.png new file mode 100644 index 00000000000..251b46a3f83 Binary files /dev/null and b/Resources/Textures/DeltaV/Misc/program_icons.rsi/stock_trading.png differ diff --git a/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/cart-chat.png b/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/cart-chat.png new file mode 100644 index 00000000000..80a921c94de Binary files /dev/null and b/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/cart-chat.png differ diff --git a/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/cart-stonk.png b/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/cart-stonk.png new file mode 100644 index 00000000000..ddfed6e915c Binary files /dev/null and b/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/cart-stonk.png differ diff --git a/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/meta.json index 4a4ba3352f8..ba96751650a 100644 --- a/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/meta.json +++ b/Resources/Textures/DeltaV/Objects/Devices/cartridge.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Timfa, plus edits by portfiend", + "copyright": "cart-chat made by kushbreth (discord), cart-cri, cart-mail, cart-stonk made by Monotheonist (github), edited from cart-log, cart-nav & cart-med cartridges; cart-log made by Skarletto (github), cart-nav, cart-med made by ArchRBX (github)", "size": { "x": 32, "y": 32 @@ -12,6 +12,12 @@ }, { "name": "cart-mail" + }, + { + "name": "cart-chat" + }, + { + "name": "cart-stonk" } ] } diff --git a/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/meta.json b/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/meta.json index 176af077199..02848c30cf2 100644 --- a/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/meta.json +++ b/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/59f2a4e10e5ba36033c9734ddebfbbdc6157472d | pda-corpsman from yogstation at https://github.com/yogstation13/Yogstation/commit/a75671b22476ed8e117229f38501b9b63f8d6bc1 | pda-martialartist by Floofers | pda-chiefjustice and pda-clerk by leonardo_dabepis (Discord) | pda-prosecutor by Timemaster99 (Discord)", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/59f2a4e10e5ba36033c9734ddebfbbdc6157472d | pda-corpsman from yogstation at https://github.com/yogstation13/Yogstation/commit/a75671b22476ed8e117229f38501b9b63f8d6bc1 | pda-martialartist by Floofers | pda-chiefjustice and pda-clerk by leonardo_dabepis (Discord) | pda-prosecutor by Timemaster99 (Discord) | admin assistant by Radezolid", "size": { "x": 32, "y": 32 @@ -40,6 +40,9 @@ }, { "name": "pda-prosecutor" + }, + { + "name": "pda-admin-assistant" } ] -} +} \ No newline at end of file diff --git a/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/pda-admin-assistant.png b/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/pda-admin-assistant.png new file mode 100644 index 00000000000..dd0e5967721 Binary files /dev/null and b/Resources/Textures/DeltaV/Objects/Devices/pda.rsi/pda-admin-assistant.png differ diff --git a/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/meta.json b/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/meta.json index 5e2c34d5304..6a76e9403d4 100644 --- a/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/meta.json +++ b/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/meta.json @@ -1,14 +1,17 @@ { - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432 | modified by Floofers. Stamp icon taken from tgstation at https://github.com/tgstation/tgstation/commit/fb1012102257b7b0a08d861fd2b8ba963c416e93, modified by leonardo_dabepis (Discord)", - "size": { - "x": 32, - "y": 32 - }, - "states": [ - { - "name": "folder-hop-ian" - } - ] + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432 | modified by Floofers. Stamp icon taken from tgstation at https://github.com/tgstation/tgstation/commit/fb1012102257b7b0a08d861fd2b8ba963c416e93, modified by leonardo_dabepis (Discord), paper_stamp-admin-assistant by noctyrnal (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "folder-hop-ian" + }, + { + "name": "paper_stamp-admin-assistant" + } + ] } diff --git a/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/paper_stamp-admin-assistant.png b/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/paper_stamp-admin-assistant.png new file mode 100644 index 00000000000..cd882ce1602 Binary files /dev/null and b/Resources/Textures/DeltaV/Objects/Misc/bureaucracy.rsi/paper_stamp-admin-assistant.png differ diff --git a/Resources/Textures/DeltaV/Objects/Misc/id_cards.rsi/idadminassistant.png b/Resources/Textures/DeltaV/Objects/Misc/id_cards.rsi/idadminassistant.png new file mode 100644 index 00000000000..0b51c9fac27 Binary files /dev/null and b/Resources/Textures/DeltaV/Objects/Misc/id_cards.rsi/idadminassistant.png differ diff --git a/Resources/Textures/DeltaV/Objects/Misc/id_cards.rsi/meta.json b/Resources/Textures/DeltaV/Objects/Misc/id_cards.rsi/meta.json new file mode 100644 index 00000000000..14db9866c0a --- /dev/null +++ b/Resources/Textures/DeltaV/Objects/Misc/id_cards.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/d917f4c2a088419d5c3aec7656b7ff8cebd1822e, idadminassistant by noctyrnal (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "idadminassistant" + } + ] +} diff --git a/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/meta.json b/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/meta.json index d17e01e8bd9..da46cba07a2 100644 --- a/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/meta.json +++ b/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Stamp sprites taken from tgstation at commit https://github.com/tgstation/tgstation/commit/fb1012102257b7b0a08d861fd2b8ba963c416e93, modified by Guess-My-Name. CJ stamp modified by Timemaster99 (Discord)", + "copyright": "Stamp sprites taken from tgstation at commit https://github.com/tgstation/tgstation/commit/fb1012102257b7b0a08d861fd2b8ba963c416e93, modified by Guess-My-Name. CJ stamp modified by Timemaster99 (Discord), stamp-admin-assistant by noctyrnal (github)", "size": { "x": 32, "y": 32 @@ -18,6 +18,9 @@ }, { "name": "stamp-cj" + }, + { + "name": "stamp-admin-assistant" } ] } diff --git a/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/stamp-admin-assistant.png b/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/stamp-admin-assistant.png new file mode 100644 index 00000000000..1dc32cfafea Binary files /dev/null and b/Resources/Textures/DeltaV/Objects/Misc/stamps.rsi/stamp-admin-assistant.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath.png new file mode 100644 index 00000000000..5839a357960 Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_alert.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_alert.png new file mode 100644 index 00000000000..c4c0b8efbeb Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_alert.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_dead.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_dead.png new file mode 100644 index 00000000000..e6978b28909 Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_dead.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_preattack.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_preattack.png new file mode 100644 index 00000000000..743e9f35be4 Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_preattack.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_retract.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_retract.png new file mode 100644 index 00000000000..d0895ccd155 Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_retract.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_spawn.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_spawn.png new file mode 100644 index 00000000000..9172b0099ce Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_spawn.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_wiggle.png b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_wiggle.png new file mode 100644 index 00000000000..f7622134039 Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/goliath_tentacle_wiggle.png differ diff --git a/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/meta.json b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/meta.json new file mode 100644 index 00000000000..b9d3f2bd7fc --- /dev/null +++ b/Resources/Textures/Mobs/Aliens/Asteroid/goliath.rsi/meta.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/5a68c5f6d3b60ee82c06e0287d1eb8108d2e1fe2/icons/mob/lavaland/lavaland_monsters.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "goliath", + "directions": 4 + }, + { + "name": "goliath_alert", + "directions": 4, + "delays": [ + [ + 1, + 0.2, + 1, + 0.5 + ], + [ + 1, + 0.2, + 1, + 0.5 + ], + [ + 1, + 0.2, + 1, + 0.5 + ], + [ + 1, + 0.2, + 1, + 0.5 + ] + ] + }, + { + "name": "goliath_preattack", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "goliath_dead" + }, + { + "name": "goliath_tentacle_spawn", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.1 + ] + ] + }, + { + "name": "goliath_tentacle_wiggle", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "goliath_tentacle_retract", + "delays": [ + [ + 0.1, + 0.2, + 0.2, + 0.2 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/dead.png b/Resources/Textures/Mobs/Animals/snail.rsi/dead.png new file mode 100644 index 00000000000..bdaecdab58b Binary files /dev/null and b/Resources/Textures/Mobs/Animals/snail.rsi/dead.png differ diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/meta.json b/Resources/Textures/Mobs/Animals/snail.rsi/meta.json new file mode 100644 index 00000000000..ca42ca9eed3 --- /dev/null +++ b/Resources/Textures/Mobs/Animals/snail.rsi/meta.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by Kezu (discord) & IProduceWidgets (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "snail", + "directions": 4 + }, + { + "name": "dead" + }, + { + "name": "spacesnail", + "directions": 4 + }, + { + "name": "spacedead" + }, + { + "name": "snoth", + "directions": 4 + }, + { + "name": "snothdead" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/snail.png b/Resources/Textures/Mobs/Animals/snail.rsi/snail.png new file mode 100644 index 00000000000..26d112c57ff Binary files /dev/null and b/Resources/Textures/Mobs/Animals/snail.rsi/snail.png differ diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/snoth.png b/Resources/Textures/Mobs/Animals/snail.rsi/snoth.png new file mode 100644 index 00000000000..5e634d12655 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/snail.rsi/snoth.png differ diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/snothdead.png b/Resources/Textures/Mobs/Animals/snail.rsi/snothdead.png new file mode 100644 index 00000000000..72962396080 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/snail.rsi/snothdead.png differ diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/spacedead.png b/Resources/Textures/Mobs/Animals/snail.rsi/spacedead.png new file mode 100644 index 00000000000..37cbde99fb0 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/snail.rsi/spacedead.png differ diff --git a/Resources/Textures/Mobs/Animals/snail.rsi/spacesnail.png b/Resources/Textures/Mobs/Animals/snail.rsi/spacesnail.png new file mode 100644 index 00000000000..3a2d97c6222 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/snail.rsi/spacesnail.png differ diff --git a/Resources/Textures/Mobs/Pets/hamlet.rsi/dead-0.png b/Resources/Textures/Mobs/Pets/hamlet.rsi/dead-0.png index cea6149d7ec..b0398783829 100644 Binary files a/Resources/Textures/Mobs/Pets/hamlet.rsi/dead-0.png and b/Resources/Textures/Mobs/Pets/hamlet.rsi/dead-0.png differ diff --git a/Resources/Textures/Mobs/Pets/hamlet.rsi/splat-0.png b/Resources/Textures/Mobs/Pets/hamlet.rsi/splat-0.png index f9ab920ac68..3f11164a42b 100644 Binary files a/Resources/Textures/Mobs/Pets/hamlet.rsi/splat-0.png and b/Resources/Textures/Mobs/Pets/hamlet.rsi/splat-0.png differ diff --git a/Resources/Textures/Mobs/Species/Human/organs.rsi/brain.png b/Resources/Textures/Mobs/Species/Human/organs.rsi/brain.png index 04d04890f6b..c4b6ac6385d 100644 Binary files a/Resources/Textures/Mobs/Species/Human/organs.rsi/brain.png and b/Resources/Textures/Mobs/Species/Human/organs.rsi/brain.png differ diff --git a/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json b/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json deleted file mode 100644 index db0ac608ed0..00000000000 --- a/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json +++ /dev/null @@ -1 +0,0 @@ -{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "https://github.com/discordia-space/CEV-Eris/raw/f7aa28fd4b4d0386c3393d829681ebca526f1d2d/icons/obj/drinks.dmi", "states": [{"name": "icon"}]} \ No newline at end of file diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-equipped-BELT.png b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-equipped-BELT.png new file mode 100644 index 00000000000..93437dd6587 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-equipped-BELT.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-inhand-left.png b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-inhand-left.png new file mode 100644 index 00000000000..085c8faf2e6 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-inhand-left.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-inhand-right.png b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-inhand-right.png new file mode 100644 index 00000000000..d737e6fa96b Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette-inhand-right.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette.png b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette.png index de137b2a626..e5de3c0bc09 100644 Binary files a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette.png and b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/baguette.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/meta.json index 29bd53c9f0a..2761d79f986 100644 --- a/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/Baked/bread.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and modified by potato1234x at https://github.com/tgstation/tgstation/commit/0631fe5bde73a68b4c12bdfa633c30b2cee442d5. Crostini created by Github user deathride58", + "copyright": "Taken from tgstation and modified by potato1234x at https://github.com/tgstation/tgstation/commit/0631fe5bde73a68b4c12bdfa633c30b2cee442d5. Crostini created by Github user deathride58, baguette taken from tgstation at commit https://github.com/tgstation/tgstation/commit/7ffd61b6fa6a6183daa8900f9a490f46f7a81955", "size": { "x": 32, "y": 32 @@ -34,6 +34,18 @@ { "name": "baguette" }, + { + "name": "baguette-equipped-BELT", + "directions": 4 + }, + { + "name": "baguette-inhand-left", + "directions": 4 + }, + { + "name": "baguette-inhand-right", + "directions": 4 + }, { "name": "banana" }, diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/croissant.png b/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/croissant.png new file mode 100644 index 00000000000..3922f8bdde8 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/croissant.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/meta.json index 7f887863aaf..56d8c81f100 100644 --- a/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and modified by Swept at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa. Chevrechaud created by Github user deathride58", + "copyright": "Taken from tgstation and modified by Swept at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa. Chevrechaud created by Github user deathride58, croissant taken from tgstation at commit https://github.com/tgstation/tgstation/commit/7ffd61b6fa6a6183daa8900f9a490f46f7a81955", "size": { "x": 32, "y": 32 @@ -123,6 +123,9 @@ }, { "name": "chevrechaud" + }, + { + "name": "croissant" } ] } diff --git a/Resources/Textures/Objects/Consumable/Food/bowl.rsi/escargot.png b/Resources/Textures/Objects/Consumable/Food/bowl.rsi/escargot.png new file mode 100644 index 00000000000..2ae8f5af72e Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/bowl.rsi/escargot.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/bowl.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/bowl.rsi/meta.json index 8e2c4444fc4..d6c3ed76766 100644 --- a/Resources/Textures/Objects/Consumable/Food/bowl.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/bowl.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and modified by Swept at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa. Fills created by potato1234_x", + "copyright": "Taken from tgstation and modified by Swept at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa. escargot from tgstation at https://github.com/tgstation/tgstation/commit/7ffd61b6fa6a6183daa8900f9a490f46f7a81955. Fills created by potato1234_x", "size": { "x": 32, "y": 32 @@ -66,6 +66,9 @@ 0.1 ] ] + }, + { + "name": "escargot" }, { "name": "eyeball" diff --git a/Resources/Textures/Objects/Consumable/Food/burger.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/burger.rsi/meta.json index 5a7ffa873f3..42c8c461e98 100644 --- a/Resources/Textures/Objects/Consumable/Food/burger.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/burger.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and modified by Swept and potato1234x at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa, ian.png created by EmoGarbage, mothroach.png created by TurboTracker", + "copyright": "Taken from tgstation and modified by Swept and potato1234x at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa, ian.png created by EmoGarbage, mothroach.png created by TurboTracker, screwed by TheShuEd", "size": { "x": 32, "y": 32 @@ -155,6 +155,9 @@ ] ] }, + { + "name": "screwed" + }, { "name": "spell" }, diff --git a/Resources/Textures/Objects/Consumable/Food/burger.rsi/screwed.png b/Resources/Textures/Objects/Consumable/Food/burger.rsi/screwed.png new file mode 100644 index 00000000000..d9a33730f64 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/burger.rsi/screwed.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/bun_bottom.png b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/bun_bottom.png new file mode 100644 index 00000000000..aec759f62a0 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/bun_bottom.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/bun_top.png b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/bun_top.png new file mode 100644 index 00000000000..8eb16ce1ff1 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/bun_top.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/cheese.png b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/cheese.png new file mode 100644 index 00000000000..2678c7f7d66 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/cheese.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/meta.json new file mode 100644 index 00000000000..2b9671e9b42 --- /dev/null +++ b/Resources/Textures/Objects/Consumable/Food/burger_sequence.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TheShuEd. Bun taken from tgstation and modified by Swept and potato1234x at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa, and edited by TheShuEd", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "bun_top" + }, + { + "name": "bun_bottom" + }, + { + "name": "cheese" + } + ] +} diff --git a/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/butter-slice.png b/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/butter-slice.png new file mode 100644 index 00000000000..81b73772a73 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/butter-slice.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/croissant-raw.png b/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/croissant-raw.png new file mode 100644 index 00000000000..be8a1e2a8f3 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/croissant-raw.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/meta.json index 6633b0b0bd6..2943d9dea33 100644 --- a/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/ingredients.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and baystation and modified by potato1234x at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24 and https://github.com/Baystation12/Baystation12/commit/a6067826de7fd8f698793f6d84e6c2f1f9b1f188. Tofu and tofu-slice were created by Discord user rosysyntax#6514. Chevrelog and chevredisk created by Github user deathride58, tortilladough tortillaflat and tortillaslice added by Phunny,", + "copyright": "Taken from tgstation and baystation and modified by potato1234x at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24 and https://github.com/Baystation12/Baystation12/commit/a6067826de7fd8f698793f6d84e6c2f1f9b1f188. Tofu and tofu-slice were created by Discord user rosysyntax#6514. Chevrelog and chevredisk created by Github user deathride58, tortilladough tortillaflat and tortillaslice added by Phunny, butter-slice and croissant-raw taken from tgstation at commit https://github.com/tgstation/tgstation/commit/7ffd61b6fa6a6183daa8900f9a490f46f7a81955", "size": { "x": 32, "y": 32 @@ -108,6 +108,12 @@ }, { "name": "tortilladough-slice" + }, + { + "name": "butter-slice" + }, + { + "name": "croissant-raw" } ] } diff --git a/Resources/Textures/Objects/Consumable/Food/meat.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/meat.rsi/meta.json index 824d3b96812..4ad75849d1a 100644 --- a/Resources/Textures/Objects/Consumable/Food/meat.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/meat.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and modified by Swept, potato1234x and deltanedas at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa", + "copyright": "Taken from tgstation and modified by Swept, potato1234x and deltanedas at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa, snail by IproduceWidgets (github) and Kezu (discord)", "size": { "x": 32, "y": 32 @@ -152,6 +152,12 @@ }, { "name": "slime" + }, + { + "name": "snail" + }, + { + "name": "snail-cooked" }, { "name": "snake" diff --git a/Resources/Textures/Objects/Consumable/Food/meat.rsi/snail-cooked.png b/Resources/Textures/Objects/Consumable/Food/meat.rsi/snail-cooked.png new file mode 100644 index 00000000000..81d8f43bbf9 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/meat.rsi/snail-cooked.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/meat.rsi/snail.png b/Resources/Textures/Objects/Consumable/Food/meat.rsi/snail.png new file mode 100644 index 00000000000..c3c7ffcef50 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/meat.rsi/snail.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/meat.rsi/tomato.png b/Resources/Textures/Objects/Consumable/Food/meat.rsi/tomato.png index 72242699f2f..0d749888b0e 100644 Binary files a/Resources/Textures/Objects/Consumable/Food/meat.rsi/tomato.png and b/Resources/Textures/Objects/Consumable/Food/meat.rsi/tomato.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/meta.json index 03fb0afdaea..d4e98e98bd8 100644 --- a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation and modified by Swept at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa", + "copyright": "Taken from tgstation and modified by Swept at https://github.com/tgstation/tgstation/commit/40d75cc340c63582fb66ce15bf75a36115f6bdaa, skewer-holymelon edited from skewer-watermelon by slarticodefast", "size": { "x": 32, "y": 32 @@ -11,94 +11,43 @@ "name": "skewer" }, { - "name": "skewer-meat1" + "name": "skewer-meat" }, { - "name": "skewer-meat2" + "name": "skewer-meat-alpha" }, { - "name": "skewer-meat3" + "name": "skewer-mushroom" }, { - "name": "skewer-meat4" + "name": "skewer-pepper" }, { - "name": "skewer-meat-alpha1" + "name": "skewer-bluepepper" }, { - "name": "skewer-meat-alpha2" + "name": "skewer-tomato" }, { - "name": "skewer-meat-alpha3" + "name": "skewer-corn" }, { - "name": "skewer-meat-alpha4" + "name": "skewer-tail" }, { - "name": "skewer-mushroom1" + "name": "skewer-tail-cooked" }, { - "name": "skewer-mushroom2" + "name": "skewer-rat" }, { - "name": "skewer-mushroom3" + "name": "skewer-snake" }, { - "name": "skewer-mushroom4" + "name": "skewer-watermelon" }, { - "name": "skewer-pepper1" - }, - { - "name": "skewer-pepper2" - }, - { - "name": "skewer-pepper3" - }, - { - "name": "skewer-pepper4" - }, - { - "name": "skewer-tomato1" - }, - { - "name": "skewer-tomato2" - }, - { - "name": "skewer-tomato3" - }, - { - "name": "skewer-tomato4" - }, - { - "name": "skewer-corn1" - }, - { - "name": "skewer-corn2" - }, - { - "name": "skewer-tail1" - }, - { - "name": "skewer-tail2" - }, - { - "name": "skewer-tail-cooked1" - }, - { - "name": "skewer-tail-cooked2" - }, - { - "name": "skewer-rat1" - }, - { - "name": "skewer-rat2" - }, - { - "name": "skewer-snake1" - }, - { - "name": "skewer-snake2" + "name": "skewer-holymelon" } ] } diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-bluepepper.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-bluepepper.png new file mode 100644 index 00000000000..4a4809ef4ae Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-bluepepper.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn.png new file mode 100644 index 00000000000..1d23efa6cd3 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn1.png deleted file mode 100644 index e1f8ec3cebf..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn2.png deleted file mode 100644 index 8dc4eec7c3a..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-corn2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-holymelon.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-holymelon.png new file mode 100644 index 00000000000..8bc88a45b8c Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-holymelon.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha.png new file mode 100644 index 00000000000..c35173308fc Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha1.png deleted file mode 100644 index 97e0d482fb0..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha2.png deleted file mode 100644 index 8d729f22e1a..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha3.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha3.png deleted file mode 100644 index 3f62a05be59..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha3.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha4.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha4.png deleted file mode 100644 index 4753c6a2f27..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat-alpha4.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat.png new file mode 100644 index 00000000000..d8989e080af Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat1.png deleted file mode 100644 index 8f1ae81e9b6..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat2.png deleted file mode 100644 index 1dbc77d261e..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat3.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat3.png deleted file mode 100644 index 39f48880185..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat3.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat4.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat4.png deleted file mode 100644 index a9f8ad789f7..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-meat4.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom.png new file mode 100644 index 00000000000..2b007414bb0 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom1.png deleted file mode 100644 index e2747afae3d..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom2.png deleted file mode 100644 index 505d9599e14..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom3.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom3.png deleted file mode 100644 index e9cbf32606f..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom3.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom4.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom4.png deleted file mode 100644 index 78f86e3a7b0..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-mushroom4.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper.png new file mode 100644 index 00000000000..bb1a0af50df Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper1.png deleted file mode 100644 index dac4665696b..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper2.png deleted file mode 100644 index 670b66ff9fe..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper3.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper3.png deleted file mode 100644 index 3c8b115ee83..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper3.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper4.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper4.png deleted file mode 100644 index d0d8caae3e0..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-pepper4.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat.png new file mode 100644 index 00000000000..10d7501bc3c Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat1.png deleted file mode 100644 index 3e41b5ea868..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat2.png deleted file mode 100644 index 36f7c5bfdbf..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-rat2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake.png new file mode 100644 index 00000000000..ad93db93542 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake1.png deleted file mode 100644 index a3a7ce808b9..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake2.png deleted file mode 100644 index aac0763b6ee..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-snake2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked.png new file mode 100644 index 00000000000..9b02bb0166b Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked1.png deleted file mode 100644 index 02e8069f3da..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked2.png deleted file mode 100644 index f40274d08bd..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail-cooked2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail.png new file mode 100644 index 00000000000..555e8678b5e Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail1.png deleted file mode 100644 index c8033c05bd0..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail2.png deleted file mode 100644 index 0227fd5470f..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tail2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato.png new file mode 100644 index 00000000000..46bbdc372dd Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato1.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato1.png deleted file mode 100644 index 39399183ee2..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato1.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato2.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato2.png deleted file mode 100644 index c3d3a70e02a..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato2.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato3.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato3.png deleted file mode 100644 index 1edf8de4c9f..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato3.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato4.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato4.png deleted file mode 100644 index cc79b96e784..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-tomato4.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-watermelon.png b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-watermelon.png new file mode 100644 index 00000000000..1e0146459e2 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/skewer.rsi/skewer-watermelon.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json index 6f699103840..4ff3230cae7 100644 --- a/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json @@ -1,12 +1,15 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24, chinese from paradise, ticket by peptide, cnds-trash based on boritos-trash and syndicakes modified by potato1234x", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24, chinese from paradise, ticket by peptide, cnds-trash based on boritos-trash and syndicakes modified by potato1234x, ramen from https://github.com/discordia-space/CEV-Eris/raw/f7aa28fd4b4d0386c3393d829681ebca526f1d2d/icons/obj/drinks.dmi", "size": { "x": 32, "y": 32 }, "states": [ + { + "name": "ramen" + }, { "name": "boritos" }, diff --git a/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/icon.png b/Resources/Textures/Objects/Consumable/Food/snacks.rsi/ramen.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/icon.png rename to Resources/Textures/Objects/Consumable/Food/snacks.rsi/ramen.png diff --git a/Resources/Textures/Objects/Consumable/Food/taco.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/taco.rsi/meta.json index 3e028b55c93..71bda2cd422 100644 --- a/Resources/Textures/Objects/Consumable/Food/taco.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/taco.rsi/meta.json @@ -7,9 +7,6 @@ "y": 32 }, "states": [ - { - "name": "tacoshell" - }, { "name": "beeftaco" }, @@ -29,4 +26,4 @@ "name": "rattaco" } ] -} +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Consumable/Food/taco.rsi/tacoshell.png b/Resources/Textures/Objects/Consumable/Food/taco.rsi/tacoshell.png deleted file mode 100644 index 63a396484ce..00000000000 Binary files a/Resources/Textures/Objects/Consumable/Food/taco.rsi/tacoshell.png and /dev/null differ diff --git a/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/cheese.png b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/cheese.png new file mode 100644 index 00000000000..59c459e0c29 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/cheese.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/meta.json new file mode 100644 index 00000000000..7704812eedf --- /dev/null +++ b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/meta.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TheShuEd, rat Taken from https://github.com/tgstation/tgstation/commit/e15c63d100db65eaaa5231133b8a2662ff439131#diff-8dd94e19fdb2ff341b57e31bce101298 and edited by TheShuEd", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "tacoshell_back" + }, + { + "name": "tacoshell_forward" + }, + { + "name": "cheese" + }, + { + "name": "rat" + } + ] +} diff --git a/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/rat.png b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/rat.png new file mode 100644 index 00000000000..6dde4d79892 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/rat.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/tacoshell_back.png b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/tacoshell_back.png new file mode 100644 index 00000000000..7cd31d3b691 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/tacoshell_back.png differ diff --git a/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/tacoshell_forward.png b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/tacoshell_forward.png new file mode 100644 index 00000000000..6c517b8dc21 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Food/taco_sequence.rsi/tacoshell_forward.png differ diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/cart-med.png b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-med.png new file mode 100644 index 00000000000..69be9eb42e1 Binary files /dev/null and b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-med.png differ diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/cart-nav.png b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-nav.png new file mode 100644 index 00000000000..427129da0ed Binary files /dev/null and b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-nav.png differ diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json index c7eb96d964e..d5b9a8df884 100644 --- a/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d , cart-log made by Skarletto (github)", + "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d and tgstation at tgstation at https://github.com/tgstation/tgstation/commit/0c15d9dbcf0f2beb230eba5d9d889ef2d1945bb8, cart-log made by Skarletto (github), cart-sec made by dieselmohawk (discord), cart-nav, cart-med made by ArchRBX (github)", "size": { "x": 32, "y": 32 @@ -75,6 +75,12 @@ }, { "name": "cart-log" + }, + { + "name": "cart-nav" + }, + { + "name": "cart-med" } ] -} +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Devices/communication.rsi/meta.json b/Resources/Textures/Objects/Devices/communication.rsi/meta.json index e9350caad8d..56e5f8f68d0 100644 --- a/Resources/Textures/Objects/Devices/communication.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/communication.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from cev-eris and modified by Swept at https://github.com/discordia-space/CEV-Eris/commit/efce5b6c3be75458ce238dcc01510e8f8a653ca6 // Radio, Beacon, Signaler Taken from Paradise at https://github.com/Henri215/Paradise/blob/27087670280de99e2fceb47194aad29a7b99b280/icons/obj/radio.dmi", + "copyright": "Taken from cev-eris and modified by Swept at https://github.com/discordia-space/CEV-Eris/commit/efce5b6c3be75458ce238dcc01510e8f8a653ca6. Uplink radio sprites by Vermidia, derivative of other lisenced sprites in the repo // Radio, Beacon, Signaler Taken from Paradise at https://github.com/Henri215/Paradise/blob/27087670280de99e2fceb47194aad29a7b99b280/icons/obj/radio.dmi", "size": { "x": 32, "y": 32 @@ -89,6 +89,18 @@ { "name": "off-walkietalkie-inhand-left", "directions": 4 + }, + { + "name": "old-radio-syndicat" + }, + { + "name": "old-radio-urist" + }, + { + "name": "old-radio-ancestor" + }, + { + "name": "old-radio-borg-assault" } ] } diff --git a/Resources/Textures/Objects/Devices/communication.rsi/old-radio-ancestor.png b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-ancestor.png new file mode 100644 index 00000000000..65ecc4c0d6e Binary files /dev/null and b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-ancestor.png differ diff --git a/Resources/Textures/Objects/Devices/communication.rsi/old-radio-borg-assault.png b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-borg-assault.png new file mode 100644 index 00000000000..d7908a14920 Binary files /dev/null and b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-borg-assault.png differ diff --git a/Resources/Textures/Objects/Devices/communication.rsi/old-radio-syndicat.png b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-syndicat.png new file mode 100644 index 00000000000..702f32fc599 Binary files /dev/null and b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-syndicat.png differ diff --git a/Resources/Textures/Objects/Devices/communication.rsi/old-radio-urist.png b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-urist.png new file mode 100644 index 00000000000..3cdf1c61bf0 Binary files /dev/null and b/Resources/Textures/Objects/Devices/communication.rsi/old-radio-urist.png differ diff --git a/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide.png b/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide.png new file mode 100644 index 00000000000..04526a2daca Binary files /dev/null and b/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide.png differ diff --git a/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide_2.png b/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide_2.png new file mode 100644 index 00000000000..9b030184875 Binary files /dev/null and b/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide_2.png differ diff --git a/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide_3.png b/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide_3.png new file mode 100644 index 00000000000..c9a53d76f24 Binary files /dev/null and b/Resources/Textures/Objects/Materials/hide.rsi/goliath_hide_3.png differ diff --git a/Resources/Textures/Objects/Materials/hide.rsi/meta.json b/Resources/Textures/Objects/Materials/hide.rsi/meta.json new file mode 100644 index 00000000000..0ec2b254800 --- /dev/null +++ b/Resources/Textures/Objects/Materials/hide.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/88d236d5c91e9a57e103957555c2e9a8c63b68fa/icons/obj/stacks/organic.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "goliath_hide" + }, + { + "name": "goliath_hide_2" + }, + { + "name": "goliath_hide_3" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-left.png b/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-left.png index c9b55e9daa5..9eae45d039a 100644 Binary files a/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-left.png and b/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-left.png differ diff --git a/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-right.png b/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-right.png index 295c2c4eca1..3353eb6f7ca 100644 Binary files a/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-right.png and b/Resources/Textures/Objects/Materials/materials.rsi/diamond-inhand-right.png differ diff --git a/Resources/Textures/Objects/Materials/materials.rsi/diamond.png b/Resources/Textures/Objects/Materials/materials.rsi/diamond.png index 5eb8aabf879..8b39437d0ac 100644 Binary files a/Resources/Textures/Objects/Materials/materials.rsi/diamond.png and b/Resources/Textures/Objects/Materials/materials.rsi/diamond.png differ diff --git a/Resources/Textures/Objects/Materials/materials.rsi/diamond_2.png b/Resources/Textures/Objects/Materials/materials.rsi/diamond_2.png new file mode 100644 index 00000000000..410d83f1c29 Binary files /dev/null and b/Resources/Textures/Objects/Materials/materials.rsi/diamond_2.png differ diff --git a/Resources/Textures/Objects/Materials/materials.rsi/diamond_3.png b/Resources/Textures/Objects/Materials/materials.rsi/diamond_3.png new file mode 100644 index 00000000000..152da2befe8 Binary files /dev/null and b/Resources/Textures/Objects/Materials/materials.rsi/diamond_3.png differ diff --git a/Resources/Textures/Objects/Materials/materials.rsi/meta.json b/Resources/Textures/Objects/Materials/materials.rsi/meta.json index 016ccddc294..3fbe1a893e2 100644 --- a/Resources/Textures/Objects/Materials/materials.rsi/meta.json +++ b/Resources/Textures/Objects/Materials/materials.rsi/meta.json @@ -78,6 +78,12 @@ { "name": "diamond" }, + { + "name": "diamond_2" + }, + { + "name": "diamond_3" + }, { "name": "diamond-inhand-left", "directions": 4 diff --git a/Resources/Textures/Objects/Materials/ore.rsi/diamond.png b/Resources/Textures/Objects/Materials/ore.rsi/diamond.png new file mode 100644 index 00000000000..b2a3c788d34 Binary files /dev/null and b/Resources/Textures/Objects/Materials/ore.rsi/diamond.png differ diff --git a/Resources/Textures/Objects/Materials/ore.rsi/meta.json b/Resources/Textures/Objects/Materials/ore.rsi/meta.json index bc5dde90121..597fa0499fb 100644 --- a/Resources/Textures/Objects/Materials/ore.rsi/meta.json +++ b/Resources/Textures/Objects/Materials/ore.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-NC-SA-3.0", - "copyright": "silver, plasma taken from https://github.com/vgstation-coders/vgstation13 at commit f2ef221849675915a78fd92fe622c32ab740e085, spacequartz taken from https://github.com/goonstation/goonstation at commit b51daf824df46a3a1426475f982c09479818e522 and reshaded by Alekshhh, bananium; uranium; iron; gold; coal by Alekshhh. Bluespace and Normality taken from tgstation at https://github.com/tgstation/tgstation/commit/f8f4aeda930fcd0805ca4cc76d9bc9412a5b3428, reshaded by Aidenkrz", + "copyright": "silver, plasma taken from https://github.com/vgstation-coders/vgstation13 at commit f2ef221849675915a78fd92fe622c32ab740e085, spacequartz taken from https://github.com/goonstation/goonstation at commit b51daf824df46a3a1426475f982c09479818e522 and reshaded by Alekshhh, bananium; uranium; iron; gold; coal by Alekshhh, diamond at commit https://github.com/tgstation/tgstation/pull/78524, edited by TheShuEd", "size": { "x": 32, "y": 32 @@ -39,6 +39,9 @@ }, { "name": "normality" + }, + { + "name": "diamond" } ] } diff --git a/Resources/Textures/Objects/Specific/Hydroponics/aloe.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/aloe.rsi/produce.png index 68f2133a998..3e0c08cf2d6 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/aloe.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/aloe.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/apple.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/apple.rsi/produce.png index 0bb7ca471b4..cc0a026b59f 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/apple.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/apple.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/blood_tomato.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/blood_tomato.rsi/produce.png index bf5f2fee7ae..be24a81a605 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/blood_tomato.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/blood_tomato.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/blue_tomato.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/blue_tomato.rsi/produce.png index 465dd4eefba..b2a6d230a3c 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/blue_tomato.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/blue_tomato.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cannabis.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/cannabis.rsi/produce.png index a03a31182b1..601dc37fbc8 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/cannabis.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/cannabis.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/dead.png new file mode 100644 index 00000000000..e5ac23c7227 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/harvest.png new file mode 100644 index 00000000000..a39a7943eee Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/meta.json new file mode 100644 index 00000000000..037593e1245 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a, redrawn by Ubaser.", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/produce.png new file mode 100644 index 00000000000..e4c043f889f Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/seed.png new file mode 100644 index 00000000000..021f448ed76 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/stage-1.png new file mode 100644 index 00000000000..b62b6da82ab Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/stage-2.png new file mode 100644 index 00000000000..62c5d75920c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/capfruit.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/dead.png new file mode 100644 index 00000000000..d5667d27219 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/harvest.png new file mode 100644 index 00000000000..16a55ee65fd Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/meta.json new file mode 100644 index 00000000000..35c8da9203a --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/meta.json @@ -0,0 +1,44 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/1dbcf389b0ec6b2c51b002df5fef8dd1519f8068 and remade by RumiTiger", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "stage-4" + }, + { + "name": "stage-5" + }, + { + "name": "stage-6" + }, + { + "name": "pit" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/pit.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/pit.png new file mode 100644 index 00000000000..12219e50679 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/pit.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/produce.png new file mode 100644 index 00000000000..9d48f86f868 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/seed.png new file mode 100644 index 00000000000..b246d45d844 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-1.png new file mode 100644 index 00000000000..21053aed961 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-2.png new file mode 100644 index 00000000000..8073923cd65 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-3.png new file mode 100644 index 00000000000..09a351404e2 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-4.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-4.png new file mode 100644 index 00000000000..82569623287 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-4.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-5.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-5.png new file mode 100644 index 00000000000..038eff2fb2c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-5.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-6.png b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-6.png new file mode 100644 index 00000000000..5bd86228a2c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/cherry.rsi/stage-6.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/dead.png new file mode 100644 index 00000000000..b79722cbe5c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/harvest.png new file mode 100644 index 00000000000..8bbddd83d85 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/meta.json new file mode 100644 index 00000000000..c2fd092c025 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/meta.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/1dbcf389b0ec6b2c51b002df5fef8dd1519f8068 and https://github.com/tgstation/tgstation/commit/ead6d8d59753ef033efdfad17f337df268038ff3 and modified by slarticodefast", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce", + "delays": [ + [ + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08 + ] + ] + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "stage-4" + }, + { + "name": "stage-5" + }, + { + "name": "stage-6" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/produce.png new file mode 100644 index 00000000000..be5b16262e5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/seed.png new file mode 100644 index 00000000000..dad99e5b3a5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-1.png new file mode 100644 index 00000000000..484b4726603 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-2.png new file mode 100644 index 00000000000..cfae4038297 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-3.png new file mode 100644 index 00000000000..e4e2ac9a399 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-4.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-4.png new file mode 100644 index 00000000000..31054ca2c5c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-4.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-5.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-5.png new file mode 100644 index 00000000000..91014722a87 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-5.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-6.png b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-6.png new file mode 100644 index 00000000000..96853e97903 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/extradimensional_orange.rsi/stage-6.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/dead.png new file mode 100644 index 00000000000..fb10912e697 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/harvest.png new file mode 100644 index 00000000000..924aedcea7a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/meta.json new file mode 100644 index 00000000000..1359fd7f8cb --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/meta.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "produce-inhand-left", + "directions": 4 + }, + { + "name": "produce-inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce-inhand-left.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce-inhand-left.png new file mode 100644 index 00000000000..0527f86b937 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce-inhand-left.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce-inhand-right.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce-inhand-right.png new file mode 100644 index 00000000000..cd3cc3216e9 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce-inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce.png new file mode 100644 index 00000000000..11c43d8d13e Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/seed.png new file mode 100644 index 00000000000..f3e99168ddc Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-1.png new file mode 100644 index 00000000000..d4204913b5e Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-2.png new file mode 100644 index 00000000000..804aa020127 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-3.png new file mode 100644 index 00000000000..1e6923c9e2d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/glasstle.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/dead.png new file mode 100644 index 00000000000..817c0f4bff1 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/harvest.png new file mode 100644 index 00000000000..5a49c3f5c53 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/meta.json new file mode 100644 index 00000000000..775f8df408f --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/meta.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/1dbcf389b0ec6b2c51b002df5fef8dd1519f8068", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "stage-4" + }, + { + "name": "stage-5" + }, + { + "name": "stage-6" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/produce.png new file mode 100644 index 00000000000..4c369829601 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/seed.png new file mode 100644 index 00000000000..9fc96ba7c29 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-1.png new file mode 100644 index 00000000000..e65be188b1e Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-2.png new file mode 100644 index 00000000000..165e17b57c9 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-3.png new file mode 100644 index 00000000000..9b193a59e3b Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-4.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-4.png new file mode 100644 index 00000000000..e66ff1ddc94 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-4.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-5.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-5.png new file mode 100644 index 00000000000..fc1807e0899 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-5.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-6.png b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-6.png new file mode 100644 index 00000000000..e2790c179ab Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/golden_apple.rsi/stage-6.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/dead.png new file mode 100644 index 00000000000..a3896d57c12 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/harvest.png new file mode 100644 index 00000000000..1a2a7d37478 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/meta.json new file mode 100644 index 00000000000..65cf434c0a6 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/meta.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/b459ea3fdee965bdc3e93e7983ad7fa610d05c12 and https://github.com/tgstation/tgstation/commit/ead6d8d59753ef033efdfad17f337df268038ff3 and modified by slarticodefast", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce", + "delays": [ + [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + ] + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "stage-4" + }, + { + "name": "stage-5" + }, + { + "name": "stage-6" + }, + { + "name": "slice" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/produce.png new file mode 100644 index 00000000000..73e458d8323 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/seed.png new file mode 100644 index 00000000000..66d13dc00c6 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/slice.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/slice.png new file mode 100644 index 00000000000..9ef34d39ae2 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/slice.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-1.png new file mode 100644 index 00000000000..f926a279dc3 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-2.png new file mode 100644 index 00000000000..4213fc32253 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-3.png new file mode 100644 index 00000000000..69b583f4e41 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-4.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-4.png new file mode 100644 index 00000000000..c496143bf67 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-4.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-5.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-5.png new file mode 100644 index 00000000000..5f9dc48217d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-5.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-6.png b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-6.png new file mode 100644 index 00000000000..a7a3cb45531 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/holymelon.rsi/stage-6.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/dead.png new file mode 100644 index 00000000000..3a1e5735cd8 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/harvest.png new file mode 100644 index 00000000000..b0417c69c0e Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/meta.json new file mode 100644 index 00000000000..cb53758e1cc --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/meta.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/1dbcf389b0ec6b2c51b002df5fef8dd1519f8068 and https://github.com/tgstation/tgstation/commit/ead6d8d59753ef033efdfad17f337df268038ff3 and modified by slarticodefast", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "stage-4" + }, + { + "name": "stage-5" + }, + { + "name": "stage-6" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/produce.png new file mode 100644 index 00000000000..e6ab15f164c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/seed.png new file mode 100644 index 00000000000..de775fe7ac9 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-1.png new file mode 100644 index 00000000000..efdf35bb125 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-2.png new file mode 100644 index 00000000000..1c9d20af4cb Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-3.png new file mode 100644 index 00000000000..bcec67f1e4d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-4.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-4.png new file mode 100644 index 00000000000..8d471502ab5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-4.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-5.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-5.png new file mode 100644 index 00000000000..bee9bd6b93d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-5.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-6.png b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-6.png new file mode 100644 index 00000000000..502f27714c1 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/meatwheat.rsi/stage-6.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/dead.png new file mode 100644 index 00000000000..6c7185bb1e4 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/harvest.png new file mode 100644 index 00000000000..d1f2111b252 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/meta.json new file mode 100644 index 00000000000..3f97a14afa6 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/meta.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/1dbcf389b0ec6b2c51b002df5fef8dd1519f8068", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/produce.png new file mode 100644 index 00000000000..fe787f35f52 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/seed.png new file mode 100644 index 00000000000..3ba27fa5b7d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-1.png new file mode 100644 index 00000000000..5b26285f7c0 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-2.png new file mode 100644 index 00000000000..df5aff42b0c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-3.png new file mode 100644 index 00000000000..1c926be364b Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/papercane.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/pineapple.rsi/slice.png b/Resources/Textures/Objects/Specific/Hydroponics/pineapple.rsi/slice.png index 1c4d96d8e15..50882ec8203 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/pineapple.rsi/slice.png and b/Resources/Textures/Objects/Specific/Hydroponics/pineapple.rsi/slice.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/meta.json index 37bc00be888..16f3df4df6f 100644 --- a/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/meta.json +++ b/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/b459ea3fdee965bdc3e93e7983ad7fa610d05c12", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/b459ea3fdee965bdc3e93e7983ad7fa610d05c12 and https://github.com/tgstation/tgstation/commit/ead6d8d59753ef033efdfad17f337df268038ff3", "size": { "x": 32, "y": 32 diff --git a/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/produce.png index 61e3fb4eaf8..655628bc881 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/watermelon.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/dead.png new file mode 100644 index 00000000000..00d4f1dfc2c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/harvest.png new file mode 100644 index 00000000000..cbedf856116 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/meta.json new file mode 100644 index 00000000000..01be1e7dc4c --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/meta.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/ead6d8d59753ef033efdfad17f337df268038ff3 and modified by slarticodefast", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/produce.png new file mode 100644 index 00000000000..aee68431aab Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/seed.png new file mode 100644 index 00000000000..fe7c8728209 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-1.png new file mode 100644 index 00000000000..abdff1afc7b Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-2.png new file mode 100644 index 00000000000..ab44f97970d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-3.png new file mode 100644 index 00000000000..6ab196981f4 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/world_pea.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/france.png b/Resources/Textures/Objects/Storage/boxes.rsi/france.png new file mode 100644 index 00000000000..f18dd0ca93f Binary files /dev/null and b/Resources/Textures/Objects/Storage/boxes.rsi/france.png differ diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json index 0cba2f59d93..bec1257f2a3 100644 --- a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json +++ b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json @@ -1,11 +1,11 @@ { - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, throwing_knives and vials was drawn by Ubaser, evidence_markers by moomoobeef, Boxes by mefinks.", - "size": { - "x": 32, - "y": 32 - }, + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, throwing_knives and vials was drawn by Ubaser, evidence_markers by moomoobeef, Boxes by mefinks.", + "size": { + "x": 32, + "y": 32 + }, "states": [ { "name": "beaker" @@ -35,7 +35,7 @@ "name": "sechud" }, { - "name": "bottle" + "name": "bottle" }, { "name": "box" @@ -155,12 +155,12 @@ "name": "writing_of_doom" }, { - "name": "headset" + "name": "headset" }, { "name": "encryptokey" - }, - { + }, + { "name": "inhand-left", "directions": 4 }, @@ -214,6 +214,9 @@ }, { "name": "vials" + }, + { + "name": "france" } ] -} +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/handdrill.png b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/handdrill.png new file mode 100644 index 00000000000..8b913e2bd9b Binary files /dev/null and b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/handdrill.png differ diff --git a/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/inhand-left.png b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/inhand-left.png new file mode 100644 index 00000000000..db6873b739f Binary files /dev/null and b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/inhand-right.png b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/inhand-right.png new file mode 100644 index 00000000000..37f9d3f44d5 Binary files /dev/null and b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/meta.json b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/meta.json new file mode 100644 index 00000000000..e9341a75be5 --- /dev/null +++ b/Resources/Textures/Objects/Tools/handdrilldiamond.rsi/meta.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "license": "CC-BY-NC-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/8fdee6e4e3dacdb7a12efaac132933dc0fc649b4 and modified by alzore_", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "handdrill" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/base.png b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/base.png new file mode 100644 index 00000000000..14d0ec58705 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/base.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/icon.png b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/icon.png new file mode 100644 index 00000000000..e388313f0ed Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/meta.json b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/meta.json new file mode 100644 index 00000000000..b7585556c63 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/meta.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Pipebomb sprites made by girthquake", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "base" + }, + { + "name": "wires" + }, + { + "name": "primed", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/primed.png b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/primed.png new file mode 100644 index 00000000000..27a70981fe7 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/primed.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/wires.png b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/wires.png new file mode 100644 index 00000000000..e564f71eab9 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/pipebomb.rsi/wires.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/base.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/base.png new file mode 100644 index 00000000000..1d7177f4eab Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/base.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/equipped-BACKPACK.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/equipped-BACKPACK.png new file mode 100644 index 00000000000..a382ba66ba1 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/equipped-BACKPACK.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/equipped-SUITSTORAGE.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/equipped-SUITSTORAGE.png new file mode 100644 index 00000000000..a382ba66ba1 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/equipped-SUITSTORAGE.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/icon.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/icon.png new file mode 100644 index 00000000000..4cdf7dc4019 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-1.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-1.png new file mode 100644 index 00000000000..4d794f80138 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-1.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-2.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-2.png new file mode 100644 index 00000000000..35f97fbe148 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-2.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-3.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-3.png new file mode 100644 index 00000000000..2008e393d1d Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-3.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-4.png b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-4.png new file mode 100644 index 00000000000..bd4685d5b0a Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/mag-unshaded-4.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/meta.json new file mode 100644 index 00000000000..9ffc0030820 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Battery/energy_shotgun.rsi/meta.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Icon sprite made by Papermaker48 for SS14, other sprites made by Boaz1111 with that sprite as reference.", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "base" + }, + { + "name": "mag-unshaded-1" + }, + { + "name": "mag-unshaded-2" + }, + { + "name": "mag-unshaded-3" + }, + { + "name": "mag-unshaded-4" + }, + { + "name": "equipped-BACKPACK", + "directions": 4 + }, + { + "name": "equipped-SUITSTORAGE", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/energy-inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/energy-inhand-left.png new file mode 100644 index 00000000000..8f7c40c4f9d Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/energy-inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/energy-inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/energy-inhand-right.png new file mode 100644 index 00000000000..9b453686633 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/energy-inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/meta.json new file mode 100644 index 00000000000..2fcf294bc98 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/meta.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Energy Shotgun inhands made by Boaz1111 based on the energy shotgun icon sprite made by Papermaker48 for SS14", + "size": { + "x": 64, + "y": 64 + }, + "states": [ + { + "name": "energy-inhand-left", + "directions": 4 + }, + { + "name": "energy-inhand-right", + "directions": 4 + }, + { + "name": "wielded-inhand-left", + "directions": 4 + }, + { + "name": "wielded-inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/wielded-inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/wielded-inhand-left.png new file mode 100644 index 00000000000..056e6026ac1 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/wielded-inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/wielded-inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/wielded-inhand-right.png new file mode 100644 index 00000000000..d5933b2f698 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Battery/inhands_64x.rsi/wielded-inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/heavylaser.png b/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/heavylaser.png new file mode 100644 index 00000000000..4db201c77cb Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/heavylaser.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/meta.json index f87ad790f69..19d8b15dcc2 100644 --- a/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi/meta.json @@ -46,6 +46,9 @@ 0.05 ] ] + }, + { + "name": "heavylaser" } ] } \ No newline at end of file diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/icon.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/base.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/icon.png rename to Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/base.png diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/meta.json index fc96d8d5194..43840761498 100644 --- a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun.rsi/meta.json @@ -8,7 +8,7 @@ }, "states": [ { - "name": "icon" + "name": "base" }, { "name": "bolt-open" diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/base.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/base.png new file mode 100644 index 00000000000..0b39a08c921 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/base.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/bolt-open.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/bolt-open.png new file mode 100644 index 00000000000..65052544c72 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/bolt-open.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/equipped-BELT.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/equipped-BELT.png new file mode 100644 index 00000000000..fb2854b6fe2 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/equipped-BELT.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/equipped-SUITSTORAGE.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/equipped-SUITSTORAGE.png new file mode 100644 index 00000000000..fb2854b6fe2 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/equipped-SUITSTORAGE.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/inhand-left.png new file mode 100644 index 00000000000..bff3d731c47 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/inhand-right.png new file mode 100644 index 00000000000..50297902ee8 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/meta.json new file mode 100644 index 00000000000..04abe1a4e05 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Shotguns/flaregun_security.rsi/meta.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from cev-eris at https://github.com/discordia-space/CEV-Eris/raw/3f9ebb72931ff884427c3004a594ec61aaaa7041/icons/obj/guns/projectile/flaregun.dmi and edited", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "bolt-open" + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "equipped-BELT", + "directions": 4 + }, + { + "name": "equipped-SUITSTORAGE", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Shaders/radial_blur.swsl b/Resources/Textures/Shaders/radial_blur.swsl new file mode 100644 index 00000000000..f3224ce06c9 --- /dev/null +++ b/Resources/Textures/Shaders/radial_blur.swsl @@ -0,0 +1,14 @@ +uniform sampler2D SCREEN_TEXTURE; +uniform highp float Strength; +const highp int SampleCount = 10; // a higher number makes the shader look better, but has a big performance impact + +// a simple radial blur +void fragment() { + highp vec2 uv = FRAGCOORD.xy * SCREEN_PIXEL_SIZE.xy; + highp vec2 direction = vec2(0.5, 0.5) - uv; + for (int i=1; i <= SampleCount; i++) + { + COLOR += zTextureSpec(SCREEN_TEXTURE, uv + float(i) * Strength / float(SampleCount) * direction); + } + COLOR = COLOR / float(SampleCount); +} diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/meta.json b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/meta.json new file mode 100644 index 00000000000..932ea0c4829 --- /dev/null +++ b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/meta.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from /tg/station at commit 9065b811726ae52be5d1889f436c01a24efbf47a, edited by github user @Flareguy for Space Station 14, modified by Vermidia", + "states": [ + { + "name": "mw" + }, + { + "name": "mw_unlit" + }, + { + "name": "mw0" + }, + { + "name": "mw_running_unlit" + }, + { + "name": "mwb" + }, + { + "name": "mwbloody0" + }, + { + "name": "mwbloody1" + }, + { + "name": "mwo" + } + ] + } \ No newline at end of file diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw.png new file mode 100644 index 00000000000..cdad6aaf282 Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw0.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw0.png new file mode 100644 index 00000000000..532e4e63acd Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw0.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_running_unlit.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_running_unlit.png new file mode 100644 index 00000000000..b751f308a6a Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_running_unlit.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_unlit.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_unlit.png new file mode 100644 index 00000000000..11e691d0317 Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_unlit.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwb.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwb.png new file mode 100644 index 00000000000..8d6462c92e2 Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwb.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwbloody0.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwbloody0.png new file mode 100644 index 00000000000..a51302a2537 Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwbloody0.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwbloody1.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwbloody1.png new file mode 100644 index 00000000000..8cfdf34281a Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwbloody1.png differ diff --git a/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwo.png b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwo.png new file mode 100644 index 00000000000..7545ff0035a Binary files /dev/null and b/Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwo.png differ diff --git a/Resources/Textures/Structures/Machines/station_anchor.rsi/meta.json b/Resources/Textures/Structures/Machines/station_anchor.rsi/meta.json new file mode 100644 index 00000000000..b345534a4c6 --- /dev/null +++ b/Resources/Textures/Structures/Machines/station_anchor.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made and posted by ubaser on the SS14 discord.", + "size": { + "x": 64, + "y": 64 + }, + "states": [ + { + "name": "station_anchor" + }, + { + "name": "station_anchor_unlit" + } + ] +} diff --git a/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png new file mode 100644 index 00000000000..694a35a0d23 Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png differ diff --git a/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png new file mode 100644 index 00000000000..a8f069f45dc Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/base.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/base.png new file mode 100644 index 00000000000..2d67101d2fc Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/base.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-0.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-0.png new file mode 100644 index 00000000000..199a65fb959 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-0.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-1.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-1.png new file mode 100644 index 00000000000..47ee108f18c Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-1.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-2.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-2.png new file mode 100644 index 00000000000..77a54a15cef Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-2.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-3.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-3.png new file mode 100644 index 00000000000..ddd26ba6270 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-3.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-4.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-4.png new file mode 100644 index 00000000000..942f3df3ad2 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-4.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-5.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-5.png new file mode 100644 index 00000000000..1b7430d8095 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-5.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-6.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-6.png new file mode 100644 index 00000000000..e415ee1a7e8 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/chem-6.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/closed.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/closed.png new file mode 100644 index 00000000000..c79f80c0c23 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/closed.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/locked.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/locked.png new file mode 100644 index 00000000000..3156cb9fa1e Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/locked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/meta.json b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/meta.json new file mode 100644 index 00000000000..a3fb20976ef --- /dev/null +++ b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/meta.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Kezu", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "locked" + }, + { + "name": "unlocked" + }, + { + "name": "closed" + }, + { + "name": "open" + }, + { + "name": "chem-0" + }, + { + "name": "chem-1" + }, + { + "name": "chem-2" + }, + { + "name": "chem-3" + }, + { + "name": "chem-4" + }, + { + "name": "chem-5" + }, + { + "name": "chem-6" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/open.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/open.png new file mode 100644 index 00000000000..ce50773db87 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/open.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/unlocked.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/unlocked.png new file mode 100644 index 00000000000..39d2fb77884 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Medical/chemistry.rsi/unlocked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-0.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-0.png new file mode 100644 index 00000000000..c090326148d Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-0.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-1.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-1.png new file mode 100644 index 00000000000..7d698e0020f Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-1.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-10.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-10.png new file mode 100644 index 00000000000..5acb43b1799 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-10.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-11.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-11.png new file mode 100644 index 00000000000..328ca05ab73 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-11.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-12.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-12.png new file mode 100644 index 00000000000..77469720a63 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-12.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-2.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-2.png new file mode 100644 index 00000000000..5eab596dd46 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-2.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-3.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-3.png new file mode 100644 index 00000000000..116080939b6 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-3.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-4.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-4.png new file mode 100644 index 00000000000..15c03124e69 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-4.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-5.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-5.png new file mode 100644 index 00000000000..ece06f2d786 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-5.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-6.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-6.png new file mode 100644 index 00000000000..8ec7b1f6123 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-6.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-7.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-7.png new file mode 100644 index 00000000000..e9468e2cdfe Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-7.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-8.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-8.png new file mode 100644 index 00000000000..d01b2a6bc87 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-8.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-9.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-9.png new file mode 100644 index 00000000000..b5b86d3e4cd Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/bar-9.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/base.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/base.png new file mode 100644 index 00000000000..ff9dd093b6b Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/base.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/meta.json b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/meta.json new file mode 100644 index 00000000000..79e0904dd92 --- /dev/null +++ b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/bar.rsi/meta.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Kezu", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "bar-0" + }, + { + "name": "bar-1" + }, + { + "name": "bar-2" + }, + { + "name": "bar-3" + }, + { + "name": "bar-4" + }, + { + "name": "bar-5" + }, + { + "name": "bar-6" + }, + { + "name": "bar-7" + }, + { + "name": "bar-8" + }, + { + "name": "bar-9" + }, + { + "name": "bar-10" + }, + { + "name": "bar-11" + }, + { + "name": "bar-12" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/base.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/base.png new file mode 100644 index 00000000000..b302518d3de Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/base.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-0.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-0.png new file mode 100644 index 00000000000..199a65fb959 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-0.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-1.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-1.png new file mode 100644 index 00000000000..ded931cadfc Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-1.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-10.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-10.png new file mode 100644 index 00000000000..8d71c464f6e Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-10.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-11.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-11.png new file mode 100644 index 00000000000..5885ae1c6e3 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-11.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-12.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-12.png new file mode 100644 index 00000000000..5afb489f643 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-12.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-2.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-2.png new file mode 100644 index 00000000000..03799bb6889 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-2.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-3.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-3.png new file mode 100644 index 00000000000..5daaf7dda89 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-3.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-4.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-4.png new file mode 100644 index 00000000000..fc7be0c7da2 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-4.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-5.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-5.png new file mode 100644 index 00000000000..150e8f3ec78 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-5.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-6.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-6.png new file mode 100644 index 00000000000..653ca142c23 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-6.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-7.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-7.png new file mode 100644 index 00000000000..8fb7d79932d Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-7.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-8.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-8.png new file mode 100644 index 00000000000..ca197f5ba5f Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-8.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-9.png b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-9.png new file mode 100644 index 00000000000..9f61aeeb480 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/kitchen-9.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/meta.json b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/meta.json new file mode 100644 index 00000000000..2daba51b6c9 --- /dev/null +++ b/Resources/Textures/Structures/Storage/Shelfs/Departments/Service/kitchen.rsi/meta.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Kezu", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "kitchen-0" + }, + { + "name": "kitchen-1" + }, + { + "name": "kitchen-2" + }, + { + "name": "kitchen-3" + }, + { + "name": "kitchen-4" + }, + { + "name": "kitchen-5" + }, + { + "name": "kitchen-6" + }, + { + "name": "kitchen-7" + }, + { + "name": "kitchen-8" + }, + { + "name": "kitchen-9" + }, + { + "name": "kitchen-10" + }, + { + "name": "kitchen-11" + }, + { + "name": "kitchen-12" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/base.png b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/base.png new file mode 100644 index 00000000000..ccaf278a543 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/base.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/closed.png b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/closed.png new file mode 100644 index 00000000000..c79f80c0c23 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/closed.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/locked.png b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/locked.png new file mode 100644 index 00000000000..3156cb9fa1e Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/locked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/meta.json b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/meta.json new file mode 100644 index 00000000000..544b43a1e80 --- /dev/null +++ b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Kezu", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "rbase" + }, + { + "name": "locked" + }, + { + "name": "unlocked" + }, + { + "name": "closed" + }, + { + "name": "open" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/open.png b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/open.png new file mode 100644 index 00000000000..ce50773db87 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/open.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/rbase.png b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/rbase.png new file mode 100644 index 00000000000..584a185caab Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/rbase.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/unlocked.png b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/unlocked.png new file mode 100644 index 00000000000..39d2fb77884 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/glass.rsi/unlocked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/base.png b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/base.png new file mode 100644 index 00000000000..430b603aa3a Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/base.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/closed.png b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/closed.png new file mode 100644 index 00000000000..c79f80c0c23 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/closed.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/locked.png b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/locked.png new file mode 100644 index 00000000000..3156cb9fa1e Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/locked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/meta.json b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/meta.json new file mode 100644 index 00000000000..544b43a1e80 --- /dev/null +++ b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Kezu", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "rbase" + }, + { + "name": "locked" + }, + { + "name": "unlocked" + }, + { + "name": "closed" + }, + { + "name": "open" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/open.png b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/open.png new file mode 100644 index 00000000000..ce50773db87 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/open.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/rbase.png b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/rbase.png new file mode 100644 index 00000000000..430b603aa3a Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/rbase.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/unlocked.png b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/unlocked.png new file mode 100644 index 00000000000..39d2fb77884 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/metal.rsi/unlocked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/base.png b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/base.png new file mode 100644 index 00000000000..a1054e575dd Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/base.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/closed.png b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/closed.png new file mode 100644 index 00000000000..df740ed7b0f Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/closed.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/locked.png b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/locked.png new file mode 100644 index 00000000000..421688fcda8 Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/locked.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/meta.json b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/meta.json new file mode 100644 index 00000000000..544b43a1e80 --- /dev/null +++ b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Kezu", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "rbase" + }, + { + "name": "locked" + }, + { + "name": "unlocked" + }, + { + "name": "closed" + }, + { + "name": "open" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/open.png b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/open.png new file mode 100644 index 00000000000..c0e25c446bd Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/open.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/rbase.png b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/rbase.png new file mode 100644 index 00000000000..a1054e575dd Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/rbase.png differ diff --git a/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/unlocked.png b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/unlocked.png new file mode 100644 index 00000000000..3a00217edae Binary files /dev/null and b/Resources/Textures/Structures/Storage/Shelfs/wood.rsi/unlocked.png differ diff --git a/Resources/Textures/Structures/Wallmounts/intercom.rsi/panel.png b/Resources/Textures/Structures/Wallmounts/intercom.rsi/panel.png index 3bfeb8df584..68f4cd1240b 100644 Binary files a/Resources/Textures/Structures/Wallmounts/intercom.rsi/panel.png and b/Resources/Textures/Structures/Wallmounts/intercom.rsi/panel.png differ diff --git a/Resources/Textures/Structures/Wallmounts/intercom.rsi/speaker.png b/Resources/Textures/Structures/Wallmounts/intercom.rsi/speaker.png index eb09c52fc3a..4bcd29d7f4b 100644 Binary files a/Resources/Textures/Structures/Wallmounts/intercom.rsi/speaker.png and b/Resources/Textures/Structures/Wallmounts/intercom.rsi/speaker.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/ai.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/ai.png index 6cee540a6b2..e532ec039e8 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/ai.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/ai.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/ai_upload.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/ai_upload.png new file mode 100644 index 00000000000..91256aa1df7 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/ai_upload.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly.png index e5b69da718e..6b7361f21c2 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png deleted file mode 100644 index 73d94ac95e4..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/arcade.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/arcade.png index 9f36d43776f..7cb18443529 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/arcade.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/arcade.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/armory.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/armory.png index 671e57aa5cf..3f237ec0466 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/armory.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/armory.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/ass.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/ass.png index 0dfa8a04f5c..9d57ebe7e9a 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/ass.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/ass.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/atmominsky.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/atmominsky.png deleted file mode 100644 index 3bd5488a62c..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/atmominsky.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/atmos.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/atmos.png index 9441b328278..ae31db348b2 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/atmos.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/atmos.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/bar.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/bar.png index e5fb2ab766e..8fdc8016af3 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/bar.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/bar.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/barbershop.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/barbershop.png index d196072a32b..b01a82717f7 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/barbershop.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/barbershop.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/biblio.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/biblio.png index ae86efe4965..9c2f15e0483 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/biblio.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/biblio.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/bridge.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/bridge.png index e34d8cf8f25..838ffa7b24b 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/bridge.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/bridge.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/cans.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/cans.png new file mode 100644 index 00000000000..53cdbbb5c90 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/cans.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo.png index bf0176ea97e..0366a82ffa6 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo_dock.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo_dock.png index df7ee4cf112..4b07e410e29 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo_dock.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/cargo_dock.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/chapel.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/chapel.png index 635f00ec536..a862131d76c 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/chapel.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/chapel.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/chem.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/chem.png index 71739f2d4a8..f8b42e9e6a3 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/chem.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/chem.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/chemistry1.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/chemistry1.png deleted file mode 100644 index d1c1ac91845..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/chemistry1.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/chemistry2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/chemistry2.png deleted file mode 100644 index f9864f7d44c..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/chemistry2.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/cloning.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/cloning.png index ecbc9370c18..da9434fcfbb 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/cloning.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/cloning.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/commander.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/commander.png index d66b76011f2..f98c0a440b2 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/commander.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/commander.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/conference_room.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/conference_room.png index c3d20f31b87..0823ef39432 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/conference_room.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/conference_room.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/court.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/court.png deleted file mode 100644 index fbb8c50593e..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/court.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/cryo.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/cryo.png new file mode 100644 index 00000000000..3a0fc6d7168 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/cryo.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/data.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/data.png new file mode 100644 index 00000000000..03a801e4838 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/data.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/deathsposal.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/deathsposal.png index 28975a1b12b..1fa88f63afa 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/deathsposal.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/deathsposal.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/dock.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/dock.png index 1716f825bbd..8d59357a358 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/dock.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/dock.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/doors.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/doors.png index 46f7585812b..107ea06c99c 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/doors.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/doors.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/drama1.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/drama1.png new file mode 100644 index 00000000000..af0d276d5c0 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/drama1.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/drama2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/drama2.png new file mode 100644 index 00000000000..538374ce404 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/drama2.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/drama3.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/drama3.png new file mode 100644 index 00000000000..cba0ec5955a Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/drama3.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/drones.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/drones.png deleted file mode 100644 index 3d7e65f7a57..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/drones.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/eng.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/eng.png index 2d12b080fc7..5a70940bd90 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/eng.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/eng.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/engine.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/engine.png index 2c4f14d7077..629680bfea8 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/engine.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/engine.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/eva.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/eva.png index 2db7d426456..3e5d2db3c32 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/eva.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/eva.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/examroom.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/examroom.png index afe1ce59865..05f0b7c0f5e 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/examroom.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/examroom.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/gravi.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/gravi.png index 21eb2e86136..811d40aa898 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/gravi.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/gravi.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro.png new file mode 100644 index 00000000000..f23fa7960de Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro1.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro1.png deleted file mode 100644 index b20654fcd64..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro1.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro2.png deleted file mode 100644 index 40d3e546ec4..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro2.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro3.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro3.png deleted file mode 100644 index ccb0e83784e..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/hydro3.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/interrogation.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/interrogation.png index 653caf39075..e5c0b186b4c 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/interrogation.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/interrogation.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/janitor.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/janitor.png index 789a53802c7..350f40dabd4 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/janitor.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/janitor.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/kitchen.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/kitchen.png new file mode 100644 index 00000000000..322d62d8d2e Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/kitchen.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/laundromat.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/laundromat.png index 251e0ebd7b7..400e73df422 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/laundromat.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/laundromat.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/law.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/law.png index de6e16d4a60..0a239af288a 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/law.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/law.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/mail.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/mail.png index f397ffe996e..ffb43a9ae5f 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/mail.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/mail.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/mats.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/mats.png new file mode 100644 index 00000000000..af933d5b9e3 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/mats.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/medbay.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/medbay.png index c9c6e1362a7..e57571f6821 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/medbay.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/medbay.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json b/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json index 97be2aaa0d0..a13df6f7d66 100644 --- a/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json +++ b/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json @@ -5,1490 +5,490 @@ "y": 32 }, "license": "CC-BY-SA-3.0", - "copyright": "Taken from https://github.com/discordia-space/CEV-Eris at commit 4e0bbe682d0a00192d24708fdb7031008aa03f18 and bee station at commit https://github.com/BeeStation/BeeStation-Hornet/commit/13dd5ac712385642574138f6d7b30eea7c2fab9c, except numerical signs which were created by discord: brainfood#7460, states: 'survival' and 'ntmining' from https://github.com/tgstation/tgstation/commit/f743754ec3ef446c8172388431effa73aeddb7ff#diff-b429dd7fccbca60d740d4887c1077a178abf1efffe57e7ae2a0b607c8a9e2202, 'janitor' edited by forgotmyotheraccount on github, 'arcade', 'barbershop', 'direction_exam', 'direction_icu', 'laundromat', 'news', 'reception', and 'salvage' made by rosieposieeee (github)", + "copyright": "Taken from https://github.com/discordia-space/CEV-Eris at commit 4e0bbe682d0a00192d24708fdb7031008aa03f18 and bee station at commit https://github.com/BeeStation/BeeStation-Hornet/commit/13dd5ac712385642574138f6d7b30eea7c2fab9c, Job signs by EmoGarbage404 (github) with inspiration from yogstation and tgstation, 'direction_exam' and 'direction_icu' made by rosieposieeee (github)", "states": [ { - "name": "ai", - "delays": [ - [ - 1 - ] - ] + "name": "ai" }, { - "name": "anomaly", - "delays": [ - [ - 1 - ] - ] + "name": "ai_upload" }, { - "name": "anomaly2" + "name": "vault" }, { - "name": "arcade", - "delays": [ - [ - 1 - ] - ] + "name": "xenoarch" }, { - "name": "armory", - "delays": [ - [ - 1 - ] - ] + "name": "anomaly" }, { - "name": "barbershop", - "delays": [ - [ - 1 - ] - ] + "name": "arcade" }, { - "name": "ass", - "delays": [ - [ - 1 - ] - ] + "name": "armory" }, { - "name": "atmos", - "delays": [ - [ - 1 - ] - ] + "name": "barbershop" }, { - "name": "atmos_air", - "delays": [ - [ - 1 - ] - ] + "name": "ass" }, { - "name": "atmos_co2", - "delays": [ - [ - 1 - ] - ] + "name": "atmos" }, { - "name": "atmos_n2", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_air" }, { - "name": "atmos_n2o", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_co2" }, { - "name": "atmos_o2", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_n2" }, { - "name": "atmos_plasma", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_n2o" }, { - "name": "atmos_tritium", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_o2" }, { - "name": "atmos_waste", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_plasma" }, { - "name": "atmosplaque", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_tritium" }, { - "name": "zumosplaque", - "delays": [ - [ - 1 - ] - ] + "name": "atmos_waste" }, { - "name": "bar", - "delays": [ - [ - 1 - ] - ] + "name": "atmosplaque" }, { - "name": "biblio", - "delays": [ - [ - 1 - ] - ] + "name": "zumosplaque" }, { - "name": "bio", - "delays": [ - [ - 1 - ] - ] + "name": "bar" }, { - "name": "biohazard", - "delays": [ - [ - 1 - ] - ] + "name": "biblio" }, { - "name": "bridge", - "delays": [ - [ - 1 - ] - ] + "name": "bio" }, { - "name": "canisters", - "delays": [ - [ - 1 - ] - ] + "name": "biohazard" }, { - "name": "cargo", - "delays": [ - [ - 1 - ] - ] + "name": "bridge" }, { - "name": "cargo_dock", - "delays": [ - [ - 1 - ] - ] + "name": "canisters" }, { - "name": "chapel", - "delays": [ - [ - 1 - ] - ] + "name": "cargo" }, { - "name": "chem", - "delays": [ - [ - 1 - ] - ] + "name": "cargo_dock" }, { - "name": "chemistry1", - "delays": [ - [ - 1 - ] - ] + "name": "chapel" }, { - "name": "chemistry2", - "delays": [ - [ - 1 - ] - ] + "name": "chem" }, { - "name": "commander", - "delays": [ - [ - 1 - ] - ] + "name": "commander" }, { - "name": "conference_room", - "delays": [ - [ - 1 - ] - ] + "name": "conference_room" }, { - "name": "corrosives", - "delays": [ - [ - 1 - ] - ] + "name": "corrosives" }, { - "name": "court", - "delays": [ - [ - 1 - ] - ] + "name": "cryogenics" }, { - "name": "cryogenics", - "delays": [ - [ - 1 - ] - ] + "name": "danger" }, { - "name": "danger", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "deathsposal", - "delays": [ - [ - 1 - ] - ] + "name": "deathsposal" }, { "name": "direction_bar", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_exam", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_icu", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_janitor", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_food", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_hop", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_library", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_chemistry", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_eng", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_evac", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_supply", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_bridge", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_med", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_sci", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_sec", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_brig", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_chapel", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_hydro", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_dorms", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_cryo", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_gravity", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_salvage", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_solar", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] + "directions": 4 }, { "name": "direction_wash", - "directions": 4, - "delays": [ - [ - 1 - ], - [ - 1 - ], - [ - 1 - ], - [ - 1 - ] - ] - }, - { - "name": "dock", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "doors", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "drones", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "electrical", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "eng", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "engine", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "eva", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "examroom", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "explosives", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "fire", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "flammable", - "delays": [ - [ - 1 - ] - ] - }, - { - "name": "cloning", - "delays": [ - [ - 1 - ] - ] + "directions": 4 + }, + { + "name": "dock" + }, + { + "name": "doors" + }, + { + "name": "mats" + }, + { + "name": "electrical" + }, + { + "name": "eng" + }, + { + "name": "engine" + }, + { + "name": "eva" + }, + { + "name": "examroom" }, { - "name": "gravi", - "delays": [ - [ - 1 - ] - ] + "name": "explosives" }, { - "name": "hydro1", - "delays": [ - [ - 1 - ] - ] + "name": "fire" }, { - "name": "hydro2", - "delays": [ - [ - 1 - ] - ] + "name": "flammable" }, { - "name": "hydro3", - "delays": [ - [ - 1 - ] - ] + "name": "cloning" }, { - "name": "interrogation", - "delays": [ - [ - 1 - ] - ] + "name": "gravi" }, { - "name": "janitor", - "delays": [ - [ - 1 - ] - ] + "name": "hydro" }, { - "name": "laser", - "delays": [ - [ - 1 - ] - ] + "name": "interrogation" }, { - "name": "laundromat", - "delays": [ - [ - 1 - ] - ] + "name": "janitor" }, { - "name": "law", - "delays": [ - [ - 1 - ] - ] + "name": "laser" }, { - "name": "magnetics", - "delays": [ - [ - 1 - ] - ] + "name": "laundromat" }, { - "name": "mail", - "delays": [ - [ - 1 - ] - ] + "name": "law" }, { - "name": "medbay", - "delays": [ - [ - 1 - ] - ] + "name": "magnetics" }, { - "name": "memetic", - "delays": [ - [ - 1 - ] - ] + "name": "mail" }, { - "name": "miner_dock", - "delays": [ - [ - 1 - ] - ] + "name": "medbay" }, { - "name": "monkey_painting", - "delays": [ - [ - 1 - ] - ] + "name": "memetic" }, { - "name": "morgue", - "delays": [ - [ - 1 - ] - ] + "name": "monkey_painting" }, { - "name": "news", - "delays": [ - [ - 1 - ] - ] + "name": "morgue" }, { - "name": "nosmoking", - "delays": [ - [ - 1 - ] - ] + "name": "news" }, { - "name": "nosmoking2", - "delays": [ - [ - 1 - ] - ] + "name": "kitchen" }, { - "name": "surgery", - "delays": [ - [ - 1 - ] - ] + "name": "drama1" }, { - "name": "optical", - "delays": [ - [ - 1 - ] - ] + "name": "drama2" }, { - "name": "oxidants", - "delays": [ - [ - 1 - ] - ] + "name": "drama3" }, { - "name": "pods", - "delays": [ - [ - 1 - ] - ] + "name": "restroom" }, { - "name": "prison", - "delays": [ - [ - 1 - ] - ] + "name": "nosmoking" }, { - "name": "psychology", - "delays": [ - [ - 1 - ] - ] + "name": "nosmoking2" }, { - "name": "radiation", - "delays": [ - [ - 1 - ] - ] + "name": "surgery" }, { - "name": "reception", - "delays": [ - [ - 1 - ] - ] + "name": "optical" }, { - "name": "rnd", - "delays": [ - [ - 1 - ] - ] + "name": "oxidants" }, { - "name": "robo", - "delays": [ - [ - 1 - ] - ] + "name": "pods" }, { - "name": "salvage", - "delays": [ - [ - 1 - ] - ] + "name": "prison" }, { - "name": "sci", - "delays": [ - [ - 1 - ] - ] + "name": "psychology" }, { - "name": "science1", - "delays": [ - [ - 1 - ] - ] + "name": "radiation" }, { - "name": "science2", - "delays": [ - [ - 1 - ] - ] + "name": "reception" }, { - "name": "secure", - "delays": [ - [ - 1 - ] - ] + "name": "rnd" }, { - "name": "securearea", - "delays": [ - [ - 1 - ] - ] + "name": "robo" }, { - "name": "shield", - "delays": [ - [ - 1 - ] - ] + "name": "salvage" }, { - "name": "shock", - "delays": [ - [ - 1 - ] - ] + "name": "sci" }, { - "name": "something-old1", - "delays": [ - [ - 1 - ] - ] + "name": "secure" }, { - "name": "something-old2", - "delays": [ - [ - 1 - ] - ] + "name": "securearea" }, { - "name": "space", - "delays": [ - [ - 1 - ] - ] + "name": "cans" }, { - "name": "telecoms", - "delays": [ - [ - 1 - ] - ] + "name": "shock" }, { - "name": "toxins2", - "delays": [ - [ - 1 - ] - ] + "name": "something-old1" }, { - "name": "toxins", - "delays": [ - [ - 1 - ] - ] + "name": "something-old2" }, { - "name": "virology", - "delays": [ - [ - 1 - ] - ] + "name": "space" }, { - "name": "xenobio", - "delays": [ - [ - 1 - ] - ] + "name": "telecoms" }, { - "name": "xenobio2", - "delays": [ - [ - 1 - ] - ] + "name": "toxins" }, { - "name": "xenolab", - "delays": [ - [ - 1 - ] - ] + "name": "virology" }, { - "name": "zomlab", - "delays": [ - [ - 1 - ] - ] + "name": "xenobio" }, { - "name": "small_secure_red", - "delays": [ - [ - 1 - ] - ] + "name": "zomlab" }, { - "name": "small_secure", - "delays": [ - [ - 1 - ] - ] + "name": "small_secure_red" }, { - "name": "medium_secure_red", - "delays": [ - [ - 1 - ] - ] + "name": "small_secure" }, { - "name": "medium_blank", - "delays": [ - [ - 1 - ] - ] + "name": "medium_secure_red" }, { - "name": "medium_magnetics", - "delays": [ - [ - 1 - ] - ] + "name": "medium_blank" }, { - "name": "medium_danger", - "delays": [ - [ - 1 - ] - ] + "name": "medium_magnetics" }, { - "name": "medium_explosives", - "delays": [ - [ - 1 - ] - ] + "name": "medium_danger" }, { - "name": "medium_cryogenics", - "delays": [ - [ - 1 - ] - ] + "name": "medium_explosives" }, { - "name": "medium_electrical", - "delays": [ - [ - 1 - ] - ] + "name": "medium_cryogenics" }, { - "name": "medium_biohazard", - "delays": [ - [ - 1 - ] - ] + "name": "medium_electrical" }, { - "name": "medium_radiation", - "delays": [ - [ - 1 - ] - ] + "name": "medium_biohazard" }, { - "name": "medium_flammable", - "delays": [ - [ - 1 - ] - ] + "name": "medium_radiation" }, { - "name": "medium_laser", - "delays": [ - [ - 1 - ] - ] + "name": "medium_flammable" }, { - "name": "medium_secure", - "delays": [ - [ - 1 - ] - ] + "name": "medium_laser" }, { - "name": "goldenplaque", - "delays": [ - [ - 1 - ] - ] + "name": "medium_secure" }, { - "name": "kiddieplaque", - "delays": [ - [ - 1 - ] - ] + "name": "goldenplaque" }, { - "name": "security", - "delays": [ - [ - 1 - ] - ] + "name": "kiddieplaque" }, { - "name": "nanotrasen_sign1", - "delays": [ - [ - 1 - ] - ] + "name": "security" }, { - "name": "nanotrasen_sign2", - "delays": [ - [ - 1 - ] - ] + "name": "data" }, { - "name": "nanotrasen_sign3", - "delays": [ - [ - 1 - ] - ] + "name": "cryo" }, { - "name": "nanotrasen_sign4", - "delays": [ - [ - 1 - ] - ] + "name": "nanotrasen_sign1" }, { - "name": "nanotrasen_sign5", - "delays": [ - [ - 1 - ] - ] + "name": "nanotrasen_sign2" }, { - "name": "atmominsky", - "delays": [ - [ - 1 - ] - ] - }, + "name": "nanotrasen_sign3" + }, { - "name": "one", - "delays": [ - [ - 1 - ] - ] - }, + "name": "nanotrasen_sign4" + }, { - "name": "two", - "delays": [ - [ - 1 - ] - ] - }, + "name": "nanotrasen_sign5" + }, { - "name": "three", - "delays": [ - [ - 1 - ] - ] - }, + "name": "one" + }, { - "name": "four", - "delays": [ - [ - 1 - ] - ] - }, + "name": "two" + }, { - "name": "five", - "delays": [ - [ - 1 - ] - ] - }, + "name": "three" + }, { - "name": "six", - "delays": [ - [ - 1 - ] - ] - }, + "name": "four" + }, { - "name": "seven", - "delays": [ - [ - 1 - ] - ] - }, + "name": "five" + }, { - "name": "eight", - "delays": [ - [ - 1 - ] - ] - }, + "name": "six" + }, + { + "name": "seven" + }, + { + "name": "eight" + }, { - "name": "nine", - "delays": [ - [ - 1 - ] - ] - }, + "name": "nine" + }, { - "name": "zero", - "delays": [ - [ - 1 - ] - ] - }, + "name": "zero" + }, { - "name": "survival", - "delays": [ - [ - 1 - ] - ] - }, + "name": "survival" + }, { - "name": "ntmining", - "delays": [ - [ - 1 - ] - ] + "name": "ntmining" } ] } diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/miner_dock.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/miner_dock.png deleted file mode 100644 index a9444f1ed6c..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/miner_dock.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/morgue.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/morgue.png index 1dc0e7d1a96..95729938071 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/morgue.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/morgue.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/news.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/news.png index 575d943c769..40b73c7c2d2 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/news.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/news.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/prison.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/prison.png index a6fe5ca1f39..cea5bba81a9 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/prison.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/prison.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/psychology.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/psychology.png index 9b0761c1bb4..1d89a1ec590 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/psychology.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/psychology.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/reception.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/reception.png index b16670d5b5f..3f3e6dfa490 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/reception.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/reception.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/restroom.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/restroom.png new file mode 100644 index 00000000000..f5903a7d155 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/restroom.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/rnd.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/rnd.png index 875b57204b3..3dc284f4794 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/rnd.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/rnd.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/robo.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/robo.png index 5b3bb6e83db..82519ba966b 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/robo.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/robo.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/salvage.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/salvage.png index 8c51238f897..20b0eb577de 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/salvage.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/salvage.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/sci.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/sci.png index eb3afab2f8f..599207f48ae 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/sci.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/sci.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/science1.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/science1.png deleted file mode 100644 index 7c67901ec43..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/science1.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/science2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/science2.png deleted file mode 100644 index 7722ef83031..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/science2.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/security.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/security.png index 7e27efa2b32..2de5d34a5ee 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/security.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/security.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/shield.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/shield.png deleted file mode 100644 index bb88d2a770e..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/shield.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/space.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/space.png index 214650d6db4..b9286eb06db 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/space.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/space.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/surgery.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/surgery.png index 254ada76fd4..f24ba5c1b33 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/surgery.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/surgery.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/telecoms.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/telecoms.png index e1e721de4aa..5896206cafe 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/telecoms.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/telecoms.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins.png index 6c28bfde003..cf11ed6155f 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins2.png deleted file mode 100644 index c2c35212d0b..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/toxins2.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/vault.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/vault.png new file mode 100644 index 00000000000..33572ca25b5 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/vault.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/virology.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/virology.png index b83297d037c..232d9a74b55 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/virology.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/virology.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenoarch.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/xenoarch.png new file mode 100644 index 00000000000..a4b26daa993 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/xenoarch.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio.png index 0d9336c01a1..4f99ab19c3d 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio2.png deleted file mode 100644 index 3e8515dda03..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenobio2.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenolab.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/xenolab.png deleted file mode 100644 index 21eb486835b..00000000000 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/xenolab.png and /dev/null differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/zomlab.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/zomlab.png index 09d54e603dc..a81784d30b5 100644 Binary files a/Resources/Textures/Structures/Wallmounts/signs.rsi/zomlab.png and b/Resources/Textures/Structures/Wallmounts/signs.rsi/zomlab.png differ diff --git a/Resources/Textures/Structures/Walls/rock.rsi/meta.json b/Resources/Textures/Structures/Walls/rock.rsi/meta.json index 27935ec089d..a37805dfb6f 100644 --- a/Resources/Textures/Structures/Walls/rock.rsi/meta.json +++ b/Resources/Textures/Structures/Walls/rock.rsi/meta.json @@ -104,7 +104,23 @@ "name": "rock_copper" }, { - "name": "rock_diamond" + "name": "rock_diamond", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] }, { "name": "rock_gold" diff --git a/Resources/Textures/Structures/Walls/rock.rsi/rock_diamond.png b/Resources/Textures/Structures/Walls/rock.rsi/rock_diamond.png index eb05ebd3d11..cc54b642f45 100644 Binary files a/Resources/Textures/Structures/Walls/rock.rsi/rock_diamond.png and b/Resources/Textures/Structures/Walls/rock.rsi/rock_diamond.png differ diff --git a/Resources/Textures/Template/signs.png b/Resources/Textures/Template/signs.png new file mode 100644 index 00000000000..4dc483183b2 Binary files /dev/null and b/Resources/Textures/Template/signs.png differ diff --git a/Resources/toolshedEngineCommandPerms.yml b/Resources/toolshedEngineCommandPerms.yml index 97de43e0b1f..4cea8a2b873 100644 --- a/Resources/toolshedEngineCommandPerms.yml +++ b/Resources/toolshedEngineCommandPerms.yml @@ -6,7 +6,6 @@ - physics - player - splat - - emplace - bin - extremes - reduce @@ -19,29 +18,29 @@ - iota - rep - to - - iterate - Flags: DEBUG Commands: - comp - delete - - do + - with + - prototyped - named - paused - - with - - count + - emplace + - do + - iterate - select - where - - prototyped + - count - types - - ecscomp - actor - spawn + - replace - mappos - pos - tp - allcomps - - replace - entitysystemupdateorder - mind @@ -53,80 +52,62 @@ - Flags: ADMIN Commands: - fuck - - ent - - as - - buildinfo - - help - - explain - - cmd - - stopwatch + - '=>' + - '?' + - 'or?' + - '??' + - rng - self + - sum + - take + - join - search + - first + - unique + - any + - contains - isnull - - help - isempty - - any - - unique - cd - ls - - loc - - vars - - '=>' - - first - - val + - stopwatch + - append + - min + - max + - average - '+' - '-' - '*' - '/' - - 'min' - - 'max' - - '&' - - '|' - - '^' - - 'neg' + - '%' + - '%/' + - '&~' + - '|~' + - '^~' + - '~' - '<' - '>' - '<=' - '>=' - '==' - '!=' - - f - - i - - s - - b - '+/' - '-/' - '*/' - '//' - - join - - append - - '?' - - 'or?' - - '??' - - rng - - 'sum' - - take - - curtick - - curtime - - realtime - - servertime - - more - - '%' - - '%/' - - '&~' - - '|~' - - '^~' - - '~' - - 'abs' - - 'average' - - 'bibytecount' - - 'shortestbitlength' - - 'countleadzeros' - - 'counttrailingzeros' - - 'fpi' - - 'fe' - - 'ftau' - - 'fepsilon' + - '&' + - '|' + - '^' + - neg + - abs + - bibytecount + - shortestbitlength + - countleadzeros + - counttrailingzeros + - fpi + - fe + - ftau + - fepsilon - dpi - de - dtau @@ -182,3 +163,29 @@ - atanpi - pick - tee + +- Flags: HOST + Commands: + - methods + - ioc + +- Commands: + - ent + - f + - i + - s + - b + - as + - var + - vars + - val + - help + - explain + - cmd + - buildinfo + - loc + - curtick + - curtime + - realtime + - servertime + - more diff --git a/RobustToolbox b/RobustToolbox index 9c30fdf5fd7..e47ba0faea6 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 9c30fdf5fd7261ea424f80478c2746e2001326e8 +Subproject commit e47ba0faea6b1143f4810547b3d617a110bdb6a5