Merge remote-tracking branch 'upstream/staging' into stable-test
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Commands;
|
||||
@@ -34,7 +36,7 @@ public sealed class ShowHealthBarsCommand : LocalizedCommands
|
||||
{
|
||||
var showHealthBarsComponent = new ShowHealthBarsComponent
|
||||
{
|
||||
DamageContainers = args.ToList(),
|
||||
DamageContainers = args.Select(arg => new ProtoId<DamageContainerPrototype>(arg)).ToList(),
|
||||
HealthStatusIcon = null,
|
||||
NetSyncEnabled = false
|
||||
};
|
||||
|
||||
@@ -124,6 +124,10 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetEv = new GetFlashEffectTargetEvent(ent);
|
||||
RaiseLocalEvent(ent, ref targetEv);
|
||||
ent = targetEv.Target;
|
||||
|
||||
EnsureComp<ColorFlashEffectComponent>(ent, out comp);
|
||||
comp.NetSyncEnabled = false;
|
||||
comp.Color = sprite.Color;
|
||||
@@ -132,3 +136,9 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity to change the target for a color flash effect.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct GetFlashEffectTargetEvent(EntityUid Target);
|
||||
|
||||
@@ -47,8 +47,7 @@
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Horizontal"
|
||||
HorizontalExpand="True" HorizontalAlignment="Center">
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
|
||||
@@ -110,18 +110,29 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
|
||||
// Alerts
|
||||
|
||||
AlertsDivider.Visible = msg.Bleeding == true;
|
||||
AlertsContainer.Visible = msg.Bleeding == true;
|
||||
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
|
||||
|
||||
AlertsDivider.Visible = showAlerts;
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
|
||||
if (showAlerts)
|
||||
AlertsContainer.DisposeAllChildren();
|
||||
|
||||
if (msg.Unrevivable == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
if (msg.Bleeding == true)
|
||||
{
|
||||
AlertsContainer.DisposeAllChildren();
|
||||
AlertsContainer.AddChild(new Label
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
|
||||
FontColorOverride = Color.Red,
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
}
|
||||
|
||||
// Damage Groups
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
VerticalExpand="False"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center">
|
||||
<controls:RecordedSplitContainer Name="ScreenContainer" HorizontalExpand="True"
|
||||
<SplitContainer Name="ScreenContainer" HorizontalExpand="True"
|
||||
VerticalExpand="True" SplitWidth="0"
|
||||
StretchDirection="TopLeft">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Name="SpawnContainer" MinWidth="200" SetWidth="600">
|
||||
@@ -82,5 +82,5 @@
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</LayoutContainer>
|
||||
</controls:RecordedSplitContainer>
|
||||
</SplitContainer>
|
||||
</mapping:MappingScreen>
|
||||
|
||||
@@ -197,7 +197,6 @@ public sealed partial class MappingScreen : InGameScreen
|
||||
|
||||
public override void SetChatSize(Vector2 size)
|
||||
{
|
||||
ScreenContainer.DesiredSplitCenter = size.X;
|
||||
ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,6 @@ public sealed class ShowThirstIconsSystem : EquipmentHudSystem<ShowThirstIconsCo
|
||||
return;
|
||||
|
||||
if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))
|
||||
ev.StatusIcons.Add(iconPrototype!);
|
||||
ev.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Content.Client.Effects;
|
||||
using Content.Client.Smoking;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Polymorph.Components;
|
||||
using Content.Shared.Polymorph.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Polymorph.Systems;
|
||||
|
||||
@@ -10,14 +13,20 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
private EntityQuery<AppearanceComponent> _appearanceQuery;
|
||||
private EntityQuery<SpriteComponent> _spriteQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
|
||||
_spriteQuery = GetEntityQuery<SpriteComponent>();
|
||||
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
|
||||
SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<ChameleonDisguisedComponent, GetFlashEffectTargetEvent>(OnGetFlashEffectTargetEvent);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<ChameleonDisguiseComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
@@ -25,9 +34,30 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
|
||||
CopyComp<SpriteComponent>(ent);
|
||||
CopyComp<GenericVisualizerComponent>(ent);
|
||||
CopyComp<SolutionContainerVisualsComponent>(ent);
|
||||
CopyComp<BurnStateVisualsComponent>(ent);
|
||||
|
||||
// reload appearance to hopefully prevent any invisible layers
|
||||
if (_appearanceQuery.TryComp(ent, out var appearance))
|
||||
_appearance.QueueUpdate(ent, appearance);
|
||||
}
|
||||
|
||||
private void OnStartup(Entity<ChameleonDisguisedComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
if (!_spriteQuery.TryComp(ent, out var sprite))
|
||||
return;
|
||||
|
||||
ent.Comp.WasVisible = sprite.Visible;
|
||||
sprite.Visible = false;
|
||||
}
|
||||
|
||||
private void OnShutdown(Entity<ChameleonDisguisedComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
if (_spriteQuery.TryComp(ent, out var sprite))
|
||||
sprite.Visible = ent.Comp.WasVisible;
|
||||
}
|
||||
|
||||
private void OnGetFlashEffectTargetEvent(Entity<ChameleonDisguisedComponent> ent, ref GetFlashEffectTargetEvent args)
|
||||
{
|
||||
args.Target = ent.Comp.Disguise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A split container that performs an action when the split resizing is finished.
|
||||
/// </summary>
|
||||
public sealed class RecordedSplitContainer : SplitContainer
|
||||
{
|
||||
public double? DesiredSplitCenter;
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
if (ResizeMode == SplitResizeMode.RespectChildrenMinSize
|
||||
&& DesiredSplitCenter != null
|
||||
&& !finalSize.Equals(Vector2.Zero))
|
||||
{
|
||||
SplitFraction = (float) DesiredSplitCenter.Value;
|
||||
|
||||
if (!Size.Equals(Vector2.Zero))
|
||||
{
|
||||
DesiredSplitCenter = null;
|
||||
}
|
||||
}
|
||||
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
VerticalExpand="False"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center">
|
||||
<controls:RecordedSplitContainer Name="ScreenContainer" HorizontalExpand="True" VerticalExpand="True" SplitWidth="0" StretchDirection="TopLeft">
|
||||
<SplitContainer Name="ScreenContainer" HorizontalExpand="True" VerticalExpand="True" SplitWidth="0" StretchDirection="TopLeft">
|
||||
<LayoutContainer Name="ViewportContainer" HorizontalExpand="True" VerticalExpand="True">
|
||||
<controls:MainViewport Name="MainViewport"/>
|
||||
<widgets:GhostGui Name="Ghost" Access="Protected" />
|
||||
@@ -26,7 +26,7 @@
|
||||
</BoxContainer>
|
||||
<alerts:AlertsUI Name="Alerts" Access="Protected" />
|
||||
</LayoutContainer>
|
||||
<PanelContainer HorizontalExpand="True" MinWidth="300">
|
||||
<PanelContainer Name="SeparatedChatPanel" MinWidth="300">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#2B2C3B" />
|
||||
</PanelContainer.PanelOverride>
|
||||
@@ -36,5 +36,5 @@
|
||||
<chat:ChatBox VerticalExpand="True" HorizontalExpand="True" Name="Chat" Access="Protected" MinSize="0 0"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:RecordedSplitContainer>
|
||||
</SplitContainer>
|
||||
</screens:SeparatedChatGameScreen>
|
||||
|
||||
@@ -40,7 +40,6 @@ public sealed partial class SeparatedChatGameScreen : InGameScreen
|
||||
|
||||
public override void SetChatSize(Vector2 size)
|
||||
{
|
||||
ScreenContainer.DesiredSplitCenter = size.X;
|
||||
ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
Margin="8 0 8 0">
|
||||
<BoxContainer Name="Buttons"
|
||||
Orientation="Vertical"
|
||||
SeparationOverride="5">
|
||||
<!-- Buttons are added here by code -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -10,20 +10,17 @@ using Robust.Shared.Utility;
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostRolesEntry : BoxContainer
|
||||
public sealed partial class GhostRoleButtonsBox : BoxContainer
|
||||
{
|
||||
private SpriteSystem _spriteSystem;
|
||||
public event Action<GhostRoleInfo>? OnRoleSelected;
|
||||
public event Action<GhostRoleInfo>? OnRoleFollow;
|
||||
|
||||
public GhostRolesEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
|
||||
public GhostRoleButtonsBox(bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_spriteSystem = spriteSystem;
|
||||
|
||||
Title.Text = name;
|
||||
Description.SetMessage(description);
|
||||
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var button = new GhostRoleEntryButtons(role);
|
||||
@@ -1,15 +1,15 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Horizontal">
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button Name="RequestButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'ghost-roles-window-request-role-button'}"
|
||||
StyleClasses="OpenRight"
|
||||
HorizontalAlignment="Left"
|
||||
SetWidth="300"/>
|
||||
HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="3"/>
|
||||
<Button Name="FollowButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'ghost-roles-window-follow-role-button'}"
|
||||
StyleClasses="OpenLeft"
|
||||
HorizontalAlignment="Right"
|
||||
SetWidth="150"/>
|
||||
HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical">
|
||||
<Label Name="Title"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<PanelContainer StyleClasses="HighDivider" />
|
||||
<RichTextLabel Name="Description"
|
||||
Margin="0 4"/>
|
||||
</BoxContainer>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostRoleInfoBox : BoxContainer
|
||||
{
|
||||
public GhostRoleInfoBox(string name, string description)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Title.Text = name;
|
||||
Description.SetMessage(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 8 8">
|
||||
<Label Name="Title"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<PanelContainer StyleClasses="HighDivider" />
|
||||
<RichTextLabel Name="Description"
|
||||
Margin="0 4"/>
|
||||
<BoxContainer Name="Buttons"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Vertical"
|
||||
SeparationOverride="5">
|
||||
<!-- Buttons are added here by code -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -5,7 +5,6 @@ using Content.Shared.Eui;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
@@ -77,6 +76,13 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
|
||||
if (state is not GhostRolesEuiState ghostState)
|
||||
return;
|
||||
|
||||
// We must save BodyVisible state, so all Collapsible boxes will not close
|
||||
// on adding new ghost role.
|
||||
// Save the current state of each Collapsible box being visible or not
|
||||
_window.SaveCollapsibleBoxesStates();
|
||||
|
||||
// Clearing the container before adding new roles
|
||||
_window.ClearEntries();
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
@@ -84,28 +90,32 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
|
||||
var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
|
||||
|
||||
// TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
|
||||
// Grouping roles
|
||||
var groupedRoles = ghostState.GhostRoles.GroupBy(
|
||||
role => (role.Name, role.Description, role.Requirements));
|
||||
|
||||
// Add a new entry for each role group
|
||||
foreach (var group in groupedRoles)
|
||||
{
|
||||
var name = group.Key.Name;
|
||||
var description = group.Key.Description;
|
||||
bool hasAccess = true;
|
||||
FormattedMessage? reason;
|
||||
|
||||
if (!requirementsManager.CheckRoleRequirements(group.Key.Requirements, null, out reason))
|
||||
{
|
||||
hasAccess = false;
|
||||
}
|
||||
var hasAccess = requirementsManager.CheckRoleRequirements(
|
||||
group.Key.Requirements,
|
||||
null,
|
||||
out var reason);
|
||||
|
||||
// Adding a new role
|
||||
_window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
|
||||
}
|
||||
|
||||
// Restore the Collapsible box state if it is saved
|
||||
_window.RestoreCollapsibleBoxesStates();
|
||||
|
||||
// Close the rules window if it is no longer needed
|
||||
var closeRulesWindow = ghostState.GhostRoles.All(role => role.Identifier != _windowRulesId);
|
||||
if (closeRulesWindow)
|
||||
{
|
||||
_windowRules?.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
@@ -12,20 +15,86 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
public event Action<GhostRoleInfo>? OnRoleRequestButtonClicked;
|
||||
public event Action<GhostRoleInfo>? OnRoleFollow;
|
||||
|
||||
private Dictionary<(string name, string description), Collapsible> _collapsibleBoxes = new();
|
||||
private HashSet<(string name, string description)> _uncollapsedStates = new();
|
||||
|
||||
public GhostRolesWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void ClearEntries()
|
||||
{
|
||||
NoRolesMessage.Visible = true;
|
||||
EntryContainer.DisposeAllChildren();
|
||||
_collapsibleBoxes.Clear();
|
||||
}
|
||||
|
||||
public void SaveCollapsibleBoxesStates()
|
||||
{
|
||||
_uncollapsedStates.Clear();
|
||||
foreach (var (key, collapsible) in _collapsibleBoxes)
|
||||
{
|
||||
if (collapsible.BodyVisible)
|
||||
{
|
||||
_uncollapsedStates.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RestoreCollapsibleBoxesStates()
|
||||
{
|
||||
foreach (var (key, collapsible) in _collapsibleBoxes)
|
||||
{
|
||||
collapsible.BodyVisible = _uncollapsedStates.Contains(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
|
||||
{
|
||||
NoRolesMessage.Visible = false;
|
||||
|
||||
var entry = new GhostRolesEntry(name, description, hasAccess, reason, roles, spriteSystem);
|
||||
entry.OnRoleSelected += OnRoleRequestButtonClicked;
|
||||
entry.OnRoleFollow += OnRoleFollow;
|
||||
EntryContainer.AddChild(entry);
|
||||
var ghostRoleInfos = roles.ToList();
|
||||
var rolesCount = ghostRoleInfos.Count;
|
||||
|
||||
var info = new GhostRoleInfoBox(name, description);
|
||||
var buttons = new GhostRoleButtonsBox(hasAccess, reason, ghostRoleInfos, spriteSystem);
|
||||
buttons.OnRoleSelected += OnRoleRequestButtonClicked;
|
||||
buttons.OnRoleFollow += OnRoleFollow;
|
||||
|
||||
EntryContainer.AddChild(info);
|
||||
|
||||
if (rolesCount > 1)
|
||||
{
|
||||
var buttonHeading = new CollapsibleHeading(Loc.GetString("ghost-roles-window-available-button", ("rolesCount", rolesCount)));
|
||||
|
||||
buttonHeading.AddStyleClass(ContainerButton.StyleClassButton);
|
||||
buttonHeading.Label.HorizontalAlignment = HAlignment.Center;
|
||||
buttonHeading.Label.HorizontalExpand = true;
|
||||
|
||||
var body = new CollapsibleBody
|
||||
{
|
||||
Margin = new Thickness(0, 5, 0, 0),
|
||||
};
|
||||
|
||||
// TODO: Add Requirements to this key when it'll be fixed and work as an equality key in GhostRolesEui
|
||||
var key = (name, description);
|
||||
|
||||
var collapsible = new Collapsible(buttonHeading, body)
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
};
|
||||
|
||||
body.AddChild(buttons);
|
||||
|
||||
EntryContainer.AddChild(collapsible);
|
||||
_collapsibleBoxes.Add(key, collapsible);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntryContainer.AddChild(buttons);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -8,6 +9,7 @@ using Content.Shared.Voting;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
@@ -28,6 +30,7 @@ namespace Content.Client.Voting.UI
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _entNetManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
|
||||
private VotingSystem _votingSystem;
|
||||
|
||||
@@ -62,7 +65,7 @@ namespace Content.Client.Voting.UI
|
||||
|
||||
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime) / 60));
|
||||
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime)));
|
||||
|
||||
foreach (StandardVoteType voteType in Enum.GetValues<StandardVoteType>())
|
||||
{
|
||||
@@ -70,6 +73,7 @@ namespace Content.Client.Voting.UI
|
||||
VoteTypeButton.AddItem(Loc.GetString(option.Name), (int)voteType);
|
||||
}
|
||||
|
||||
_state.OnStateChanged += OnStateChanged;
|
||||
VoteTypeButton.OnItemSelected += VoteTypeSelected;
|
||||
CreateButton.OnPressed += CreatePressed;
|
||||
FollowButton.OnPressed += FollowSelected;
|
||||
@@ -101,6 +105,14 @@ namespace Content.Client.Voting.UI
|
||||
UpdateVoteTimeout();
|
||||
}
|
||||
|
||||
private void OnStateChanged(StateChangedEventArgs obj)
|
||||
{
|
||||
if (obj.NewState is not GameplayState)
|
||||
return;
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void CanCallVoteChanged(bool obj)
|
||||
{
|
||||
if (!obj)
|
||||
|
||||
@@ -19,11 +19,6 @@ public sealed class VotingSystem : EntitySystem
|
||||
|
||||
private void OnVotePlayerListResponseEvent(VotePlayerListResponseEvent msg)
|
||||
{
|
||||
if (!_ghostSystem.IsGhost)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VotePlayerListResponse?.Invoke(msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
|
||||
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) });
|
||||
|
||||
@@ -168,7 +168,7 @@ public sealed class SuicideCommandTests
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Run the suicide command in the console
|
||||
/// Should only ghost the player but not kill them
|
||||
/// </summary>
|
||||
@@ -241,6 +241,7 @@ public sealed class SuicideCommandTests
|
||||
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;
|
||||
@@ -276,6 +277,8 @@ public sealed class SuicideCommandTests
|
||||
// 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();
|
||||
|
||||
@@ -313,6 +316,7 @@ public sealed class SuicideCommandTests
|
||||
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;
|
||||
@@ -348,6 +352,8 @@ public sealed class SuicideCommandTests
|
||||
// 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();
|
||||
|
||||
|
||||
@@ -651,7 +651,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = "admin-smite-become-mouse-name",
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
|
||||
|
||||
@@ -35,8 +35,10 @@ using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.Server.Silicons.Laws;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Silicons.Laws.Components;
|
||||
using Robust.Server.Player;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using static Content.Shared.Configurable.ConfigurationComponent;
|
||||
|
||||
@@ -345,7 +347,30 @@ namespace Content.Server.Administration.Systems
|
||||
Impact = LogImpact.Low
|
||||
});
|
||||
|
||||
if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent))
|
||||
// This logic is needed to be able to modify the AI's laws through its core and eye.
|
||||
EntityUid? target = null;
|
||||
SiliconLawBoundComponent? lawBoundComponent = null;
|
||||
|
||||
if (TryComp(args.Target, out lawBoundComponent))
|
||||
{
|
||||
target = args.Target;
|
||||
}
|
||||
// When inspecting the core we can find the entity with its laws by looking at the AiHolderComponent.
|
||||
else if (TryComp<StationAiHolderComponent>(args.Target, out var holder) && holder.Slot.Item != null
|
||||
&& TryComp(holder.Slot.Item, out lawBoundComponent))
|
||||
{
|
||||
target = holder.Slot.Item.Value;
|
||||
// For the eye we can find the entity with its laws as the source of the movement relay since the eye
|
||||
// is just a proxy for it to move around and look around the station.
|
||||
}
|
||||
else if (TryComp<MovementRelayTargetComponent>(args.Target, out var relay)
|
||||
&& TryComp(relay.Source, out lawBoundComponent))
|
||||
{
|
||||
target = relay.Source;
|
||||
|
||||
}
|
||||
|
||||
if (lawBoundComponent != null && target != null)
|
||||
{
|
||||
args.Verbs.Add(new Verb()
|
||||
{
|
||||
@@ -359,7 +384,7 @@ namespace Content.Server.Administration.Systems
|
||||
return;
|
||||
}
|
||||
_euiManager.OpenEui(ui, session);
|
||||
ui.UpdateLaws(lawBoundComponent, args.Target);
|
||||
ui.UpdateLaws(lawBoundComponent, target.Value);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
|
||||
});
|
||||
|
||||
@@ -47,20 +47,23 @@ namespace Content.Server.Administration.Systems
|
||||
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
|
||||
private static partial Regex DiscordRegex();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private string _webhookUrl = string.Empty;
|
||||
private WebhookData? _webhookData;
|
||||
|
||||
private string _onCallUrl = string.Empty;
|
||||
private WebhookData? _onCallData;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
private string _footerIconUrl = string.Empty;
|
||||
private string _avatarUrl = string.Empty;
|
||||
private string _serverName = string.Empty;
|
||||
|
||||
private readonly
|
||||
Dictionary<NetUserId, (string? id, string username, string description, string? characterName, GameRunLevel
|
||||
lastRunLevel)> _relayMessages = new();
|
||||
private readonly Dictionary<NetUserId, DiscordRelayInteraction> _relayMessages = new();
|
||||
|
||||
private Dictionary<NetUserId, string> _oldMessageIds = new();
|
||||
private readonly Dictionary<NetUserId, Queue<string>> _messageQueues = new();
|
||||
private readonly Dictionary<NetUserId, Queue<DiscordRelayedData>> _messageQueues = new();
|
||||
private readonly HashSet<NetUserId> _processingChannels = new();
|
||||
private readonly Dictionary<NetUserId, (TimeSpan Timestamp, bool Typing)> _typingUpdateTimestamps = new();
|
||||
private string _overrideClientName = string.Empty;
|
||||
@@ -82,12 +85,16 @@ namespace Content.Server.Administration.Systems
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Subs.CVar(_config, CCVars.DiscordOnCallWebhook, OnCallChanged, true);
|
||||
|
||||
Subs.CVar(_config, CCVars.DiscordAHelpWebhook, OnWebhookChanged, true);
|
||||
Subs.CVar(_config, CCVars.DiscordAHelpFooterIcon, OnFooterIconChanged, true);
|
||||
Subs.CVar(_config, CCVars.DiscordAHelpAvatar, OnAvatarChanged, true);
|
||||
Subs.CVar(_config, CVars.GameHostName, OnServerNameChanged, true);
|
||||
Subs.CVar(_config, CCVars.AdminAhelpOverrideClientName, OnOverrideChanged, true);
|
||||
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("AHELP");
|
||||
|
||||
var defaultParams = new AHelpMessageParams(
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
@@ -96,7 +103,7 @@ namespace Content.Server.Administration.Systems
|
||||
_gameTicker.RunLevel,
|
||||
playedSound: false
|
||||
);
|
||||
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Length;
|
||||
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Message.Length;
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
|
||||
@@ -111,6 +118,33 @@ namespace Content.Server.Administration.Systems
|
||||
);
|
||||
}
|
||||
|
||||
private async void OnCallChanged(string url)
|
||||
{
|
||||
_onCallUrl = url;
|
||||
|
||||
if (url == string.Empty)
|
||||
return;
|
||||
|
||||
var match = DiscordRegex().Match(url);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
Log.Error("On call URL does not appear to be valid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (match.Groups.Count <= 2)
|
||||
{
|
||||
Log.Error("Could not get webhook ID or token for on call URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
var webhookId = match.Groups[1].Value;
|
||||
var webhookToken = match.Groups[2].Value;
|
||||
|
||||
_onCallData = await GetWebhookData(webhookId, webhookToken);
|
||||
}
|
||||
|
||||
private void PlayerRateLimitedAction(ICommonSession obj)
|
||||
{
|
||||
RaiseNetworkEvent(
|
||||
@@ -259,13 +293,13 @@ namespace Content.Server.Administration.Systems
|
||||
|
||||
// Store the Discord message IDs of the previous round
|
||||
_oldMessageIds = new Dictionary<NetUserId, string>();
|
||||
foreach (var message in _relayMessages)
|
||||
foreach (var (user, interaction) in _relayMessages)
|
||||
{
|
||||
var id = message.Value.id;
|
||||
var id = interaction.Id;
|
||||
if (id == null)
|
||||
return;
|
||||
|
||||
_oldMessageIds[message.Key] = id;
|
||||
_oldMessageIds[user] = id;
|
||||
}
|
||||
|
||||
_relayMessages.Clear();
|
||||
@@ -330,10 +364,10 @@ namespace Content.Server.Administration.Systems
|
||||
var webhookToken = match.Groups[2].Value;
|
||||
|
||||
// Fire and forget
|
||||
await SetWebhookData(webhookId, webhookToken);
|
||||
_webhookData = await GetWebhookData(webhookId, webhookToken);
|
||||
}
|
||||
|
||||
private async Task SetWebhookData(string id, string token)
|
||||
private async Task<WebhookData?> GetWebhookData(string id, string token)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}");
|
||||
|
||||
@@ -342,10 +376,10 @@ namespace Content.Server.Administration.Systems
|
||||
{
|
||||
_sawmill.Log(LogLevel.Error,
|
||||
$"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
_webhookData = JsonSerializer.Deserialize<WebhookData>(content);
|
||||
return JsonSerializer.Deserialize<WebhookData>(content);
|
||||
}
|
||||
|
||||
private void OnFooterIconChanged(string url)
|
||||
@@ -358,14 +392,14 @@ namespace Content.Server.Administration.Systems
|
||||
_avatarUrl = url;
|
||||
}
|
||||
|
||||
private async void ProcessQueue(NetUserId userId, Queue<string> messages)
|
||||
private async void ProcessQueue(NetUserId userId, Queue<DiscordRelayedData> messages)
|
||||
{
|
||||
// Whether an embed already exists for this player
|
||||
var exists = _relayMessages.TryGetValue(userId, out var existingEmbed);
|
||||
|
||||
// Whether the message will become too long after adding these new messages
|
||||
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Length, MessageLengthCap) + "\n".Length)
|
||||
+ existingEmbed.description.Length > DescriptionMax;
|
||||
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Message.Length, MessageLengthCap) + "\n".Length)
|
||||
+ existingEmbed?.Description.Length > DescriptionMax;
|
||||
|
||||
// If there is no existing embed, or it is getting too long, we create a new embed
|
||||
if (!exists || tooLong)
|
||||
@@ -385,10 +419,10 @@ namespace Content.Server.Administration.Systems
|
||||
// If we have all the data required, we can link to the embed of the previous round or embed that was too long
|
||||
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
|
||||
{
|
||||
if (tooLong && existingEmbed.id != null)
|
||||
if (tooLong && existingEmbed?.Id != null)
|
||||
{
|
||||
linkToPrevious =
|
||||
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.id})**\n";
|
||||
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**\n";
|
||||
}
|
||||
else if (_oldMessageIds.TryGetValue(userId, out var id) && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
@@ -398,13 +432,22 @@ namespace Content.Server.Administration.Systems
|
||||
}
|
||||
|
||||
var characterName = _minds.GetCharacterName(userId);
|
||||
existingEmbed = (null, lookup.Username, linkToPrevious, characterName, _gameTicker.RunLevel);
|
||||
existingEmbed = new DiscordRelayInteraction()
|
||||
{
|
||||
Id = null,
|
||||
CharacterName = characterName,
|
||||
Description = linkToPrevious,
|
||||
Username = lookup.Username,
|
||||
LastRunLevel = _gameTicker.RunLevel,
|
||||
};
|
||||
|
||||
_relayMessages[userId] = existingEmbed;
|
||||
}
|
||||
|
||||
// Previous message was in another RunLevel, so show that in the embed
|
||||
if (existingEmbed.lastRunLevel != _gameTicker.RunLevel)
|
||||
if (existingEmbed!.LastRunLevel != _gameTicker.RunLevel)
|
||||
{
|
||||
existingEmbed.description += _gameTicker.RunLevel switch
|
||||
existingEmbed.Description += _gameTicker.RunLevel switch
|
||||
{
|
||||
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
|
||||
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
|
||||
@@ -413,26 +456,35 @@ namespace Content.Server.Administration.Systems
|
||||
$"{_gameTicker.RunLevel} was not matched."),
|
||||
};
|
||||
|
||||
existingEmbed.lastRunLevel = _gameTicker.RunLevel;
|
||||
existingEmbed.LastRunLevel = _gameTicker.RunLevel;
|
||||
}
|
||||
|
||||
// If last message of the new batch is SOS then relay it to on-call.
|
||||
// ... as long as it hasn't been relayed already.
|
||||
var discordMention = messages.Last();
|
||||
var onCallRelay = !discordMention.Receivers && !existingEmbed.OnCall;
|
||||
|
||||
// Add available messages to the embed description
|
||||
while (messages.TryDequeue(out var message))
|
||||
{
|
||||
// In case someone thinks they're funny
|
||||
if (message.Length > MessageLengthCap)
|
||||
message = message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
|
||||
string text;
|
||||
|
||||
existingEmbed.description += $"\n{message}";
|
||||
// In case someone thinks they're funny
|
||||
if (message.Message.Length > MessageLengthCap)
|
||||
text = message.Message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
|
||||
else
|
||||
text = message.Message;
|
||||
|
||||
existingEmbed.Description += $"\n{text}";
|
||||
}
|
||||
|
||||
var payload = GeneratePayload(existingEmbed.description,
|
||||
existingEmbed.username,
|
||||
existingEmbed.characterName);
|
||||
var payload = GeneratePayload(existingEmbed.Description,
|
||||
existingEmbed.Username,
|
||||
existingEmbed.CharacterName);
|
||||
|
||||
// If there is no existing embed, create a new one
|
||||
// Otherwise patch (edit) it
|
||||
if (existingEmbed.id == null)
|
||||
if (existingEmbed.Id == null)
|
||||
{
|
||||
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
@@ -455,11 +507,11 @@ namespace Content.Server.Administration.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
existingEmbed.id = id.ToString();
|
||||
existingEmbed.Id = id.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.id}",
|
||||
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.Id}",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
|
||||
if (!request.IsSuccessStatusCode)
|
||||
@@ -474,6 +526,43 @@ namespace Content.Server.Administration.Systems
|
||||
|
||||
_relayMessages[userId] = existingEmbed;
|
||||
|
||||
// Actually do the on call relay last, we just need to grab it before we dequeue every message above.
|
||||
if (onCallRelay &&
|
||||
_onCallData != null)
|
||||
{
|
||||
existingEmbed.OnCall = true;
|
||||
var roleMention = _config.GetCVar(CCVars.DiscordAhelpMention);
|
||||
|
||||
if (!string.IsNullOrEmpty(roleMention))
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine($"<@&{roleMention}>");
|
||||
message.AppendLine("Unanswered SOS");
|
||||
|
||||
// Need webhook data to get the correct link for that channel rather than on-call data.
|
||||
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
|
||||
{
|
||||
message.AppendLine(
|
||||
$"**[Go to ahelp](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**");
|
||||
}
|
||||
|
||||
payload = GeneratePayload(message.ToString(), existingEmbed.Username, existingEmbed.CharacterName);
|
||||
|
||||
var request = await _httpClient.PostAsync($"{_onCallUrl}?wait=true",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
|
||||
var content = await request.Content.ReadAsStringAsync();
|
||||
if (!request.IsSuccessStatusCode)
|
||||
{
|
||||
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting relay message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
existingEmbed.OnCall = false;
|
||||
}
|
||||
|
||||
_processingChannels.Remove(userId);
|
||||
}
|
||||
|
||||
@@ -652,7 +741,7 @@ namespace Content.Server.Administration.Systems
|
||||
if (sendsWebhook)
|
||||
{
|
||||
if (!_messageQueues.ContainsKey(msg.UserId))
|
||||
_messageQueues[msg.UserId] = new Queue<string>();
|
||||
_messageQueues[msg.UserId] = new Queue<DiscordRelayedData>();
|
||||
|
||||
var str = message.Text;
|
||||
var unameLength = senderSession.Name.Length;
|
||||
@@ -701,7 +790,7 @@ namespace Content.Server.Administration.Systems
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string GenerateAHelpMessage(AHelpMessageParams parameters)
|
||||
private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
|
||||
{
|
||||
var stringbuilder = new StringBuilder();
|
||||
|
||||
@@ -718,13 +807,57 @@ namespace Content.Server.Administration.Systems
|
||||
stringbuilder.Append($" **{parameters.RoundTime}**");
|
||||
if (!parameters.PlayedSound)
|
||||
stringbuilder.Append(" **(S)**");
|
||||
|
||||
if (parameters.Icon == null)
|
||||
stringbuilder.Append($" **{parameters.Username}:** ");
|
||||
else
|
||||
stringbuilder.Append($" **{parameters.Username}** ");
|
||||
stringbuilder.Append(parameters.Message);
|
||||
return stringbuilder.ToString();
|
||||
|
||||
return new DiscordRelayedData()
|
||||
{
|
||||
Receivers = !parameters.NoReceivers,
|
||||
Message = stringbuilder.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
private record struct DiscordRelayedData
|
||||
{
|
||||
/// <summary>
|
||||
/// Was anyone online to receive it.
|
||||
/// </summary>
|
||||
public bool Receivers;
|
||||
|
||||
/// <summary>
|
||||
/// What's the payload to send to discord.
|
||||
/// </summary>
|
||||
public string Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class specifically for holding information regarding existing Discord embeds
|
||||
/// </summary>
|
||||
private sealed class DiscordRelayInteraction
|
||||
{
|
||||
public string? Id;
|
||||
|
||||
public string Username = String.Empty;
|
||||
|
||||
public string? CharacterName;
|
||||
|
||||
/// <summary>
|
||||
/// Contents for the discord message.
|
||||
/// </summary>
|
||||
public string Description = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Run level of the last interaction. If different we'll link to the last Id.
|
||||
/// </summary>
|
||||
public GameRunLevel LastRunLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Did we relay this interaction to OnCall previously.
|
||||
/// </summary>
|
||||
public bool OnCall;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed partial class AmeControllerComponent : SharedAmeControllerComponen
|
||||
/// </summary>
|
||||
[DataField("injectSound")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier InjectSound = new SoundCollectionSpecifier("MetalThud");
|
||||
public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Machines/ame_fuelinjection.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The last time this could have injected fuel into the AME.
|
||||
|
||||
@@ -680,7 +680,10 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
if (TryComp<HandsComponent>(user, out var hands))
|
||||
{
|
||||
if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!_botany.CanHarvest(component.Seed))
|
||||
{
|
||||
|
||||
@@ -428,6 +428,7 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
|
||||
{
|
||||
var cartridgeEvent = args.MessageEvent;
|
||||
cartridgeEvent.LoaderUid = GetNetEntity(uid);
|
||||
cartridgeEvent.Actor = args.Actor;
|
||||
|
||||
RelayEvent(component, cartridgeEvent, true);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ public sealed class CursedMaskSystem : SharedCursedMaskSystem
|
||||
}
|
||||
|
||||
var npcFaction = EnsureComp<NpcFactionMemberComponent>(wearer);
|
||||
ent.Comp.OldFactions = npcFaction.Factions;
|
||||
ent.Comp.OldFactions.Clear();
|
||||
ent.Comp.OldFactions.UnionWith(npcFaction.Factions);
|
||||
_npcFaction.ClearFactions((wearer, npcFaction), false);
|
||||
_npcFaction.AddFaction((wearer, npcFaction), ent.Comp.CursedMaskFaction);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class ExaminableDamageSystem : EntitySystem
|
||||
|
||||
var level = GetDamageLevel(uid, component);
|
||||
var msg = Loc.GetString(messages[level]);
|
||||
args.PushMarkup(msg);
|
||||
args.PushMarkup(msg,-99);
|
||||
}
|
||||
|
||||
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
|
||||
|
||||
@@ -46,8 +46,8 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
SetBoltsDown(ent, true);
|
||||
}
|
||||
|
||||
UpdateBoltLightStatus(ent);
|
||||
ent.Comp.Powered = args.Powered;
|
||||
Dirty(ent, ent.Comp);
|
||||
UpdateBoltLightStatus(ent);
|
||||
}
|
||||
}
|
||||
|
||||
82
Content.Server/EntityEffects/Effects/FlashReactionEffect.cs
Normal file
82
Content.Server/EntityEffects/Effects/FlashReactionEffect.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Server.Flash;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.EntityEffects.Effects;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class FlashReactionEffect : EntityEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Flash range per unit of reagent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float RangePerUnit = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum flash range.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxRange = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// How much to entities are slowed down.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float SlowTo = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The time entities will be flashed in seconds.
|
||||
/// The default is chosen to be better than the hand flash so it is worth using it for grenades etc.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Duration = 4f;
|
||||
|
||||
/// <summary>
|
||||
/// The prototype ID used for the visual effect.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? FlashEffectPrototype = "ReactionFlash";
|
||||
|
||||
/// <summary>
|
||||
/// The sound the flash creates.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Weapons/flash.ogg");
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
=> Loc.GetString("reagent-effect-guidebook-flash-reaction-effect", ("chance", Probability));
|
||||
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
var transform = args.EntityManager.GetComponent<TransformComponent>(args.TargetEntity);
|
||||
var transformSystem = args.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
var range = 1f;
|
||||
|
||||
if (args is EntityEffectReagentArgs reagentArgs)
|
||||
range = MathF.Min((float)(reagentArgs.Quantity * RangePerUnit), MaxRange);
|
||||
|
||||
args.EntityManager.System<FlashSystem>().FlashArea(
|
||||
args.TargetEntity,
|
||||
null,
|
||||
range,
|
||||
Duration * 1000,
|
||||
slowTo: SlowTo,
|
||||
sound: Sound);
|
||||
|
||||
if (FlashEffectPrototype == null)
|
||||
return;
|
||||
|
||||
var uid = args.EntityManager.SpawnEntity(FlashEffectPrototype, transformSystem.GetMapCoordinates(transform));
|
||||
transformSystem.AttachToGridOrMap(uid);
|
||||
|
||||
if (!args.EntityManager.TryGetComponent<PointLightComponent>(uid, out var pointLightComp))
|
||||
return;
|
||||
var pointLightSystem = args.EntityManager.System<SharedPointLightSystem>();
|
||||
// PointLights with a radius lower than 1.1 are too small to be visible, so this is hardcoded
|
||||
pointLightSystem.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp);
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
// 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)));
|
||||
if(comp.GiveCodewords)
|
||||
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
|
||||
}
|
||||
|
||||
// TODO: figure out how to handle this? add priority to briefing event?
|
||||
|
||||
@@ -213,14 +213,7 @@ namespace Content.Server.Ghost
|
||||
|
||||
private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args)
|
||||
{
|
||||
if (_actions.AddAction(uid, ref component.BooActionEntity, out var act, component.BooAction)
|
||||
&& act.UseDelay != null)
|
||||
{
|
||||
var start = _gameTiming.CurTime;
|
||||
var end = start + act.UseDelay.Value;
|
||||
_actions.SetCooldown(component.BooActionEntity.Value, start, end);
|
||||
}
|
||||
|
||||
_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);
|
||||
|
||||
@@ -201,6 +201,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
|
||||
? bloodSolution.FillFraction
|
||||
: 0,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
|
||||
using Content.Server.Medical.Components;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Server.Traits.Assorted;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
@@ -196,6 +197,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
|
||||
|
||||
var bloodAmount = float.NaN;
|
||||
var bleeding = false;
|
||||
var unrevivable = false;
|
||||
|
||||
if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
|
||||
_solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName,
|
||||
@@ -205,12 +207,16 @@ public sealed class HealthAnalyzerSystem : EntitySystem
|
||||
bleeding = bloodstream.BleedAmount > 0;
|
||||
}
|
||||
|
||||
if (HasComp<UnrevivableComponent>(target))
|
||||
unrevivable = true;
|
||||
|
||||
_uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage(
|
||||
GetNetEntity(target),
|
||||
bodyTemperature,
|
||||
bloodAmount,
|
||||
scanMode,
|
||||
bleeding
|
||||
bleeding,
|
||||
unrevivable
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
|
||||
protected override void SplattedCreamPie(EntityUid uid, CreamPieComponent creamPie)
|
||||
{
|
||||
_audio.PlayPvs(_audio.GetSound(creamPie.Sound), uid, AudioParams.Default.WithVariation(0.125f));
|
||||
// The entity is deleted, so play the sound at its position rather than parenting
|
||||
var coordinates = Transform(uid).Coordinates;
|
||||
_audio.PlayPvs(_audio.GetSound(creamPie.Sound), coordinates, AudioParams.Default.WithVariation(0.125f));
|
||||
|
||||
if (EntityManager.TryGetComponent(uid, out FoodComponent? foodComp))
|
||||
{
|
||||
@@ -49,12 +51,9 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
{
|
||||
_puddle.TrySpillAt(uid, solution, out _, false);
|
||||
}
|
||||
if (foodComp.Trash.Count == 0)
|
||||
foreach (var trash in foodComp.Trash)
|
||||
{
|
||||
foreach (var trash in foodComp.Trash)
|
||||
{
|
||||
EntityManager.SpawnEntity(trash, Transform(uid).Coordinates);
|
||||
}
|
||||
EntityManager.SpawnEntity(trash, Transform(uid).Coordinates);
|
||||
}
|
||||
}
|
||||
ActivatePayload(uid);
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.Shuttles.Events;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Parallax.Biomes;
|
||||
using Content.Shared.Parallax.Biomes.Layers;
|
||||
@@ -51,6 +52,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
|
||||
private EntityQuery<BiomeComponent> _biomeQuery;
|
||||
private EntityQuery<FixturesComponent> _fixturesQuery;
|
||||
private EntityQuery<GhostComponent> _ghostQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
private readonly HashSet<EntityUid> _handledEntities = new();
|
||||
@@ -81,6 +83,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
Log.Level = LogLevel.Debug;
|
||||
_biomeQuery = GetEntityQuery<BiomeComponent>();
|
||||
_fixturesQuery = GetEntityQuery<FixturesComponent>();
|
||||
_ghostQuery = GetEntityQuery<GhostComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
SubscribeLocalEvent<BiomeComponent, MapInitEvent>(OnBiomeMapInit);
|
||||
SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
|
||||
@@ -315,6 +318,11 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanLoad(EntityUid uid)
|
||||
{
|
||||
return !_ghostQuery.HasComp(uid);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
@@ -332,7 +340,8 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
if (_xformQuery.TryGetComponent(pSession.AttachedEntity, out var xform) &&
|
||||
_handledEntities.Add(pSession.AttachedEntity.Value) &&
|
||||
_biomeQuery.TryGetComponent(xform.MapUid, out var biome) &&
|
||||
biome.Enabled)
|
||||
biome.Enabled &&
|
||||
CanLoad(pSession.AttachedEntity.Value))
|
||||
{
|
||||
var worldPos = _transform.GetWorldPosition(xform);
|
||||
AddChunksInRange(biome, worldPos);
|
||||
@@ -349,7 +358,8 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
if (!_handledEntities.Add(viewer) ||
|
||||
!_xformQuery.TryGetComponent(viewer, out xform) ||
|
||||
!_biomeQuery.TryGetComponent(xform.MapUid, out biome) ||
|
||||
!biome.Enabled)
|
||||
!biome.Enabled ||
|
||||
!CanLoad(viewer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,99 +1,5 @@
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Polymorph.Components;
|
||||
using Content.Shared.Polymorph.Systems;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Server.Polymorph.Systems;
|
||||
|
||||
public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
|
||||
{
|
||||
[Dependency] private readonly MetaDataSystem _meta = default!;
|
||||
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
|
||||
[Dependency] private readonly PolymorphSystem _polymorph = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, GotEquippedHandEvent>(OnEquippedHand);
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, DisguiseToggleNoRotEvent>(OnToggleNoRot);
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, DisguiseToggleAnchoredEvent>(OnToggleAnchored);
|
||||
}
|
||||
|
||||
private void OnEquippedHand(Entity<ChameleonDisguiseComponent> ent, ref GotEquippedHandEvent args)
|
||||
{
|
||||
if (!TryComp<PolymorphedEntityComponent>(ent, out var poly))
|
||||
return;
|
||||
|
||||
_polymorph.Revert((ent, poly));
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public override void Disguise(ChameleonProjectorComponent proj, EntityUid user, EntityUid entity)
|
||||
{
|
||||
if (_polymorph.PolymorphEntity(user, proj.Polymorph) is not {} disguise)
|
||||
return;
|
||||
|
||||
// make disguise look real (for simple things at least)
|
||||
var meta = MetaData(entity);
|
||||
_meta.SetEntityName(disguise, meta.EntityName);
|
||||
_meta.SetEntityDescription(disguise, meta.EntityDescription);
|
||||
|
||||
var comp = EnsureComp<ChameleonDisguiseComponent>(disguise);
|
||||
comp.SourceEntity = entity;
|
||||
comp.SourceProto = Prototype(entity)?.ID;
|
||||
Dirty(disguise, comp);
|
||||
|
||||
// no sechud trolling
|
||||
RemComp<StatusIconComponent>(disguise);
|
||||
|
||||
_appearance.CopyData(entity, disguise);
|
||||
|
||||
var mass = CompOrNull<PhysicsComponent>(entity)?.Mass ?? 0f;
|
||||
|
||||
// let the disguise die when its taken enough damage, which then transfers to the player
|
||||
// health is proportional to mass, and capped to not be insane
|
||||
if (TryComp<MobThresholdsComponent>(disguise, out var thresholds))
|
||||
{
|
||||
// if the player is of flesh and blood, cap max health to theirs
|
||||
// so that when reverting damage scales 1:1 and not round removing
|
||||
var playerMax = _mobThreshold.GetThresholdForState(user, MobState.Dead).Float();
|
||||
var max = playerMax == 0f ? proj.MaxHealth : Math.Max(proj.MaxHealth, playerMax);
|
||||
|
||||
var health = Math.Clamp(mass, proj.MinHealth, proj.MaxHealth);
|
||||
_mobThreshold.SetMobStateThreshold(disguise, health, MobState.Critical, thresholds);
|
||||
_mobThreshold.SetMobStateThreshold(disguise, max, MobState.Dead, thresholds);
|
||||
}
|
||||
|
||||
// add actions for controlling transform aspects
|
||||
_actions.AddAction(disguise, proj.NoRotAction);
|
||||
_actions.AddAction(disguise, proj.AnchorAction);
|
||||
}
|
||||
|
||||
private void OnToggleNoRot(Entity<ChameleonDisguiseComponent> ent, ref DisguiseToggleNoRotEvent args)
|
||||
{
|
||||
var xform = Transform(ent);
|
||||
xform.NoLocalRotation = !xform.NoLocalRotation;
|
||||
}
|
||||
|
||||
private void OnToggleAnchored(Entity<ChameleonDisguiseComponent> ent, ref DisguiseToggleAnchoredEvent args)
|
||||
{
|
||||
var uid = ent.Owner;
|
||||
var xform = Transform(uid);
|
||||
if (xform.Anchored)
|
||||
_xform.Unanchor(uid, xform);
|
||||
else
|
||||
_xform.AnchorEntity((uid, xform));
|
||||
}
|
||||
}
|
||||
public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem;
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class RadioDeviceSystem : EntitySystem
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
// Used to prevent a shitter from using a bunch of radios to spam chat.
|
||||
private HashSet<(string, EntityUid)> _recentlySent = new();
|
||||
private HashSet<(string, EntityUid, RadioChannelPrototype)> _recentlySent = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -114,7 +114,7 @@ public sealed class RadioDeviceSystem : EntitySystem
|
||||
{
|
||||
if (args.Powered)
|
||||
return;
|
||||
SetMicrophoneEnabled(uid, null, false, true, component);
|
||||
SetMicrophoneEnabled(uid, null, false, true, component);
|
||||
}
|
||||
|
||||
public void SetMicrophoneEnabled(EntityUid uid, EntityUid? user, bool enabled, bool quiet = false, RadioMicrophoneComponent? component = null)
|
||||
@@ -191,8 +191,9 @@ public sealed class RadioDeviceSystem : EntitySystem
|
||||
if (HasComp<RadioSpeakerComponent>(args.Source))
|
||||
return; // no feedback loops please.
|
||||
|
||||
if (_recentlySent.Add((args.Message, args.Source)))
|
||||
_radio.SendRadioMessage(args.Source, args.Message, _protoMan.Index<RadioChannelPrototype>(component.BroadcastChannel), uid);
|
||||
var channel = _protoMan.Index<RadioChannelPrototype>(component.BroadcastChannel)!;
|
||||
if (_recentlySent.Add((args.Message, args.Source, channel)))
|
||||
_radio.SendRadioMessage(args.Source, args.Message, channel, uid);
|
||||
}
|
||||
|
||||
private void OnAttemptListen(EntityUid uid, RadioMicrophoneComponent component, ListenAttemptEvent args)
|
||||
@@ -279,7 +280,7 @@ public sealed class RadioDeviceSystem : EntitySystem
|
||||
if (TryComp<RadioMicrophoneComponent>(ent, out var mic))
|
||||
mic.BroadcastChannel = channel;
|
||||
if (TryComp<RadioSpeakerComponent>(ent, out var speaker))
|
||||
speaker.Channels = new(){ channel };
|
||||
speaker.Channels = new() { channel };
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Content.Server.Research.Disk
|
||||
_research.ModifyServerPoints(args.Target.Value, component.Points, server);
|
||||
_popupSystem.PopupEntity(Loc.GetString("research-disk-inserted", ("points", component.Points)), args.Target.Value, args.User);
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, ResearchDiskComponent component, MapInitEvent args)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Eui;
|
||||
@@ -52,8 +52,8 @@ public sealed class SiliconLawEui : BaseEui
|
||||
return;
|
||||
|
||||
var player = _entityManager.GetEntity(message.Target);
|
||||
|
||||
_siliconLawSystem.SetLaws(message.Laws, player);
|
||||
if (_entityManager.TryGetComponent<SiliconLawProviderComponent>(player, out var playerProviderComp))
|
||||
_siliconLawSystem.SetLaws(message.Laws, player, playerProviderComp.LawUploadSound);
|
||||
}
|
||||
|
||||
private bool IsAllowed()
|
||||
|
||||
@@ -21,6 +21,8 @@ using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Silicons.Laws;
|
||||
|
||||
@@ -113,7 +115,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
component.Lawset = args.Lawset;
|
||||
|
||||
// gotta tell player to check their laws
|
||||
NotifyLawsChanged(uid);
|
||||
NotifyLawsChanged(uid, component.LawUploadSound);
|
||||
|
||||
// new laws may allow antagonist behaviour so make it clear for admins
|
||||
if (TryComp<EmagSiliconLawComponent>(uid, out var emag))
|
||||
@@ -149,14 +151,11 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
return;
|
||||
|
||||
base.OnGotEmagged(uid, component, ref args);
|
||||
NotifyLawsChanged(uid);
|
||||
NotifyLawsChanged(uid, component.EmaggedSound);
|
||||
EnsureEmaggedRole(uid, component);
|
||||
|
||||
_stunSystem.TryParalyze(uid, component.StunTime, true);
|
||||
|
||||
if (!_mind.TryGetMind(uid, out var mindId, out _))
|
||||
return;
|
||||
_roles.MindPlaySound(mindId, component.EmaggedSound);
|
||||
}
|
||||
|
||||
private void OnEmagMindAdded(EntityUid uid, EmagSiliconLawComponent component, MindAddedMessage args)
|
||||
@@ -237,7 +236,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
return ev.Laws;
|
||||
}
|
||||
|
||||
public void NotifyLawsChanged(EntityUid uid)
|
||||
public void NotifyLawsChanged(EntityUid uid, SoundSpecifier? cue = null)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(uid, out var actor))
|
||||
return;
|
||||
@@ -245,6 +244,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
var msg = Loc.GetString("laws-update-notify");
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red);
|
||||
|
||||
if (cue != null && _mind.TryGetMind(uid, out var mindId, out _))
|
||||
_roles.MindPlaySound(mindId, cue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -269,7 +271,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
/// <summary>
|
||||
/// Set the laws of a silicon entity while notifying the player.
|
||||
/// </summary>
|
||||
public void SetLaws(List<SiliconLaw> newLaws, EntityUid target)
|
||||
public void SetLaws(List<SiliconLaw> newLaws, EntityUid target, SoundSpecifier? cue = null)
|
||||
{
|
||||
if (!TryComp<SiliconLawProviderComponent>(target, out var component))
|
||||
return;
|
||||
@@ -278,7 +280,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
component.Lawset = new SiliconLawset();
|
||||
|
||||
component.Lawset.Laws = newLaws;
|
||||
NotifyLawsChanged(target);
|
||||
NotifyLawsChanged(target, cue);
|
||||
}
|
||||
|
||||
protected override void OnUpdaterInsert(Entity<SiliconLawUpdaterComponent> ent, ref EntInsertedIntoContainerMessage args)
|
||||
@@ -292,9 +294,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
|
||||
while (query.MoveNext(out var update))
|
||||
{
|
||||
SetLaws(lawset, update);
|
||||
if (provider.LawUploadSound != null && _mind.TryGetMind(update, out var mindId, out _))
|
||||
_roles.MindPlaySound(mindId, provider.LawUploadSound);
|
||||
SetLaws(lawset, update, provider.LawUploadSound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Content.Server.Voting.Managers
|
||||
|
||||
private VotingSystem? _votingSystem;
|
||||
private RoleSystem? _roleSystem;
|
||||
private GameTicker? _gameTicker;
|
||||
|
||||
private static readonly Dictionary<StandardVoteType, CVarDef<bool>> _voteTypesToEnableCVars = new()
|
||||
{
|
||||
@@ -70,8 +71,8 @@ namespace Content.Server.Voting.Managers
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null);
|
||||
}
|
||||
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
||||
ticker.UpdateInfoText();
|
||||
_gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
||||
_gameTicker.UpdateInfoText();
|
||||
if (timeoutVote)
|
||||
TimeoutStandardVote(voteType);
|
||||
}
|
||||
@@ -346,8 +347,14 @@ namespace Content.Server.Voting.Managers
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var voterEligibility = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? VoterEligibility.GhostMinimumPlaytime : VoterEligibility.MinimumPlaytime;
|
||||
if (_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) && _gameTicker!.RunLevel == GameRunLevel.PreRoundLobby)
|
||||
voterEligibility = VoterEligibility.MinimumPlaytime;
|
||||
|
||||
var eligibleVoterNumberRequirement = _cfg.GetCVar(CCVars.VotekickEligibleNumberRequirement);
|
||||
var eligibleVoterNumber = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? CalculateEligibleVoterNumber(VoterEligibility.GhostMinimumPlaytime) : CalculateEligibleVoterNumber(VoterEligibility.MinimumPlaytime);
|
||||
var eligibleVoterNumber = CalculateEligibleVoterNumber(voterEligibility);
|
||||
|
||||
string target = args[0];
|
||||
string reason = args[1];
|
||||
@@ -441,7 +448,7 @@ namespace Content.Server.Voting.Managers
|
||||
},
|
||||
Duration = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimer)),
|
||||
InitiatorTimeout = TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.VotekickTimeout)),
|
||||
VoterEligibility = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? VoterEligibility.GhostMinimumPlaytime : VoterEligibility.MinimumPlaytime,
|
||||
VoterEligibility = voterEligibility,
|
||||
DisplayVotes = false,
|
||||
TargetEntity = targetNetEntity
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
namespace Content.Server.Voting.Managers
|
||||
{
|
||||
public sealed partial class VoteManager : IVoteManager
|
||||
@@ -40,7 +39,7 @@ namespace Content.Server.Voting.Managers
|
||||
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
|
||||
[Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
|
||||
|
||||
private int _nextVoteId = 1;
|
||||
|
||||
@@ -282,7 +281,7 @@ namespace Content.Server.Voting.Managers
|
||||
}
|
||||
|
||||
// Admin always see the vote count, even if the vote is set to hide it.
|
||||
if (_adminMgr.HasAdminFlag(player, AdminFlags.Moderator))
|
||||
if (v.DisplayVotes || _adminMgr.HasAdminFlag(player, AdminFlags.Moderator))
|
||||
{
|
||||
msg.DisplayVotes = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -12,6 +13,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
|
||||
namespace Content.Server.Voting;
|
||||
|
||||
@@ -24,6 +26,8 @@ public sealed class VotingSystem : EntitySystem
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly JobSystem _jobs = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -34,8 +38,7 @@ public sealed class VotingSystem : EntitySystem
|
||||
|
||||
private async void OnVotePlayerListRequestEvent(VotePlayerListRequestEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession.AttachedEntity is not { Valid: true } entity
|
||||
|| !await CheckVotekickInitEligibility(args.SenderSession))
|
||||
if (!await CheckVotekickInitEligibility(args.SenderSession))
|
||||
{
|
||||
var deniedResponse = new VotePlayerListResponseEvent(new (NetUserId, NetEntity, string)[0], true);
|
||||
RaiseNetworkEvent(deniedResponse, args.SenderSession.Channel);
|
||||
@@ -46,17 +49,23 @@ public sealed class VotingSystem : EntitySystem
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player.AttachedEntity is not { Valid: true } attached)
|
||||
continue;
|
||||
|
||||
if (attached == entity) continue;
|
||||
if (args.SenderSession == player) continue;
|
||||
|
||||
if (_adminManager.IsAdmin(player, false)) continue;
|
||||
|
||||
var playerName = GetPlayerVoteListName(attached);
|
||||
var netEntity = GetNetEntity(attached);
|
||||
if (player.AttachedEntity is not { Valid: true } attached)
|
||||
{
|
||||
var playerName = player.Name;
|
||||
var netEntity = NetEntity.Invalid;
|
||||
players.Add((player.UserId, netEntity, playerName));
|
||||
}
|
||||
else
|
||||
{
|
||||
var playerName = GetPlayerVoteListName(attached);
|
||||
var netEntity = GetNetEntity(attached);
|
||||
|
||||
players.Add((player.UserId, netEntity, playerName));
|
||||
players.Add((player.UserId, netEntity, playerName));
|
||||
}
|
||||
}
|
||||
|
||||
var response = new VotePlayerListResponseEvent(players.ToArray(), false);
|
||||
@@ -86,22 +95,29 @@ public sealed class VotingSystem : EntitySystem
|
||||
if (initiator.AttachedEntity != null && _adminManager.IsAdmin(initiator.AttachedEntity.Value, false))
|
||||
return true;
|
||||
|
||||
if (_cfg.GetCVar(CCVars.VotekickInitiatorGhostRequirement))
|
||||
// If cvar enabled, skip the ghost requirement in the preround lobby
|
||||
if (!_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) || (_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) && _gameTicker.RunLevel != GameRunLevel.PreRoundLobby))
|
||||
{
|
||||
// Must be ghost
|
||||
if (!TryComp(initiator.AttachedEntity, out GhostComponent? ghostComp))
|
||||
return false;
|
||||
if (_cfg.GetCVar(CCVars.VotekickInitiatorGhostRequirement))
|
||||
{
|
||||
// Must be ghost
|
||||
if (!TryComp(initiator.AttachedEntity, out GhostComponent? ghostComp))
|
||||
return false;
|
||||
|
||||
// Must have been dead for x seconds
|
||||
if ((int)_gameTiming.RealTime.Subtract(ghostComp.TimeOfDeath).TotalSeconds < _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime))
|
||||
return false;
|
||||
// Must have been dead for x seconds
|
||||
if ((int)_gameTiming.RealTime.Subtract(ghostComp.TimeOfDeath).TotalSeconds < _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Must be whitelisted
|
||||
if (!await _dbManager.GetWhitelistStatusAsync(initiator.UserId))
|
||||
if (!await _dbManager.GetWhitelistStatusAsync(initiator.UserId) && _cfg.GetCVar(CCVars.VotekickInitiatorWhitelistedRequirement))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
// Must be eligible to vote
|
||||
var playtime = _playtimeManager.GetPlayTimes(initiator);
|
||||
return playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) && (overallTime >= TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickEligibleVoterPlaytime))
|
||||
|| !_cfg.GetCVar(CCVars.VotekickInitiatorTimeRequirement));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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 sealed partial class GunSystem
|
||||
*/
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,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>
|
||||
|
||||
@@ -813,6 +813,9 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
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;
|
||||
|
||||
@@ -130,9 +130,6 @@ public sealed partial class SleepingSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
_blindableSystem.UpdateIsBlind(ent.Owner);
|
||||
_actionsSystem.AddAction(ent, ref ent.Comp.WakeAction, WakeActionId, ent);
|
||||
|
||||
// TODO remove hardcoded time.
|
||||
_actionsSystem.SetCooldown(ent.Comp.WakeAction, _gameTiming.CurTime, _gameTiming.CurTime + TimeSpan.FromSeconds(2f));
|
||||
}
|
||||
|
||||
private void OnSpeakAttempt(Entity<SleepingComponent> ent, ref SpeakAttemptEvent args)
|
||||
|
||||
@@ -461,6 +461,18 @@ namespace Content.Shared.CCVar
|
||||
* Discord
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// The role that will get mentioned if a new SOS ahelp comes in.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> DiscordAhelpMention =
|
||||
CVarDef.Create("discord.on_call_ping", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
||||
|
||||
/// <summary>
|
||||
/// URL of the discord webhook to relay unanswered ahelp messages.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> DiscordOnCallWebhook =
|
||||
CVarDef.Create("discord.on_call_webhook", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
||||
|
||||
/// <summary>
|
||||
/// URL of the Discord webhook which will relay all ahelp messages.
|
||||
/// </summary>
|
||||
@@ -1469,7 +1481,7 @@ namespace Content.Shared.CCVar
|
||||
/// Config for when the votekick should be allowed to be called based on number of eligible voters.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickEligibleNumberRequirement =
|
||||
CVarDef.Create("votekick.eligible_number", 10, CVar.SERVERONLY);
|
||||
CVarDef.Create("votekick.eligible_number", 5, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a votekick initiator must be a ghost or not.
|
||||
@@ -1477,6 +1489,18 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<bool> VotekickInitiatorGhostRequirement =
|
||||
CVarDef.Create("votekick.initiator_ghost_requirement", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Should the initiator be whitelisted to initiate a votekick?
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickInitiatorWhitelistedRequirement =
|
||||
CVarDef.Create("votekick.initiator_whitelist_requirement", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Should the initiator be able to start a votekick if they are bellow the votekick.voter_playtime requirement?
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickInitiatorTimeRequirement =
|
||||
CVarDef.Create("votekick.initiator_time_requirement", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a votekick voter must be a ghost or not.
|
||||
/// </summary>
|
||||
@@ -1493,7 +1517,7 @@ namespace Content.Shared.CCVar
|
||||
/// Config for how many seconds a player must have been dead to initiate a votekick / be able to vote on a votekick.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickEligibleVoterDeathtime =
|
||||
CVarDef.Create("votekick.voter_deathtime", 180, CVar.REPLICATED | CVar.SERVER);
|
||||
CVarDef.Create("votekick.voter_deathtime", 30, CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// The required ratio of eligible voters that must agree for a votekick to go through.
|
||||
@@ -1537,6 +1561,12 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<int> VotekickBanDuration =
|
||||
CVarDef.Create("votekick.ban_duration", 180, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the ghost requirement settings for votekicks should be ignored for the lobby.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickIgnoreGhostReqInLobby =
|
||||
CVarDef.Create("votekick.ignore_ghost_req_in_lobby", true, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* BAN
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed partial class CartridgeLoaderComponent : Component
|
||||
/// The maximum amount of programs that can be installed on the cartridge loader entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int DiskSpace = 5;
|
||||
public int DiskSpace = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the cartridge loader will play notifications if it supports it at all
|
||||
|
||||
@@ -17,4 +17,7 @@ public sealed class CartridgeUiMessage : BoundUserInterfaceMessage
|
||||
public abstract class CartridgeMessageEvent : EntityEventArgs
|
||||
{
|
||||
public NetEntity LoaderUid;
|
||||
|
||||
[NonSerialized]
|
||||
public EntityUid Actor;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Content.Shared.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a container that also contains a solution.
|
||||
/// This means that reactive entities react when inserted into the container.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ReactiveContainerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The container that holds the solution.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string Container = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The solution in the container.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string Solution = default!;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class ReactiveContainerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ReactiveContainerComponent, EntInsertedIntoContainerMessage>(OnInserted);
|
||||
SubscribeLocalEvent<ReactiveContainerComponent, SolutionContainerChangedEvent>(OnSolutionChange);
|
||||
}
|
||||
|
||||
private void OnInserted(EntityUid uid, ReactiveContainerComponent comp, EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
// Only reactive entities can react with the solution
|
||||
if (!HasComp<ReactiveComponent>(args.Entity))
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, comp.Solution, out _, out var solution))
|
||||
return;
|
||||
if (solution.Volume == 0)
|
||||
return;
|
||||
|
||||
_reactiveSystem.DoEntityReaction(args.Entity, solution, ReactionMethod.Touch);
|
||||
}
|
||||
|
||||
private void OnSolutionChange(EntityUid uid, ReactiveContainerComponent comp, SolutionContainerChangedEvent args)
|
||||
{
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, comp.Solution, out _, out var solution))
|
||||
return;
|
||||
if (solution.Volume == 0)
|
||||
return;
|
||||
if (!TryComp<ContainerManagerComponent>(uid, out var manager))
|
||||
return;
|
||||
if (!_containerSystem.TryGetContainer(uid, comp.Container, out var container))
|
||||
return;
|
||||
|
||||
foreach (var entity in container.ContainedEntities)
|
||||
{
|
||||
if (!HasComp<ReactiveComponent>(entity))
|
||||
continue;
|
||||
_reactiveSystem.DoEntityReaction(entity, solution, ReactionMethod.Touch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
}
|
||||
|
||||
#region ComponentManagement
|
||||
|
||||
/// <summary>
|
||||
/// Spawn in starting items for any item slots that should have one.
|
||||
/// </summary>
|
||||
@@ -70,7 +71,8 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
if (slot.HasItem || string.IsNullOrEmpty(slot.StartingItem))
|
||||
continue;
|
||||
|
||||
var item = EntityManager.SpawnEntity(slot.StartingItem, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
|
||||
var item = Spawn(slot.StartingItem, Transform(uid).Coordinates);
|
||||
|
||||
if (slot.ContainerSlot != null)
|
||||
_containers.Insert(item, slot.ContainerSlot);
|
||||
}
|
||||
@@ -99,7 +101,8 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
if (itemSlots.Slots.TryGetValue(id, out var existing))
|
||||
{
|
||||
if (existing.Local)
|
||||
Log.Error($"Duplicate item slot key. Entity: {EntityManager.GetComponent<MetaDataComponent>(uid).EntityName} ({uid}), key: {id}");
|
||||
Log.Error(
|
||||
$"Duplicate item slot key. Entity: {EntityManager.GetComponent<MetaDataComponent>(uid).EntityName} ({uid}), key: {id}");
|
||||
else
|
||||
// server state takes priority
|
||||
slot.CopyFrom(existing);
|
||||
@@ -134,7 +137,10 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
Dirty(uid, itemSlots);
|
||||
}
|
||||
|
||||
public bool TryGetSlot(EntityUid uid, string slotId, [NotNullWhen(true)] out ItemSlot? itemSlot, ItemSlotsComponent? component = null)
|
||||
public bool TryGetSlot(EntityUid uid,
|
||||
string slotId,
|
||||
[NotNullWhen(true)] out ItemSlot? itemSlot,
|
||||
ItemSlotsComponent? component = null)
|
||||
{
|
||||
itemSlot = null;
|
||||
|
||||
@@ -143,9 +149,11 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
|
||||
return component.Slots.TryGetValue(slotId, out itemSlot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interactions
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to take an item from a slot, if any are set to EjectOnInteract.
|
||||
/// </summary>
|
||||
@@ -201,20 +209,50 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
if (!EntityManager.TryGetComponent(args.User, out HandsComponent? hands))
|
||||
return;
|
||||
|
||||
if (itemSlots.Slots.Count == 0)
|
||||
return;
|
||||
|
||||
// If any slot can be inserted into don't show popup.
|
||||
// If any whitelist passes, but slot is locked, then show locked.
|
||||
// If whitelist fails all, show whitelist fail.
|
||||
|
||||
// valid, insertable slots (if any)
|
||||
var slots = new List<ItemSlot>();
|
||||
|
||||
string? whitelistFailPopup = null;
|
||||
string? lockedFailPopup = null;
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (!slot.InsertOnInteract)
|
||||
continue;
|
||||
|
||||
if (!CanInsert(uid, args.Used, args.User, slot, swap: slot.Swap, popup: args.User))
|
||||
continue;
|
||||
if (CanInsert(uid, args.Used, args.User, slot, slot.Swap))
|
||||
{
|
||||
slots.Add(slot);
|
||||
}
|
||||
else
|
||||
{
|
||||
var allowed = CanInsertWhitelist(args.Used, slot);
|
||||
if (lockedFailPopup == null && slot.LockedFailPopup != null && allowed && slot.Locked)
|
||||
lockedFailPopup = slot.LockedFailPopup;
|
||||
|
||||
slots.Add(slot);
|
||||
if (whitelistFailPopup == null && slot.WhitelistFailPopup != null)
|
||||
whitelistFailPopup = slot.WhitelistFailPopup;
|
||||
}
|
||||
}
|
||||
|
||||
if (slots.Count == 0)
|
||||
{
|
||||
// it's a bit weird that the popupMessage is stored with the item slots themselves, but in practice
|
||||
// the popup messages will just all be the same, so it's probably fine.
|
||||
//
|
||||
// doing a check to make sure that they're all the same or something is probably frivolous
|
||||
if (lockedFailPopup != null)
|
||||
_popupSystem.PopupClient(Loc.GetString(lockedFailPopup), uid, args.User);
|
||||
else if (whitelistFailPopup != null)
|
||||
_popupSystem.PopupClient(Loc.GetString(whitelistFailPopup), uid, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop the held item onto the floor. Return if the user cannot drop.
|
||||
if (!_handsSystem.TryDrop(args.User, args.Used, handsComp: hands))
|
||||
@@ -236,23 +274,31 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Insert
|
||||
|
||||
/// <summary>
|
||||
/// Insert an item into a slot. This does not perform checks, so make sure to also use <see
|
||||
/// cref="CanInsert"/> or just use <see cref="TryInsert"/> instead.
|
||||
/// </summary>
|
||||
/// <param name="excludeUserAudio">If true, will exclude the user when playing sound. Does nothing client-side.
|
||||
/// Useful for predicted interactions</param>
|
||||
private void Insert(EntityUid uid, ItemSlot slot, EntityUid item, EntityUid? user, bool excludeUserAudio = false)
|
||||
private void Insert(EntityUid uid,
|
||||
ItemSlot slot,
|
||||
EntityUid item,
|
||||
EntityUid? user,
|
||||
bool excludeUserAudio = false)
|
||||
{
|
||||
bool? inserted = slot.ContainerSlot != null ? _containers.Insert(item, slot.ContainerSlot) : null;
|
||||
// ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
|
||||
|
||||
// Logging
|
||||
if (inserted != null && inserted.Value && user != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user.Value)} inserted {ToPrettyString(item)} into {slot.ContainerSlot?.ID + " slot of "}{ToPrettyString(uid)}");
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(user.Value)} inserted {ToPrettyString(item)} into {slot.ContainerSlot?.ID + " slot of "}{ToPrettyString(uid)}");
|
||||
|
||||
_audioSystem.PlayPredicted(slot.InsertSound, uid, excludeUserAudio ? user : null);
|
||||
}
|
||||
@@ -261,46 +307,53 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
/// Check whether a given item can be inserted into a slot. Unless otherwise specified, this will return
|
||||
/// false if the slot is already filled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a popup entity is given, and if the item slot is set to generate a popup message when it fails to
|
||||
/// pass the whitelist or due to slot being locked, then this will generate an appropriate popup.
|
||||
/// </remarks>
|
||||
public bool CanInsert(EntityUid uid, EntityUid usedUid, EntityUid? user, ItemSlot slot, bool swap = false, EntityUid? popup = null)
|
||||
public bool CanInsert(EntityUid uid,
|
||||
EntityUid usedUid,
|
||||
EntityUid? user,
|
||||
ItemSlot slot,
|
||||
bool swap = false)
|
||||
{
|
||||
if (slot.ContainerSlot == null)
|
||||
return false;
|
||||
|
||||
if (_whitelistSystem.IsWhitelistFail(slot.Whitelist, usedUid) || _whitelistSystem.IsBlacklistPass(slot.Blacklist, usedUid))
|
||||
{
|
||||
if (popup.HasValue && slot.WhitelistFailPopup.HasValue)
|
||||
_popupSystem.PopupClient(Loc.GetString(slot.WhitelistFailPopup), uid, popup.Value);
|
||||
if (slot.HasItem && (!swap || swap && !CanEject(uid, user, slot)))
|
||||
return false;
|
||||
|
||||
if (!CanInsertWhitelist(usedUid, slot))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.Locked)
|
||||
{
|
||||
if (popup.HasValue && slot.LockedFailPopup.HasValue)
|
||||
_popupSystem.PopupClient(Loc.GetString(slot.LockedFailPopup), uid, popup.Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.HasItem && (!swap || (swap && !CanEject(uid, user, slot))))
|
||||
return false;
|
||||
|
||||
var ev = new ItemSlotInsertAttemptEvent(uid, usedUid, user, slot);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
RaiseLocalEvent(usedUid, ref ev);
|
||||
if (ev.Cancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _containers.CanInsert(usedUid, slot.ContainerSlot, assumeEmpty: swap);
|
||||
}
|
||||
|
||||
private bool CanInsertWhitelist(EntityUid usedUid, ItemSlot slot)
|
||||
{
|
||||
if (_whitelistSystem.IsWhitelistFail(slot.Whitelist, usedUid)
|
||||
|| _whitelistSystem.IsBlacklistPass(slot.Blacklist, usedUid))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to insert item into a specific slot.
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsert(EntityUid uid, string id, EntityUid item, EntityUid? user, ItemSlotsComponent? itemSlots = null, bool excludeUserAudio = false)
|
||||
public bool TryInsert(EntityUid uid,
|
||||
string id,
|
||||
EntityUid item,
|
||||
EntityUid? user,
|
||||
ItemSlotsComponent? itemSlots = null,
|
||||
bool excludeUserAudio = false)
|
||||
{
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return false;
|
||||
@@ -315,7 +368,11 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
/// Tries to insert item into a specific slot.
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsert(EntityUid uid, ItemSlot slot, EntityUid item, EntityUid? user, bool excludeUserAudio = false)
|
||||
public bool TryInsert(EntityUid uid,
|
||||
ItemSlot slot,
|
||||
EntityUid item,
|
||||
EntityUid? user,
|
||||
bool excludeUserAudio = false)
|
||||
{
|
||||
if (!CanInsert(uid, item, user, slot))
|
||||
return false;
|
||||
@@ -329,7 +386,11 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
/// Does not check action blockers.
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsertFromHand(EntityUid uid, ItemSlot slot, EntityUid user, HandsComponent? hands = null, bool excludeUserAudio = false)
|
||||
public bool TryInsertFromHand(EntityUid uid,
|
||||
ItemSlot slot,
|
||||
EntityUid user,
|
||||
HandsComponent? hands = null,
|
||||
bool excludeUserAudio = false)
|
||||
{
|
||||
if (!Resolve(user, ref hands, false))
|
||||
return false;
|
||||
@@ -443,6 +504,7 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Eject
|
||||
@@ -462,7 +524,7 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.ContainerSlot?.ContainedEntity is not {} item)
|
||||
if (slot.ContainerSlot?.ContainedEntity is not { } item)
|
||||
return false;
|
||||
|
||||
var ev = new ItemSlotEjectAttemptEvent(uid, item, user, slot);
|
||||
@@ -487,7 +549,9 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
|
||||
// Logging
|
||||
if (ejected != null && ejected.Value && user != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user.Value)} ejected {ToPrettyString(item)} from {slot.ContainerSlot?.ID + " slot of "}{ToPrettyString(uid)}");
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(user.Value)} ejected {ToPrettyString(item)} from {slot.ContainerSlot?.ID + " slot of "}{ToPrettyString(uid)}");
|
||||
|
||||
_audioSystem.PlayPredicted(slot.EjectSound, uid, excludeUserAudio ? user : null);
|
||||
}
|
||||
@@ -496,7 +560,11 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
/// Try to eject an item from a slot.
|
||||
/// </summary>
|
||||
/// <returns>False if item slot is locked or has no item inserted</returns>
|
||||
public bool TryEject(EntityUid uid, ItemSlot slot, EntityUid? user, [NotNullWhen(true)] out EntityUid? item, bool excludeUserAudio = false)
|
||||
public bool TryEject(EntityUid uid,
|
||||
ItemSlot slot,
|
||||
EntityUid? user,
|
||||
[NotNullWhen(true)] out EntityUid? item,
|
||||
bool excludeUserAudio = false)
|
||||
{
|
||||
item = null;
|
||||
|
||||
@@ -518,8 +586,12 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
/// Try to eject item from a slot.
|
||||
/// </summary>
|
||||
/// <returns>False if the id is not valid, the item slot is locked, or it has no item inserted</returns>
|
||||
public bool TryEject(EntityUid uid, string id, EntityUid? user,
|
||||
[NotNullWhen(true)] out EntityUid? item, ItemSlotsComponent? itemSlots = null, bool excludeUserAudio = false)
|
||||
public bool TryEject(EntityUid uid,
|
||||
string id,
|
||||
EntityUid? user,
|
||||
[NotNullWhen(true)] out EntityUid? item,
|
||||
ItemSlotsComponent? itemSlots = null,
|
||||
bool excludeUserAudio = false)
|
||||
{
|
||||
item = null;
|
||||
|
||||
@@ -550,12 +622,16 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Verbs
|
||||
private void AddAlternativeVerbs(EntityUid uid, ItemSlotsComponent itemSlots, GetVerbsEvent<AlternativeVerb> args)
|
||||
|
||||
private void AddAlternativeVerbs(EntityUid uid,
|
||||
ItemSlotsComponent itemSlots,
|
||||
GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (args.Hands == null || !args.CanAccess ||!args.CanInteract)
|
||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -649,7 +725,9 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInteractionVerbsVerbs(EntityUid uid, ItemSlotsComponent itemSlots, GetVerbsEvent<InteractionVerb> args)
|
||||
private void AddInteractionVerbsVerbs(EntityUid uid,
|
||||
ItemSlotsComponent itemSlots,
|
||||
GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
@@ -708,7 +786,7 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
new SpriteSpecifier.Texture(
|
||||
new ResPath("/Textures/Interface/VerbIcons/insert.svg.192dpi.png"));
|
||||
}
|
||||
else if(slot.EjectOnInteract)
|
||||
else if (slot.EjectOnInteract)
|
||||
{
|
||||
// Inserting/ejecting is a primary interaction for this entity. Instead of using the insert
|
||||
// category, we will use a single "Place <item>" verb.
|
||||
@@ -727,9 +805,11 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
args.Verbs.Add(insertVerb);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BUIs
|
||||
|
||||
private void HandleButtonPressed(EntityUid uid, ItemSlotsComponent component, ItemSlotButtonPressedEvent args)
|
||||
{
|
||||
if (!component.Slots.TryGetValue(args.SlotId, out var slot))
|
||||
@@ -740,6 +820,7 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
else if (args.TryInsert && !slot.HasItem)
|
||||
TryInsertFromHand(uid, slot, args.Actor);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -47,7 +47,7 @@ public abstract partial class InventorySystem
|
||||
|
||||
private void OnEntRemoved(EntityUid uid, InventoryComponent component, EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if(!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component))
|
||||
if (!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component))
|
||||
return;
|
||||
|
||||
var unequippedEvent = new DidUnequipEvent(uid, args.Entity, slotDef);
|
||||
@@ -59,8 +59,8 @@ public abstract partial class InventorySystem
|
||||
|
||||
private void OnEntInserted(EntityUid uid, InventoryComponent component, EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
if(!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component))
|
||||
return;
|
||||
if (!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component))
|
||||
return;
|
||||
|
||||
var equippedEvent = new DidEquipEvent(uid, args.Entity, slotDef);
|
||||
RaiseLocalEvent(uid, equippedEvent, true);
|
||||
@@ -118,7 +118,7 @@ public abstract partial class InventorySystem
|
||||
|
||||
RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor));
|
||||
|
||||
TryEquip(actor, actor, held.Value, ev.Slot, predicted: true, inventory: inventory, force: true, checkDoafter:true);
|
||||
TryEquip(actor, actor, held.Value, ev.Slot, predicted: true, inventory: inventory, force: true, checkDoafter: true);
|
||||
}
|
||||
|
||||
public bool TryEquip(EntityUid uid, EntityUid itemUid, string slot, bool silent = false, bool force = false, bool predicted = false,
|
||||
@@ -365,6 +365,25 @@ public abstract partial class InventorySystem
|
||||
ClothingComponent? clothing = null,
|
||||
bool reparent = true,
|
||||
bool checkDoafter = false)
|
||||
{
|
||||
var itemsDropped = 0;
|
||||
return TryUnequip(actor, target, slot, out removedItem, ref itemsDropped,
|
||||
silent, force, predicted, inventory, clothing, reparent, checkDoafter);
|
||||
}
|
||||
|
||||
private bool TryUnequip(
|
||||
EntityUid actor,
|
||||
EntityUid target,
|
||||
string slot,
|
||||
[NotNullWhen(true)] out EntityUid? removedItem,
|
||||
ref int itemsDropped,
|
||||
bool silent = false,
|
||||
bool force = false,
|
||||
bool predicted = false,
|
||||
InventoryComponent? inventory = null,
|
||||
ClothingComponent? clothing = null,
|
||||
bool reparent = true,
|
||||
bool checkDoafter = false)
|
||||
{
|
||||
removedItem = null;
|
||||
|
||||
@@ -423,17 +442,27 @@ public abstract partial class InventorySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_containerSystem.Remove(removedItem.Value, slotContainer, force: force, reparent: reparent))
|
||||
return false;
|
||||
|
||||
// this is in order to keep track of whether this is the first instance of a recursion call
|
||||
var firstRun = itemsDropped == 0;
|
||||
++itemsDropped;
|
||||
|
||||
foreach (var slotDef in inventory.Slots)
|
||||
{
|
||||
if (slotDef != slotDefinition && slotDef.DependsOn == slotDefinition.Name)
|
||||
{
|
||||
//this recursive call might be risky
|
||||
TryUnequip(actor, target, slotDef.Name, true, true, predicted, inventory, reparent: reparent);
|
||||
TryUnequip(actor, target, slotDef.Name, out _, ref itemsDropped, true, true, predicted, inventory, reparent: reparent);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_containerSystem.Remove(removedItem.Value, slotContainer, force: force, reparent: reparent))
|
||||
return false;
|
||||
// we check if any items were dropped, and make a popup if they were.
|
||||
// the reason we check for > 1 is because the first item is always the one we are trying to unequip,
|
||||
// whereas we only want to notify for extra dropped items.
|
||||
if (!silent && _gameTiming.IsFirstTimePredicted && firstRun && itemsDropped > 1)
|
||||
_popup.PopupClient(Loc.GetString("inventory-component-dropped-from-unequip", ("items", itemsDropped - 1)), target, target);
|
||||
|
||||
// TODO: Inventory needs a hot cleanup hoo boy
|
||||
// Check if something else (AKA toggleable) dumped it into a container.
|
||||
@@ -466,7 +495,7 @@ public abstract partial class InventorySystem
|
||||
if ((containerSlot == null || slotDefinition == null) && !TryGetSlotContainer(target, slot, out containerSlot, out slotDefinition, inventory))
|
||||
return false;
|
||||
|
||||
if (containerSlot.ContainedEntity is not {} itemUid)
|
||||
if (containerSlot.ContainedEntity is not { } itemUid)
|
||||
return false;
|
||||
|
||||
if (!_containerSystem.CanRemove(itemUid, containerSlot))
|
||||
|
||||
@@ -13,14 +13,16 @@ public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage
|
||||
public float BloodLevel;
|
||||
public bool? ScanMode;
|
||||
public bool? Bleeding;
|
||||
public bool? Unrevivable;
|
||||
|
||||
public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding)
|
||||
public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable)
|
||||
{
|
||||
TargetEntity = targetEntity;
|
||||
Temperature = temperature;
|
||||
BloodLevel = bloodLevel;
|
||||
ScanMode = scanMode;
|
||||
Bleeding = bleeding;
|
||||
Unrevivable = unrevivable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,24 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
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>
|
||||
|
||||
@@ -33,18 +33,10 @@ public sealed class HungerSystem : EntitySystem
|
||||
[ValidatePrototypeId<SatiationIconPrototype>]
|
||||
private const string HungerIconStarvingId = "HungerIconStarving";
|
||||
|
||||
private SatiationIconPrototype? _hungerIconOverfed;
|
||||
private SatiationIconPrototype? _hungerIconPeckish;
|
||||
private SatiationIconPrototype? _hungerIconStarving;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
DebugTools.Assert(_prototype.TryIndex(HungerIconOverfedId, out _hungerIconOverfed) &&
|
||||
_prototype.TryIndex(HungerIconPeckishId, out _hungerIconPeckish) &&
|
||||
_prototype.TryIndex(HungerIconStarvingId, out _hungerIconStarving));
|
||||
|
||||
SubscribeLocalEvent<HungerComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<HungerComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<HungerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
@@ -221,13 +213,13 @@ public sealed class HungerSystem : EntitySystem
|
||||
switch (component.CurrentThreshold)
|
||||
{
|
||||
case HungerThreshold.Overfed:
|
||||
prototype = _hungerIconOverfed;
|
||||
_prototype.TryIndex(HungerIconOverfedId, out prototype);
|
||||
break;
|
||||
case HungerThreshold.Peckish:
|
||||
prototype = _hungerIconPeckish;
|
||||
_prototype.TryIndex(HungerIconPeckishId, out prototype);
|
||||
break;
|
||||
case HungerThreshold.Starving:
|
||||
prototype = _hungerIconStarving;
|
||||
_prototype.TryIndex(HungerIconStarvingId, out prototype);
|
||||
break;
|
||||
default:
|
||||
prototype = null;
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
@@ -31,18 +32,10 @@ public sealed class ThirstSystem : EntitySystem
|
||||
[ValidatePrototypeId<SatiationIconPrototype>]
|
||||
private const string ThirstIconParchedId = "ThirstIconParched";
|
||||
|
||||
private SatiationIconPrototype? _thirstIconOverhydrated = null;
|
||||
private SatiationIconPrototype? _thirstIconThirsty = null;
|
||||
private SatiationIconPrototype? _thirstIconParched = null;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
DebugTools.Assert(_prototype.TryIndex(ThirstIconOverhydratedId, out _thirstIconOverhydrated) &&
|
||||
_prototype.TryIndex(ThirstIconThirstyId, out _thirstIconThirsty) &&
|
||||
_prototype.TryIndex(ThirstIconParchedId, out _thirstIconParched));
|
||||
|
||||
SubscribeLocalEvent<ThirstComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
SubscribeLocalEvent<ThirstComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<ThirstComponent, RejuvenateEvent>(OnRejuvenate);
|
||||
@@ -128,26 +121,28 @@ public sealed class ThirstSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetStatusIconPrototype(ThirstComponent component, out SatiationIconPrototype? prototype)
|
||||
public bool TryGetStatusIconPrototype(ThirstComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype)
|
||||
{
|
||||
switch (component.CurrentThirstThreshold)
|
||||
{
|
||||
case ThirstThreshold.OverHydrated:
|
||||
prototype = _thirstIconOverhydrated;
|
||||
return true;
|
||||
_prototype.TryIndex(ThirstIconOverhydratedId, out prototype);
|
||||
break;
|
||||
|
||||
case ThirstThreshold.Thirsty:
|
||||
prototype = _thirstIconThirsty;
|
||||
return true;
|
||||
_prototype.TryIndex(ThirstIconThirstyId, out prototype);
|
||||
break;
|
||||
|
||||
case ThirstThreshold.Parched:
|
||||
prototype = _thirstIconParched;
|
||||
return true;
|
||||
_prototype.TryIndex(ThirstIconParchedId, out prototype);
|
||||
break;
|
||||
|
||||
default:
|
||||
prototype = null;
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
return prototype != null;
|
||||
}
|
||||
|
||||
private void UpdateEffects(EntityUid uid, ThirstComponent component)
|
||||
|
||||
@@ -2,7 +2,6 @@ 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,8 +14,11 @@ public sealed partial class ShowHealthBarsComponent : Component
|
||||
/// <summary>
|
||||
/// Displays health bars of the damage containers.
|
||||
/// </summary>
|
||||
[DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageContainerPrototype>))]
|
||||
public List<string> DamageContainers = new();
|
||||
[DataField]
|
||||
public List<ProtoId<DamageContainerPrototype>> DamageContainers = new()
|
||||
{
|
||||
"Biological"
|
||||
};
|
||||
|
||||
[DataField]
|
||||
public ProtoId<HealthIconPrototype>? HealthStatusIcon = "HealthIconFine";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Overlays;
|
||||
|
||||
@@ -13,6 +13,9 @@ public sealed partial class ShowHealthIconsComponent : Component
|
||||
/// <summary>
|
||||
/// Displays health status icons of the damage containers.
|
||||
/// </summary>
|
||||
[DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageContainerPrototype>))]
|
||||
public List<string> DamageContainers = new();
|
||||
[DataField]
|
||||
public List<ProtoId<DamageContainerPrototype>> DamageContainers = new()
|
||||
{
|
||||
"Biological"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Polymorph.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -7,9 +8,22 @@ namespace Content.Shared.Polymorph.Components;
|
||||
/// Component added to disguise entities.
|
||||
/// Used by client to copy over appearance from the disguise's source entity.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedChameleonProjectorSystem))]
|
||||
[AutoGenerateComponentState(true)]
|
||||
public sealed partial class ChameleonDisguiseComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The user of this disguise.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid User;
|
||||
|
||||
/// <summary>
|
||||
/// The projector that created this disguise.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid Projector;
|
||||
|
||||
/// <summary>
|
||||
/// The disguise source entity for copying the sprite.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using Content.Shared.Polymorph.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Polymorph.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to a player when they use a chameleon projector.
|
||||
/// Handles making them invisible and revealing when damaged enough or switching hands.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedChameleonProjectorSystem))]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class ChameleonDisguisedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The disguise entity parented to the player.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid Disguise;
|
||||
|
||||
/// <summary>
|
||||
/// For client, whether the user's sprite was previously visible or not.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool WasVisible;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Polymorph.Systems;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -25,22 +24,26 @@ public sealed partial class ChameleonProjectorComponent : Component
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// <summary>
|
||||
/// Polymorph configuration for the disguise entity.
|
||||
/// Disguise entity to spawn and use.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public PolymorphConfiguration Polymorph = new();
|
||||
public EntProtoId DisguiseProto = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Action for disabling your disguise's rotation.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId NoRotAction = "ActionDisguiseNoRot";
|
||||
[DataField]
|
||||
public EntityUid? NoRotActionEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Action for anchoring your disguise in place.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId AnchorAction = "ActionDisguiseAnchor";
|
||||
[DataField]
|
||||
public EntityUid? AnchorActionEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum health to give the disguise.
|
||||
@@ -55,14 +58,8 @@ public sealed partial class ChameleonProjectorComponent : Component
|
||||
public float MaxHealth = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// Popup shown to the user when they try to disguise as an invalid entity.
|
||||
/// User currently disguised by this projector, if any
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId InvalidPopup = "chameleon-projector-invalid";
|
||||
|
||||
/// <summary>
|
||||
/// Popup shown to the user when they disguise as a valid entity.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId SuccessPopup = "chameleon-projector-success";
|
||||
public EntityUid? Disguised;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,264 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Coordinates;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Polymorph.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Shared.Polymorph.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles whitelist/blacklist checking.
|
||||
/// Actual polymorphing and deactivation is done serverside.
|
||||
/// Handles disguise validation, disguising and revealing.
|
||||
/// Most appearance copying is done clientside.
|
||||
/// </summary>
|
||||
public abstract class SharedChameleonProjectorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISerializationManager _serMan = default!;
|
||||
[Dependency] private readonly MetaDataSystem _meta = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, InteractHandEvent>(OnDisguiseInteractHand, before: [typeof(SharedItemSystem)]);
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, DamageChangedEvent>(OnDisguiseDamaged);
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, InsertIntoEntityStorageAttemptEvent>(OnDisguiseInsertAttempt);
|
||||
SubscribeLocalEvent<ChameleonDisguiseComponent, ComponentShutdown>(OnDisguiseShutdown);
|
||||
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, AfterInteractEvent>(OnInteract);
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, GetVerbsEvent<UtilityVerb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, DisguiseToggleNoRotEvent>(OnToggleNoRot);
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, DisguiseToggleAnchoredEvent>(OnToggleAnchored);
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, HandDeselectedEvent>(OnDeselected);
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, GotUnequippedHandEvent>(OnUnequipped);
|
||||
SubscribeLocalEvent<ChameleonProjectorComponent, ComponentShutdown>(OnProjectorShutdown);
|
||||
}
|
||||
|
||||
#region Disguise entity
|
||||
|
||||
private void OnDisguiseInteractHand(Entity<ChameleonDisguiseComponent> ent, ref InteractHandEvent args)
|
||||
{
|
||||
TryReveal(ent.Comp.User);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDisguiseDamaged(Entity<ChameleonDisguiseComponent> ent, ref DamageChangedEvent args)
|
||||
{
|
||||
// this mirrors damage 1:1
|
||||
if (args.DamageDelta is {} damage)
|
||||
_damageable.TryChangeDamage(ent.Comp.User, damage);
|
||||
}
|
||||
|
||||
private void OnDisguiseInsertAttempt(Entity<ChameleonDisguiseComponent> ent, ref InsertIntoEntityStorageAttemptEvent args)
|
||||
{
|
||||
// stay parented to the user, not the storage
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnDisguiseShutdown(Entity<ChameleonDisguiseComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_actions.RemoveProvidedActions(ent.Comp.User, ent.Comp.Projector);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Projector
|
||||
|
||||
private void OnInteract(Entity<ChameleonProjectorComponent> ent, ref AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Target is not {} target)
|
||||
if (args.Handled || !args.CanReach || args.Target is not {} target)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
TryDisguise(ent, args.User, target);
|
||||
}
|
||||
|
||||
private void OnGetVerbs(Entity<ChameleonProjectorComponent> ent, ref GetVerbsEvent<UtilityVerb> args)
|
||||
{
|
||||
if (!args.CanAccess)
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
args.Handled = true;
|
||||
var target = args.Target;
|
||||
args.Verbs.Add(new UtilityVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryDisguise(ent, user, target);
|
||||
},
|
||||
Text = Loc.GetString("chameleon-projector-set-disguise")
|
||||
});
|
||||
}
|
||||
|
||||
public bool TryDisguise(Entity<ChameleonProjectorComponent> ent, EntityUid user, EntityUid target)
|
||||
{
|
||||
if (_container.IsEntityInContainer(target))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("chameleon-projector-inside-container"), target, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsInvalid(ent.Comp, target))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString(ent.Comp.InvalidPopup), target, user);
|
||||
return;
|
||||
_popup.PopupClient(Loc.GetString("chameleon-projector-invalid"), target, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
_popup.PopupClient(Loc.GetString(ent.Comp.SuccessPopup), target, user);
|
||||
Disguise(ent.Comp, user, target);
|
||||
_popup.PopupClient(Loc.GetString("chameleon-projector-success"), target, user);
|
||||
Disguise(ent, user, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnToggleNoRot(Entity<ChameleonProjectorComponent> ent, ref DisguiseToggleNoRotEvent args)
|
||||
{
|
||||
if (ent.Comp.Disguised is not {} uid)
|
||||
return;
|
||||
|
||||
var xform = Transform(uid);
|
||||
_xform.SetLocalRotationNoLerp(uid, 0, xform);
|
||||
xform.NoLocalRotation = !xform.NoLocalRotation;
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnToggleAnchored(Entity<ChameleonProjectorComponent> ent, ref DisguiseToggleAnchoredEvent args)
|
||||
{
|
||||
if (ent.Comp.Disguised is not {} uid)
|
||||
return;
|
||||
|
||||
var xform = Transform(uid);
|
||||
if (xform.Anchored)
|
||||
_xform.Unanchor(uid, xform);
|
||||
else
|
||||
_xform.AnchorEntity((uid, xform));
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDeselected(Entity<ChameleonProjectorComponent> ent, ref HandDeselectedEvent args)
|
||||
{
|
||||
RevealProjector(ent);
|
||||
}
|
||||
|
||||
private void OnUnequipped(Entity<ChameleonProjectorComponent> ent, ref GotUnequippedHandEvent args)
|
||||
{
|
||||
RevealProjector(ent);
|
||||
}
|
||||
|
||||
private void OnProjectorShutdown(Entity<ChameleonProjectorComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
RevealProjector(ent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region API
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an entity cannot be used as a disguise.
|
||||
/// </summary>
|
||||
public bool IsInvalid(ChameleonProjectorComponent comp, EntityUid target)
|
||||
{
|
||||
return _whitelistSystem.IsWhitelistFail(comp.Whitelist, target)
|
||||
|| _whitelistSystem.IsBlacklistPass(comp.Blacklist, target);
|
||||
return _whitelist.IsWhitelistFail(comp.Whitelist, target)
|
||||
|| _whitelist.IsBlacklistPass(comp.Blacklist, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On server, polymorphs the user into an entity and sets up the disguise.
|
||||
/// </summary>
|
||||
public virtual void Disguise(ChameleonProjectorComponent comp, EntityUid user, EntityUid entity)
|
||||
public void Disguise(Entity<ChameleonProjectorComponent> ent, EntityUid user, EntityUid entity)
|
||||
{
|
||||
var proj = ent.Comp;
|
||||
|
||||
// no spawning prediction sorry
|
||||
if (_net.IsClient)
|
||||
return;
|
||||
|
||||
// reveal first to allow quick switching
|
||||
TryReveal(user);
|
||||
|
||||
// add actions for controlling transform aspects
|
||||
_actions.AddAction(user, ref proj.NoRotActionEntity, proj.NoRotAction, container: ent);
|
||||
_actions.AddAction(user, ref proj.AnchorActionEntity, proj.AnchorAction, container: ent);
|
||||
|
||||
proj.Disguised = user;
|
||||
|
||||
var disguise = SpawnAttachedTo(proj.DisguiseProto, user.ToCoordinates());
|
||||
|
||||
var disguised = AddComp<ChameleonDisguisedComponent>(user);
|
||||
disguised.Disguise = disguise;
|
||||
Dirty(user, disguised);
|
||||
|
||||
// make disguise look real (for simple things at least)
|
||||
var meta = MetaData(entity);
|
||||
_meta.SetEntityName(disguise, meta.EntityName);
|
||||
_meta.SetEntityDescription(disguise, meta.EntityDescription);
|
||||
|
||||
var comp = EnsureComp<ChameleonDisguiseComponent>(disguise);
|
||||
comp.User = user;
|
||||
comp.Projector = ent;
|
||||
comp.SourceEntity = entity;
|
||||
comp.SourceProto = Prototype(entity)?.ID;
|
||||
Dirty(disguise, comp);
|
||||
|
||||
// item disguises can be picked up to be revealed, also makes sure their examine size is correct
|
||||
CopyComp<ItemComponent>((disguise, comp));
|
||||
|
||||
_appearance.CopyData(entity, disguise);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the disguise, if the user is disguised.
|
||||
/// </summary>
|
||||
public bool TryReveal(Entity<ChameleonDisguisedComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
if (TryComp<ChameleonDisguiseComponent>(ent.Comp.Disguise, out var disguise)
|
||||
&& TryComp<ChameleonProjectorComponent>(disguise.Projector, out var proj))
|
||||
{
|
||||
proj.Disguised = null;
|
||||
}
|
||||
|
||||
var xform = Transform(ent);
|
||||
xform.NoLocalRotation = false;
|
||||
_xform.Unanchor(ent, xform);
|
||||
|
||||
Del(ent.Comp.Disguise);
|
||||
RemComp<ChameleonDisguisedComponent>(ent);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reveal a projector's user, if any.
|
||||
/// </summary>
|
||||
public void RevealProjector(Entity<ChameleonProjectorComponent> ent)
|
||||
{
|
||||
if (ent.Comp.Disguised is {} user)
|
||||
TryReveal(user);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Copy a component from the source entity/prototype to the disguise entity.
|
||||
/// </summary>
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed partial class SiliconLawProviderComponent : Component
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays for the Silicon player
|
||||
/// when the particular lawboard has been inserted.
|
||||
/// when the law change is processed for the provider.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? LawUploadSound = new SoundPathSpecifier("/Audio/Misc/cryo_warning.ogg");
|
||||
|
||||
@@ -324,6 +324,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
|
||||
if (TryComp(user, out EyeComponent? eyeComp))
|
||||
{
|
||||
_eye.SetDrawFov(user, false, eyeComp);
|
||||
_eye.SetTarget(user, ent.Comp.RemoteEntity.Value, eyeComp);
|
||||
}
|
||||
|
||||
@@ -356,6 +357,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
|
||||
if (TryComp(args.Entity, out EyeComponent? eyeComp))
|
||||
{
|
||||
_eye.SetDrawFov(args.Entity, true, eyeComp);
|
||||
_eye.SetTarget(args.Entity, null, eyeComp);
|
||||
}
|
||||
ClearEye(ent);
|
||||
|
||||
@@ -22,6 +22,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");
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -156,6 +157,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>
|
||||
@@ -232,6 +257,12 @@ public sealed partial class GunComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ClumsyProof = false;
|
||||
|
||||
/// <summary>
|
||||
/// Firing direction for an item not being held (e.g. shuttle cannons, thrown guns still firing).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 DefaultDirection = new Vector2(0, -1);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -67,7 +67,10 @@ public abstract partial class SharedGunSystem
|
||||
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 @@ public abstract partial class SharedGunSystem
|
||||
/// </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()
|
||||
|
||||
@@ -218,7 +218,7 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
/// </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;
|
||||
@@ -258,6 +258,9 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
|
||||
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.
|
||||
@@ -278,18 +281,24 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
|
||||
// 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);
|
||||
@@ -301,7 +310,8 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -328,6 +338,10 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
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)
|
||||
@@ -347,6 +361,22 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
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);
|
||||
var shotEv = new GunShotEvent(user, ev.Ammo);
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Examine;
|
||||
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;
|
||||
@@ -125,7 +126,7 @@ public sealed class WieldableSystem : EntitySystem
|
||||
|
||||
private void OnExamine(EntityUid uid, GunWieldBonusComponent component, ref ExaminedEvent args)
|
||||
{
|
||||
if (HasComp<GunRequiresWieldComponent>(uid))
|
||||
if (HasComp<GunRequiresWieldComponent>(uid))
|
||||
return;
|
||||
|
||||
if (component.WieldBonusExamineMessage != null)
|
||||
@@ -253,7 +254,7 @@ public sealed class WieldableSystem : EntitySystem
|
||||
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();
|
||||
@@ -298,7 +299,7 @@ public sealed class WieldableSystem : EntitySystem
|
||||
_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);
|
||||
}
|
||||
|
||||
|
||||
BIN
Resources/Audio/Machines/ame_fuelinjection.ogg
Normal file
BIN
Resources/Audio/Machines/ame_fuelinjection.ogg
Normal file
Binary file not shown.
@@ -181,3 +181,8 @@
|
||||
license: "CC0-1.0"
|
||||
copyright: "by ScarKy0"
|
||||
source: "https://github.com/space-wizards/space-station-14/pull/32012"
|
||||
|
||||
- files: ["ame_fuelinjection.ogg"]
|
||||
license: "CC0-1.0"
|
||||
copyright: "by AftrLite (Github). Uses audio from hypospray.ogg and hiss.ogg (Found in Resources/Audio/Items)"
|
||||
source: "https://github.com/space-wizards/space-station-14/pull/33097"
|
||||
|
||||
@@ -567,5 +567,28 @@ Entries:
|
||||
id: 70
|
||||
time: '2024-10-16T22:24:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32844
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: CC, ERT, Admin, and Deathsquad PDA's now have all departmental programs
|
||||
pre-installed.
|
||||
type: Tweak
|
||||
id: 71
|
||||
time: '2024-10-31T14:53:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32601
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- message: Added on-call functionality for discord relay to get notified on unanswered
|
||||
ahelps.
|
||||
type: Add
|
||||
id: 72
|
||||
time: '2024-11-02T09:29:16.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30443
|
||||
- author: nikthechampiongr
|
||||
changes:
|
||||
- message: It is now possible to edit the AI's laws through its core and eye.
|
||||
type: Fix
|
||||
id: 73
|
||||
time: '2024-11-02T13:21:10.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32461
|
||||
Name: Admin
|
||||
Order: 1
|
||||
|
||||
@@ -1,391 +1,4 @@
|
||||
Entries:
|
||||
- author: Errant
|
||||
changes:
|
||||
- message: Medical Mask sprite now works on vox.
|
||||
type: Fix
|
||||
id: 7052
|
||||
time: '2024-08-07T03:41:40.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30702
|
||||
- author: Lyroth001
|
||||
changes:
|
||||
- message: Dragons are immune to flashes
|
||||
type: Tweak
|
||||
id: 7053
|
||||
time: '2024-08-07T07:42:00.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30658
|
||||
- author: ShadowCommander
|
||||
changes:
|
||||
- message: Rollerbeds can now be dragged to the player to fold and pick them up.
|
||||
type: Add
|
||||
id: 7054
|
||||
time: '2024-08-07T09:19:10.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30002
|
||||
- author: Errant
|
||||
changes:
|
||||
- message: Survivors arriving via the Unknown Shuttle event, ERT and CBURN agents,
|
||||
and Death Squad members are now equipped with the appropriate species-specific
|
||||
survival gear.
|
||||
type: Fix
|
||||
- message: Unknown Shuttle event can once again spawn vox characters.
|
||||
type: Tweak
|
||||
id: 7055
|
||||
time: '2024-08-07T09:26:40.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29746
|
||||
- author: IProduceWidgets
|
||||
changes:
|
||||
- message: butter is slippery
|
||||
type: Tweak
|
||||
id: 7056
|
||||
time: '2024-08-07T21:47:03.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29772
|
||||
- author: Mervill
|
||||
changes:
|
||||
- message: Gas Miners now have detailed examine text
|
||||
type: Tweak
|
||||
id: 7057
|
||||
time: '2024-08-08T02:14:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30480
|
||||
- author: BackeTako
|
||||
changes:
|
||||
- message: "!, \u203D and multiple punctuations now work for Spanish."
|
||||
type: Tweak
|
||||
id: 7058
|
||||
time: '2024-08-08T03:08:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30551
|
||||
- author: strO0pwafel
|
||||
changes:
|
||||
- message: Fixed inconsistent naming of CentComm.
|
||||
type: Fix
|
||||
id: 7059
|
||||
time: '2024-08-08T10:04:20.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29217
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: Buffed the range of EMP implants from a radius of 1.75 tiles to 2.75
|
||||
tiles.
|
||||
type: Tweak
|
||||
- message: Buffed the range of EMP grenades from a radius of 4 tiles to 5.5 tiles.
|
||||
type: Tweak
|
||||
id: 7060
|
||||
time: '2024-08-08T10:04:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30660
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: You can drop food or drinks from your hands to interrupt eating it, again.
|
||||
type: Fix
|
||||
- message: Barber scissors are now interrupted if the item is dropped or if the
|
||||
user changes hands.
|
||||
type: Tweak
|
||||
id: 7061
|
||||
time: '2024-08-08T11:39:47.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30361
|
||||
- author: TheShuEd
|
||||
changes:
|
||||
- message: Add "thieving beacon" to Thief antag - a device that counts objects within
|
||||
a radius of itself as stolen.
|
||||
type: Add
|
||||
- message: Return thief structures stealing objectives.
|
||||
type: Add
|
||||
- message: Animal theft objectives can no longer appear if the animals are not on
|
||||
the station.
|
||||
type: Fix
|
||||
id: 7062
|
||||
time: '2024-08-08T13:17:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29997
|
||||
- author: lzk228
|
||||
changes:
|
||||
- message: RD labcoat added in RD's dresser.
|
||||
type: Add
|
||||
id: 7063
|
||||
time: '2024-08-08T22:50:57.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30671
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Animal DNA now shows up as "unknown DNA" in the Forensic Scanner.
|
||||
type: Tweak
|
||||
- message: Forensic Scanner can now scan fluid containers for DNA in reagents.
|
||||
type: Tweak
|
||||
- message: Fluids keep their DNA data when moved.
|
||||
type: Fix
|
||||
- message: Fluids now stain containers they're in with DNA. Make sure to scrub your
|
||||
blood bucket after use!
|
||||
type: Add
|
||||
- message: Vomit now includes DNA!
|
||||
type: Add
|
||||
id: 7064
|
||||
time: '2024-08-08T23:27:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/26699
|
||||
- author: themias
|
||||
changes:
|
||||
- message: Butter can be sliced, cookie and toast recipes now use butter slices.
|
||||
type: Tweak
|
||||
- message: Chefvend butter reduced from 4 to 3.
|
||||
type: Tweak
|
||||
id: 7065
|
||||
time: '2024-08-08T23:32:42.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30789
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: You should have significantly less friction when moving in space.
|
||||
type: Tweak
|
||||
id: 7066
|
||||
time: '2024-08-09T04:52:25.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29383
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: Hardsuits and EVA suits now count as protection for unscrewing lightbulbs.
|
||||
type: Add
|
||||
- message: More gloves were given the ability to unscrew light bulbs.
|
||||
type: Add
|
||||
- message: Behonkers no longer hurt you when melee attacking them or interacting
|
||||
with them.
|
||||
type: Remove
|
||||
id: 7067
|
||||
time: '2024-08-09T05:32:41.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30244
|
||||
- author: Ian321
|
||||
changes:
|
||||
- message: The warden is now an important job.
|
||||
type: Tweak
|
||||
id: 7068
|
||||
time: '2024-08-09T05:45:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30745
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Added tooltips to the agent ID job icons
|
||||
type: Add
|
||||
id: 7069
|
||||
time: '2024-08-09T06:14:07.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/28575
|
||||
- author: stalengd
|
||||
changes:
|
||||
- message: Head bandana no longer blocks food eating.
|
||||
type: Fix
|
||||
id: 7070
|
||||
time: '2024-08-09T06:17:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/28910
|
||||
- author: Ubaser
|
||||
changes:
|
||||
- message: Oxygen and nitrogen canisters now have new sprites when worn.
|
||||
type: Add
|
||||
id: 7071
|
||||
time: '2024-08-09T10:32:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30809
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: Buckling someone now triggers a short do-after.
|
||||
type: Tweak
|
||||
id: 7072
|
||||
time: '2024-08-09T15:43:02.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29621
|
||||
- author: Ubaser
|
||||
changes:
|
||||
- message: Normal crowbars cannot be placed in pockets, but can now fit in belts.
|
||||
type: Tweak
|
||||
- message: Depending on where you obtained the crowbars, they will now have different
|
||||
colours.
|
||||
type: Tweak
|
||||
id: 7073
|
||||
time: '2024-08-09T19:29:00.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/28988
|
||||
- author: lzk228
|
||||
changes:
|
||||
- message: Hotplate works again.
|
||||
type: Fix
|
||||
id: 7074
|
||||
time: '2024-08-10T01:10:39.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30830
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: Maintenance closets have more variety in what they can contain.
|
||||
type: Tweak
|
||||
id: 7075
|
||||
time: '2024-08-10T02:12:40.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30579
|
||||
- author: Ko4erga
|
||||
changes:
|
||||
- message: Changed chemistry airlock color.
|
||||
type: Tweak
|
||||
id: 7076
|
||||
time: '2024-08-10T03:23:47.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30666
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Bent pipes now deal 8 thrown damage instead of 3.
|
||||
type: Tweak
|
||||
id: 7077
|
||||
time: '2024-08-10T03:30:43.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30634
|
||||
- author: shampunj
|
||||
changes:
|
||||
- message: Rat king can now wideswing
|
||||
type: Tweak
|
||||
id: 7078
|
||||
time: '2024-08-10T12:47:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30808
|
||||
- author: BackeTako
|
||||
changes:
|
||||
- message: Added a suitskirt for the psychologist
|
||||
type: Add
|
||||
id: 7079
|
||||
time: '2024-08-10T12:51:54.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30709
|
||||
- author: Unkn0wnGh0st333
|
||||
changes:
|
||||
- message: ERT Chaplain starting gear was fixed and will no longer give the ERT
|
||||
Engineer gear
|
||||
type: Fix
|
||||
id: 7080
|
||||
time: '2024-08-10T12:55:20.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30855
|
||||
- author: Ubaser
|
||||
changes:
|
||||
- message: Light tube structures now have new sprites.
|
||||
type: Tweak
|
||||
id: 7081
|
||||
time: '2024-08-10T15:00:22.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29091
|
||||
- author: lzk228
|
||||
changes:
|
||||
- message: Standartized some clothing recipes.
|
||||
type: Tweak
|
||||
id: 7082
|
||||
time: '2024-08-10T18:16:56.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29315
|
||||
- author: TheShuEd
|
||||
changes:
|
||||
- message: You cat cut burger bun into two halfs, and make custom burgers now!
|
||||
type: Add
|
||||
id: 7083
|
||||
time: '2024-08-10T19:31:32.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30755
|
||||
- author: thetolbean
|
||||
changes:
|
||||
- message: Updated Core's boxing ring beacon label to be correct
|
||||
type: Tweak
|
||||
id: 7084
|
||||
time: '2024-08-10T19:50:08.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30800
|
||||
- author: Flareguy
|
||||
changes:
|
||||
- message: Added vox sprites for most of the remaining common mask items.
|
||||
type: Add
|
||||
id: 7085
|
||||
time: '2024-08-10T21:06:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30838
|
||||
- author: Scribbles0, Plykiya, nikthechampiongr
|
||||
changes:
|
||||
- message: You can now execute incapacitated/cuffed entities or yourself with certain
|
||||
sharp weapons. Executions require a do-after, deal 9x the damage of a normal
|
||||
attack, and ignore damage resistances.
|
||||
type: Add
|
||||
id: 7086
|
||||
time: '2024-08-11T03:05:54.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30104
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: Cooking shelves have been renamed to Kitchen shelves, you can now put
|
||||
more drinks and cutlery in them.
|
||||
type: Tweak
|
||||
id: 7087
|
||||
time: '2024-08-11T06:21:34.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30858
|
||||
- author: BombasterDS
|
||||
changes:
|
||||
- message: You can now change the suit's sensors on incapacitated people without
|
||||
taking it off
|
||||
type: Tweak
|
||||
id: 7088
|
||||
time: '2024-08-11T09:04:43.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29668
|
||||
- author: lzk228
|
||||
changes:
|
||||
- message: RCD UI can be activated only in hand.
|
||||
type: Tweak
|
||||
id: 7089
|
||||
time: '2024-08-11T09:15:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30861
|
||||
- author: TokenStyle
|
||||
changes:
|
||||
- message: Pax reagent now refresh pacified.
|
||||
type: Tweak
|
||||
- message: Pax reagent pacified duration has been changed from 2 seconds to 4 seconds.
|
||||
type: Tweak
|
||||
id: 7090
|
||||
time: '2024-08-11T09:19:01.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30778
|
||||
- author: osjarw
|
||||
changes:
|
||||
- message: You can once again pull and buckle pets.
|
||||
type: Fix
|
||||
id: 7091
|
||||
time: '2024-08-11T09:22:46.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30066
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: The sentient plant mutation can no longer be propagated to other trays
|
||||
via seeds or swabs. It has also been made more rare.
|
||||
type: Tweak
|
||||
id: 7092
|
||||
time: '2024-08-11T09:23:14.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29133
|
||||
- author: Moomoobeef
|
||||
changes:
|
||||
- message: Added The Throngler (Plushie) to the grand lottery!!
|
||||
type: Add
|
||||
id: 7093
|
||||
time: '2024-08-11T10:26:37.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29978
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: Energy sword weapons are now execution-capable.
|
||||
type: Add
|
||||
id: 7094
|
||||
time: '2024-08-11T22:12:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30909
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Added diamond-tipped mining drills, a science printed item found under
|
||||
T2 industrial.
|
||||
type: Add
|
||||
- message: Standard mining drills now attack faster and break rocks in two hits.
|
||||
type: Tweak
|
||||
- message: Moved ore bags of holding to T2 industrial tech.
|
||||
type: Tweak
|
||||
- message: Grappling guns are now researched with Salvage Equipment.
|
||||
type: Tweak
|
||||
- message: Diamonds are now worth 2,000 spesos each.
|
||||
type: Tweak
|
||||
id: 7095
|
||||
time: '2024-08-12T01:49:09.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30814
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- message: With space law being overhauled, you can now examine items to see their
|
||||
contraband status, and whether with your current access you'd be accosted by
|
||||
Security for owning it. This will also clearly show non-stealth syndicate items.
|
||||
type: Add
|
||||
id: 7096
|
||||
time: '2024-08-12T03:57:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/28688
|
||||
- author: DieselMohawk
|
||||
changes:
|
||||
- message: Resprited Security vest and helmet
|
||||
type: Tweak
|
||||
id: 7097
|
||||
time: '2024-08-12T06:20:56.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30291
|
||||
- author: Ubaser
|
||||
changes:
|
||||
- message: Mantles are now available in loadouts, requiring 20 hours of that head
|
||||
of department's time.
|
||||
type: Add
|
||||
- message: Mantles are no longer spawned in dressers or able to be printed at uniform
|
||||
lathes.
|
||||
type: Remove
|
||||
id: 7098
|
||||
time: '2024-08-12T07:14:46.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30929
|
||||
- author: LeoSantich
|
||||
changes:
|
||||
- message: Updated 'narsie' and 'ratvar' to 'Nar'Sie' and 'Ratvar' in randomly generated
|
||||
@@ -3959,3 +3572,380 @@
|
||||
id: 7551
|
||||
time: '2024-10-24T03:41:03.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32965
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Mix 1u aluminium, 1u potassium and 1u sulfur for a flash reaction effect.
|
||||
The radius scales with the reagent amount.
|
||||
type: Add
|
||||
id: 7552
|
||||
time: '2024-10-25T22:47:12.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32377
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: Fixed the Lone Nuclear Operative mid-round antagonist being extremely
|
||||
rare.
|
||||
type: Fix
|
||||
id: 7553
|
||||
time: '2024-10-26T02:16:45.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32942
|
||||
- author: Moomoobeef
|
||||
changes:
|
||||
- message: Bowls no longer make an eating sound when drinking from them.
|
||||
type: Fix
|
||||
id: 7554
|
||||
time: '2024-10-26T04:00:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32819
|
||||
- author: SaphireLattice
|
||||
changes:
|
||||
- message: Added a warning about unrevivability in the health analyzer UI.
|
||||
type: Add
|
||||
id: 7555
|
||||
time: '2024-10-26T17:22:09.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32636
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Fixed pie throwing sound not playing.
|
||||
type: Fix
|
||||
id: 7556
|
||||
time: '2024-10-27T04:25:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33017
|
||||
- author: stalengd
|
||||
changes:
|
||||
- message: Fixed playtime labels not being able to correctly display time greater
|
||||
than 24 hours
|
||||
type: Fix
|
||||
id: 7557
|
||||
time: '2024-10-28T18:00:00.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32974
|
||||
- author: august-sun
|
||||
changes:
|
||||
- message: Extended the minimum round time for meteor swarm events.
|
||||
type: Tweak
|
||||
id: 7558
|
||||
time: '2024-10-28T21:25:34.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32876
|
||||
- author: deltanedas
|
||||
changes:
|
||||
- message: Fixed lava planet expeditions not working.
|
||||
type: Fix
|
||||
id: 7559
|
||||
time: '2024-10-29T05:00:29.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33042
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- message: Fix separated game screen bumping slightly.
|
||||
type: Fix
|
||||
id: 7560
|
||||
time: '2024-10-29T05:07:57.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33046
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Proto-kitentic crushers, glaives, and daggers now have more accurate
|
||||
inhand sprites.
|
||||
type: Tweak
|
||||
id: 7561
|
||||
time: '2024-10-30T07:38:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32212
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Security belts now contain a holobarrier projector and a handheld security
|
||||
radio by default rather than tear gas and a flashbang.
|
||||
type: Tweak
|
||||
id: 7562
|
||||
time: '2024-10-30T07:40:33.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32291
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Added three bottle boxes to the nanomed plus inventory for doctors to
|
||||
carry small amounts of chemicals on their person
|
||||
type: Add
|
||||
id: 7563
|
||||
time: '2024-10-30T07:41:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33018
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Added the interdyne defibrillator, a black-and-red defibrillator that
|
||||
can be used as a melee weapon.
|
||||
type: Add
|
||||
- message: The syndicate medical bundle now contains an interdyne defibrillator,
|
||||
a collection of various instant injectors, tourniquets, and several combat kits.
|
||||
The price has been raised to 24 tc.
|
||||
type: Tweak
|
||||
id: 7564
|
||||
time: '2024-10-30T09:15:30.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32720
|
||||
- author: Boaz1111
|
||||
changes:
|
||||
- message: Pill bottles can now only store pills.
|
||||
type: Tweak
|
||||
id: 7565
|
||||
time: '2024-10-31T10:56:07.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33074
|
||||
- author: Jarmer123
|
||||
changes:
|
||||
- message: You can now find a spare bible in the PietyVend
|
||||
type: Add
|
||||
id: 7566
|
||||
time: '2024-10-31T13:26:46.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32363
|
||||
- author: justinbrick
|
||||
changes:
|
||||
- message: Added a pop-up notification when extra items are dropped while unequipping
|
||||
something.
|
||||
type: Tweak
|
||||
id: 7567
|
||||
time: '2024-10-31T14:12:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33078
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: The maximum amount of programs that can be installed on a PDA has been
|
||||
increased from 5 to 8
|
||||
type: Tweak
|
||||
- message: The Detective and Head of Security now get the logprobe program pre-installed
|
||||
on their PDA.
|
||||
type: Tweak
|
||||
id: 7568
|
||||
time: '2024-10-31T14:53:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32601
|
||||
- author: Psychpsyo
|
||||
changes:
|
||||
- message: Carp plushies can now be placed in mop buckets, along with other rehydratable
|
||||
things like monkey cubes.
|
||||
type: Add
|
||||
id: 7569
|
||||
time: '2024-10-31T18:46:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33079
|
||||
- author: Bhijn and Myr
|
||||
changes:
|
||||
- message: Tail thumping has been downmixed to mono to fix the sound lacking any
|
||||
sort of positioning. They're now capable of having a presence in the actual
|
||||
soundspace, in turn meaning lizards are no longer occupying your headset at
|
||||
all times of day.
|
||||
type: Fix
|
||||
id: 7570
|
||||
time: '2024-10-31T21:30:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33092
|
||||
- author: reesque
|
||||
changes:
|
||||
- message: pie not dropping tin on thrown
|
||||
type: Fix
|
||||
id: 7571
|
||||
time: '2024-11-01T01:43:11.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33013
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Votekicks can now be initiated during the pregame lobby.
|
||||
type: Fix
|
||||
id: 7572
|
||||
time: '2024-11-01T01:52:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32528
|
||||
- author: PopGamer46
|
||||
changes:
|
||||
- message: Fixed bolt lights of recently unpowered bolted doors
|
||||
type: Fix
|
||||
id: 7573
|
||||
time: '2024-11-01T02:04:09.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33063
|
||||
- author: RumiTiger
|
||||
changes:
|
||||
- message: A chocolate and banana muffin has been added to the game. The berry and
|
||||
cherry muffins have also been resprited!
|
||||
type: Add
|
||||
- message: Now you can make a regular, chocolate, banana, and berry muffin!
|
||||
type: Tweak
|
||||
- message: Muffin tins have been added to the game. They are needed for making muffins!
|
||||
type: Add
|
||||
id: 7574
|
||||
time: '2024-11-01T02:06:46.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29318
|
||||
- author: ScarKy0
|
||||
changes:
|
||||
- message: AI can no longer toggle seeing jobs off.
|
||||
type: Tweak
|
||||
- message: Borgs can no longer see mindshield status.
|
||||
type: Fix
|
||||
id: 7575
|
||||
time: '2024-11-01T02:32:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33069
|
||||
- author: Minemoder5000
|
||||
changes:
|
||||
- message: The cargo shuttle's cargo pallets can no longer sell or buy.
|
||||
type: Fix
|
||||
id: 7576
|
||||
time: '2024-11-01T06:22:39.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33022
|
||||
- author: aspiringLich
|
||||
changes:
|
||||
- message: Fixed the logic triggering popups when inserting items into machines.
|
||||
type: Fix
|
||||
id: 7577
|
||||
time: '2024-11-02T01:33:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/28856
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Pills are explosion resistant.
|
||||
type: Tweak
|
||||
id: 7578
|
||||
time: '2024-11-02T09:51:45.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32458
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Handcrafted gauze now takes 3 seconds instead of 10 seconds of crafting
|
||||
type: Tweak
|
||||
- message: Medical techfab gauze recipe now takes 1 cloth instead of 2
|
||||
type: Tweak
|
||||
id: 7579
|
||||
time: '2024-11-02T09:53:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32744
|
||||
- author: Ubaser
|
||||
changes:
|
||||
- message: Service workers can now be antagonists.
|
||||
type: Fix
|
||||
id: 7580
|
||||
time: '2024-11-02T10:07:52.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31359
|
||||
- author: AftrLite
|
||||
changes:
|
||||
- message: The AME now has a new sound effect for fuel injection!
|
||||
type: Add
|
||||
id: 7581
|
||||
time: '2024-11-02T13:19:33.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33097
|
||||
- author: deltanedas
|
||||
changes:
|
||||
- message: Printing cables in an autolathe is now 20 times faster.
|
||||
type: Tweak
|
||||
id: 7582
|
||||
time: '2024-11-02T13:24:08.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31521
|
||||
- author: Centronias
|
||||
changes:
|
||||
- message: Fixed a bug where attempting to speak into a handheld radio and an intercom
|
||||
simultaneously would lead to only one device transmitting the message.
|
||||
type: Fix
|
||||
id: 7583
|
||||
time: '2024-11-02T15:04:22.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32737
|
||||
- author: joshepvodka
|
||||
changes:
|
||||
- message: Headphones are now selectable in loadouts.
|
||||
type: Add
|
||||
id: 7584
|
||||
time: '2024-11-02T15:12:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33067
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: Wielding weapons no longer displays your character's real identity when
|
||||
your identity is hidden.
|
||||
type: Fix
|
||||
id: 7585
|
||||
time: '2024-11-03T11:25:30.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33134
|
||||
- author: MilenVolf
|
||||
changes:
|
||||
- message: Ghost roles menu is now collapsible! This makes it much easier to find
|
||||
a specific role that you want.
|
||||
type: Tweak
|
||||
id: 7586
|
||||
time: '2024-11-04T00:49:42.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32717
|
||||
- author: Tr1bute
|
||||
changes:
|
||||
- message: A popup now appears when attempting to harvest mutated plants that require
|
||||
sharp tools.
|
||||
type: Add
|
||||
id: 7587
|
||||
time: '2024-11-04T00:56:41.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33136
|
||||
- author: Boolean-Buckeye
|
||||
changes:
|
||||
- message: Emergency lights can be constructed and deconstructed
|
||||
type: Add
|
||||
id: 7588
|
||||
time: '2024-11-04T03:24:22.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32945
|
||||
- author: thetolbean
|
||||
changes:
|
||||
- message: The web coat and boots can now be broken down into silk.
|
||||
type: Fix
|
||||
id: 7589
|
||||
time: '2024-11-04T03:33:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33121
|
||||
- author: Kickguy223
|
||||
changes:
|
||||
- message: Ion Storms now notify a Borg that their laws have changed via a sound
|
||||
cue.
|
||||
type: Add
|
||||
id: 7590
|
||||
time: '2024-11-04T11:23:12.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32887
|
||||
- author: DrSmugleaf
|
||||
changes:
|
||||
- message: Fixed not being able to see vote counts for non-votekick votes.
|
||||
type: Fix
|
||||
id: 7591
|
||||
time: '2024-11-05T00:12:29.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33170
|
||||
- author: kirus59
|
||||
changes:
|
||||
- message: Fixed display of icons when using hunger or thirst huds
|
||||
type: Fix
|
||||
- message: Fixed errors spam when using beer goggles
|
||||
type: Fix
|
||||
id: 7592
|
||||
time: '2024-11-05T16:58:23.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32832
|
||||
- author: Errant
|
||||
changes:
|
||||
- message: Syndicate Reinforcements no longer list their (hidden and unused) codewords
|
||||
on the Round End window.
|
||||
type: Fix
|
||||
id: 7593
|
||||
time: '2024-11-05T18:18:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33181
|
||||
- author: Baptr0b0t
|
||||
changes:
|
||||
- message: Fixed Intelicard can see through walls.
|
||||
type: Fix
|
||||
id: 7594
|
||||
time: '2024-11-06T03:28:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33177
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Removed the wanted list cartridge from the captain's PDA.
|
||||
type: Remove
|
||||
id: 7595
|
||||
time: '2024-11-06T13:20:05.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33084
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: Flare guns have been reworked to no longer automatically eject shells
|
||||
after firing, and must now be opened manually to insert or eject shells. A safety
|
||||
feature has also been added to prevent loading lethal shells into them.
|
||||
type: Tweak
|
||||
- message: Flare guns are twice as likely to appear in emergency closets, with the
|
||||
same rarity as MRE's.
|
||||
type: Tweak
|
||||
- message: Added the security shell gun, a modified flare gun which is capable of
|
||||
handling both nonlethal and lethal shotgun shells. It is able to be crafted
|
||||
in the security techfab without needing any research.
|
||||
type: Add
|
||||
id: 7596
|
||||
time: '2024-11-06T14:27:11.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32829
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: WT550 and C20-r's "burst" firemodes are now actual bursts.
|
||||
type: Tweak
|
||||
- message: Drozd is now a burst-only weapon (DPS remains unchanged!).
|
||||
type: Tweak
|
||||
id: 7597
|
||||
time: '2024-11-06T14:39:16.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31292
|
||||
- author: PopGamer46
|
||||
changes:
|
||||
- message: Fixed Lambordeere (botany shuttle) bolt buttons not being connected
|
||||
type: Fix
|
||||
id: 7598
|
||||
time: '2024-11-07T22:02:25.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33065
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,3 +31,4 @@ plant-holder-component-heat-improper-warning = The [color=orange]improper temper
|
||||
plant-holder-component-pressure-improper-warning = The [color=lightblue]improper environment pressure alert[/color] is blinking.
|
||||
plant-holder-component-gas-missing-warning = The [color=cyan]improper gas environment alert[/color] is blinking.
|
||||
plant-holder-component-early-sample-message = The plant hasn't grown enough to take a sample yet.
|
||||
plant-holder-component-ligneous-cant-harvest-message = The plant is too tough to harvest with your bare hands.
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
chameleon-projector-inside-container = There's no room to scan that!
|
||||
chameleon-projector-invalid = You can't disguise as that!
|
||||
chameleon-projector-success = Projected new disguise.
|
||||
chameleon-projector-set-disguise = Set Disguise
|
||||
|
||||
@@ -14,6 +14,7 @@ ghost-target-window-current-button = Warp: {$name}
|
||||
ghost-target-window-warp-to-most-followed = Warp to Most Followed
|
||||
|
||||
ghost-roles-window-title = Ghost Roles
|
||||
ghost-roles-window-available-button = Available ({$rolesCount})
|
||||
ghost-roles-window-join-raffle-button = Join raffle
|
||||
ghost-roles-window-raffle-in-progress-button =
|
||||
Join raffle ({$time} left, { $players ->
|
||||
|
||||
@@ -37,6 +37,12 @@ reagent-effect-guidebook-emp-reaction-effect =
|
||||
*[other] cause
|
||||
} an electromagnetic pulse
|
||||
|
||||
reagent-effect-guidebook-flash-reaction-effect =
|
||||
{ $chance ->
|
||||
[1] Causes
|
||||
*[other] cause
|
||||
} a blinding flash
|
||||
|
||||
reagent-effect-guidebook-foam-area-reaction-effect =
|
||||
{ $chance ->
|
||||
[1] Creates
|
||||
|
||||
@@ -2,3 +2,9 @@ inventory-component-can-equip-cannot = You can't equip this!
|
||||
inventory-component-can-equip-does-not-fit = This doesn't fit!
|
||||
|
||||
inventory-component-can-unequip-cannot = You can't unequip this!
|
||||
|
||||
inventory-component-dropped-from-unequip =
|
||||
You dropped {$items ->
|
||||
[1] an item!
|
||||
*[other] some items!
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mop bucket
|
||||
mop-bucket-slot-component-slot-name-shark = Shark
|
||||
mop-bucket-slot-component-slot-name-item = Item
|
||||
mop-bucket-slot-component-eject-verb = Take out
|
||||
# janitorial trolley
|
||||
janitorial-trolley-slot-component-slot-name-plunger = Plunger
|
||||
janitorial-trolley-slot-component-slot-name-sign = Sign
|
||||
|
||||
@@ -15,7 +15,8 @@ health-analyzer-window-entity-damage-total-text = Total Damage:
|
||||
health-analyzer-window-damage-group-text = {$damageGroup}: {$amount}
|
||||
health-analyzer-window-damage-type-text = {$damageType}: {$amount}
|
||||
|
||||
health-analyzer-window-entity-bleeding-text = Patient is bleeding!
|
||||
health-analyzer-window-entity-unrevivable-text = [color=red]Unique body composition detected! Patient can not be resuscitated by normal means![/color]
|
||||
health-analyzer-window-entity-bleeding-text = [color=red]Patient is bleeding![/color]
|
||||
|
||||
health-analyzer-window-scan-mode-text = Scan Mode:
|
||||
health-analyzer-window-scan-mode-active = Active
|
||||
|
||||
@@ -73,3 +73,4 @@ station-beacon-tools = Tools
|
||||
station-beacon-disposals = Disposals
|
||||
station-beacon-cryosleep = Cryosleep
|
||||
station-beacon-escape-pod = Escape Pod
|
||||
station-beacon-vox = Vox Break Room
|
||||
|
||||
@@ -216,8 +216,8 @@ uplink-chemistry-kit-desc = A starter kit for the aspiring chemist, includes tox
|
||||
uplink-knives-kit-name = Throwing Knives Kit
|
||||
uplink-knives-kit-desc = A set of 4 syndicate branded throwing knives, perfect for embedding into the body of your victims.
|
||||
|
||||
uplink-meds-bundle-name = Medical Bundle
|
||||
uplink-meds-bundle-desc = All you need to get your comrades back in the fight: mainly a combat medkit, a defibrillator and three combat medipens.
|
||||
uplink-meds-bundle-name = Interdyne Medical Bundle
|
||||
uplink-meds-bundle-desc = An assortment of autoinjectors and premium medical equipment to cover for every possible situation. Contains an elite compact defibrillator that can be used as a weapon.
|
||||
|
||||
uplink-ammo-bundle-name = Ammo Bundle
|
||||
uplink-ammo-bundle-desc = Reloading! Contains 4 magazines for the C-20r, 4 drums for the Bulldog, and 2 ammo boxes for the L6 SAW.
|
||||
|
||||
@@ -25,8 +25,8 @@ ui-vote-type-not-available = This vote type has been disabled
|
||||
|
||||
# Vote option only available for specific users.
|
||||
ui-vote-trusted-users-notice =
|
||||
This vote option is only available to whitelisted players.
|
||||
In addition, you must have been a ghost for { $timeReq } minutes.
|
||||
This vote option is only available to players who have enough playtime or are whitelisted.
|
||||
In addition, you must have been a ghost for { $timeReq } seconds.
|
||||
|
||||
# Warning to not abuse a specific vote option.
|
||||
ui-vote-abuse-warning =
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
# Shown when examining the window. Each entry represents the window's health condition
|
||||
comp-window-damaged-1 = It looks fully intact.
|
||||
comp-window-damaged-2 = It has a few scratches
|
||||
comp-window-damaged-2 = It has a few scratches.
|
||||
comp-window-damaged-3 = It has a few small cracks.
|
||||
comp-window-damaged-4 = It has several big cracks running along its surface.
|
||||
comp-window-damaged-5 = It has deep cracks across multiple layers.
|
||||
comp-window-damaged-6 = It's extremely cracked and on the verge of shattering.
|
||||
comp-window-damaged-4 = [color=yellow]It has several big cracks running along its surface.[/color]
|
||||
comp-window-damaged-5 = [color=orange]It has deep cracks across multiple layers.[/color]
|
||||
comp-window-damaged-6 = [color=red]It's extremely cracked and on the verge of shattering.[/color]
|
||||
|
||||
### Interaction Messages
|
||||
|
||||
# Shown when knocking on a window
|
||||
comp-window-knock = *knock knock*
|
||||
|
||||
|
||||
@@ -6576,7 +6576,7 @@ entities:
|
||||
- uid: 2031
|
||||
components:
|
||||
- type: Transform
|
||||
pos: 3.6114278,-10.732791
|
||||
pos: 4.0109396,-12.223828
|
||||
parent: 104
|
||||
- uid: 2095
|
||||
components:
|
||||
@@ -10993,6 +10993,13 @@ entities:
|
||||
- type: Transform
|
||||
pos: 11.5,-6.5
|
||||
parent: 104
|
||||
- proto: HandLabeler
|
||||
entities:
|
||||
- uid: 1826
|
||||
components:
|
||||
- type: Transform
|
||||
pos: 3.4542694,-10.616036
|
||||
parent: 104
|
||||
- proto: KitchenKnife
|
||||
entities:
|
||||
- uid: 1061
|
||||
|
||||
@@ -1206,6 +1206,10 @@ entities:
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 0.5,5.5
|
||||
parent: 1
|
||||
- type: DeviceLinkSource
|
||||
linkedPorts:
|
||||
50:
|
||||
- Pressed: DoorBolt
|
||||
- uid: 199
|
||||
components:
|
||||
- type: MetaData
|
||||
@@ -1214,6 +1218,10 @@ entities:
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 0.5,2.5
|
||||
parent: 1
|
||||
- type: DeviceLinkSource
|
||||
linkedPorts:
|
||||
82:
|
||||
- Pressed: DoorBolt
|
||||
- proto: SMESBasic
|
||||
entities:
|
||||
- uid: 65
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62944,11 +62944,6 @@ entities:
|
||||
- type: Transform
|
||||
pos: 35.5,16.5
|
||||
parent: 60
|
||||
- uid: 24676
|
||||
components:
|
||||
- type: Transform
|
||||
pos: 11.5,-11.5
|
||||
parent: 60
|
||||
- proto: DefaultStationBeaconAICore
|
||||
entities:
|
||||
- uid: 18703
|
||||
@@ -63342,6 +63337,13 @@ entities:
|
||||
- type: Transform
|
||||
pos: 3.5,-6.5
|
||||
parent: 60
|
||||
- proto: DefaultStationBeaconVox
|
||||
entities:
|
||||
- uid: 23890
|
||||
components:
|
||||
- type: Transform
|
||||
pos: 11.5,-11.5
|
||||
parent: 60
|
||||
- proto: DefaultStationBeaconWardensOffice
|
||||
entities:
|
||||
- uid: 24426
|
||||
|
||||
@@ -11584,7 +11584,7 @@ entities:
|
||||
pos: 24.5,16.5
|
||||
parent: 8364
|
||||
- type: Door
|
||||
secondsUntilStateChange: -18243.016
|
||||
secondsUntilStateChange: -18318.04
|
||||
state: Opening
|
||||
- type: DeviceLinkSource
|
||||
lastSignals:
|
||||
@@ -72442,6 +72442,13 @@ entities:
|
||||
- type: Transform
|
||||
pos: -31.5,4.5
|
||||
parent: 8364
|
||||
- proto: DefaultStationBeaconVox
|
||||
entities:
|
||||
- uid: 27921
|
||||
components:
|
||||
- type: Transform
|
||||
pos: -42.5,-11.5
|
||||
parent: 8364
|
||||
- proto: DefaultStationBeaconWardensOffice
|
||||
entities:
|
||||
- uid: 27736
|
||||
@@ -84431,7 +84438,7 @@ entities:
|
||||
pos: -34.5,-14.5
|
||||
parent: 8364
|
||||
- type: Door
|
||||
secondsUntilStateChange: -12431.553
|
||||
secondsUntilStateChange: -12506.576
|
||||
state: Closing
|
||||
- uid: 15010
|
||||
components:
|
||||
@@ -84924,7 +84931,7 @@ entities:
|
||||
pos: -4.5,-71.5
|
||||
parent: 8364
|
||||
- type: Door
|
||||
secondsUntilStateChange: -4179.284
|
||||
secondsUntilStateChange: -4254.308
|
||||
state: Closing
|
||||
- proto: Fireplace
|
||||
entities:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12942,6 +12942,10 @@ entities:
|
||||
rot: -1.5707963267948966 rad
|
||||
pos: 30.5,-42.5
|
||||
parent: 2
|
||||
- type: DeviceList
|
||||
devices:
|
||||
- 20006
|
||||
- 20005
|
||||
- uid: 8436
|
||||
components:
|
||||
- type: Transform
|
||||
@@ -63749,6 +63753,13 @@ entities:
|
||||
- type: Transform
|
||||
pos: 54.5,-33.5
|
||||
parent: 2
|
||||
- proto: DefaultStationBeaconVox
|
||||
entities:
|
||||
- uid: 16173
|
||||
components:
|
||||
- type: Transform
|
||||
pos: -31.5,-2.5
|
||||
parent: 2
|
||||
- proto: DefaultStationBeaconWardensOffice
|
||||
entities:
|
||||
- uid: 20871
|
||||
@@ -95454,6 +95465,9 @@ entities:
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 26.5,-42.5
|
||||
parent: 2
|
||||
- type: DeviceNetwork
|
||||
deviceLists:
|
||||
- 8434
|
||||
- type: AtmosPipeColor
|
||||
color: '#0335FCFF'
|
||||
- uid: 20037
|
||||
@@ -96655,6 +96669,9 @@ entities:
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 26.5,-45.5
|
||||
parent: 2
|
||||
- type: DeviceNetwork
|
||||
deviceLists:
|
||||
- 8434
|
||||
- type: AtmosPipeColor
|
||||
color: '#FF1212FF'
|
||||
- uid: 20038
|
||||
@@ -106044,11 +106061,11 @@ entities:
|
||||
After several simulations where Superconducting Magnetic Energy Storage units were drained from the main system from [italic]a variety[/italic] of user errors and other shenanigans, it has been determined that the Singularity should be put on its own power loop, disconnected from the main station. The upsides of this include but are not limited to:
|
||||
|
||||
|
||||
[bold]1.[/bold] Limited external forces from the containments power.
|
||||
[bold]1.[/bold] Limited external forces from the containments power.
|
||||
|
||||
[bold]2.[/bold] An "early warning" system, if you see JUST the PA room run out of power, you know there is an issue.
|
||||
[bold]2.[/bold] An "early warning" system, if you see JUST the PA room run out of power, you know there is an issue.
|
||||
|
||||
[bold]3.[/bold] Due to being on its own small loop, its much easier to spot faults in the system.
|
||||
[bold]3.[/bold] Due to being on its own small loop, its much easier to spot faults in the system.
|
||||
|
||||
|
||||
[italic]While we have listed the upsides we also acknowledge the downside,[/italic] for it being on its own loop you will need an external force if the system "runs out of juice". Our recommendation for this is simply attaching a generator to said SMES and letting it get to full charge before continuing operations but as said from another of our technicians... "just attach it to the main grid for like, a hot moment, and kickstart the thing!"
|
||||
|
||||
@@ -10880,7 +10880,7 @@ entities:
|
||||
pos: -42.5,29.5
|
||||
parent: 5350
|
||||
- type: Door
|
||||
secondsUntilStateChange: -5745.716
|
||||
secondsUntilStateChange: -5834.8745
|
||||
state: Opening
|
||||
- type: DeviceLinkSink
|
||||
invokeCounter: 2
|
||||
@@ -56106,11 +56106,6 @@ entities:
|
||||
rot: -1.5707963267948966 rad
|
||||
pos: -73.5,7.5
|
||||
parent: 5350
|
||||
- uid: 6164
|
||||
components:
|
||||
- type: Transform
|
||||
pos: -71.5,-8.5
|
||||
parent: 5350
|
||||
- uid: 6165
|
||||
components:
|
||||
- type: Transform
|
||||
|
||||
@@ -14470,7 +14470,7 @@ entities:
|
||||
lastSignals:
|
||||
DoorStatus: True
|
||||
- type: Door
|
||||
secondsUntilStateChange: -76151.9
|
||||
secondsUntilStateChange: -76213.97
|
||||
state: Opening
|
||||
- uid: 6934
|
||||
components:
|
||||
@@ -14482,7 +14482,7 @@ entities:
|
||||
lastSignals:
|
||||
DoorStatus: True
|
||||
- type: Door
|
||||
secondsUntilStateChange: -76154.53
|
||||
secondsUntilStateChange: -76216.6
|
||||
state: Opening
|
||||
- uid: 6935
|
||||
components:
|
||||
@@ -14494,7 +14494,7 @@ entities:
|
||||
lastSignals:
|
||||
DoorStatus: True
|
||||
- type: Door
|
||||
secondsUntilStateChange: -76153.38
|
||||
secondsUntilStateChange: -76215.45
|
||||
state: Opening
|
||||
- uid: 6936
|
||||
components:
|
||||
@@ -14505,7 +14505,7 @@ entities:
|
||||
lastSignals:
|
||||
DoorStatus: True
|
||||
- type: Door
|
||||
secondsUntilStateChange: -76152.6
|
||||
secondsUntilStateChange: -76214.67
|
||||
state: Opening
|
||||
- proto: AirlockTheatreLocked
|
||||
entities:
|
||||
@@ -81255,6 +81255,13 @@ entities:
|
||||
- type: Transform
|
||||
pos: -54.5,-11.5
|
||||
parent: 2
|
||||
- proto: DefaultStationBeaconVox
|
||||
entities:
|
||||
- uid: 29615
|
||||
components:
|
||||
- type: Transform
|
||||
pos: -14.5,17.5
|
||||
parent: 2
|
||||
- proto: DefaultStationBeaconWardensOffice
|
||||
entities:
|
||||
- uid: 11912
|
||||
@@ -94258,7 +94265,7 @@ entities:
|
||||
pos: -13.5,-1.5
|
||||
parent: 2
|
||||
- type: Door
|
||||
secondsUntilStateChange: -67475.88
|
||||
secondsUntilStateChange: -67537.95
|
||||
- type: DeviceNetwork
|
||||
deviceLists:
|
||||
- 18275
|
||||
@@ -138807,7 +138814,7 @@ entities:
|
||||
pos: 36.5,-35.5
|
||||
parent: 2
|
||||
- type: Door
|
||||
secondsUntilStateChange: -104307.73
|
||||
secondsUntilStateChange: -104369.8
|
||||
state: Opening
|
||||
- uid: 5211
|
||||
components:
|
||||
@@ -190162,7 +190169,7 @@ entities:
|
||||
pos: 24.5,2.5
|
||||
parent: 21002
|
||||
- type: Door
|
||||
secondsUntilStateChange: -449512.38
|
||||
secondsUntilStateChange: -449574.44
|
||||
state: Opening
|
||||
- uid: 28863
|
||||
components:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user