Merge remote-tracking branch 'upstream/staging' into stable-test

This commit is contained in:
Vasilis The Pikachu
2024-11-10 14:10:53 +01:00
277 changed files with 6723 additions and 3446 deletions

View File

@@ -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
};

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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)

View File

@@ -19,11 +19,6 @@ public sealed class VotingSystem : EntitySystem
private void OnVotePlayerListResponseEvent(VotePlayerListResponseEvent msg)
{
if (!_ghostSystem.IsGhost)
{
return;
}
VotePlayerListResponse?.Invoke(msg);
}

View File

@@ -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) });

View File

@@ -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();

View File

@@ -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");

View File

@@ -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"),
});

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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))
{

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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?

View File

@@ -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);

View File

@@ -201,6 +201,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
? bloodSolution.FillFraction
: 0,
null,
null,
null
));
}

View File

@@ -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
));
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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);
}
}
}

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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)

View File

@@ -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
*/

View File

@@ -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

View File

@@ -17,4 +17,7 @@ public sealed class CartridgeUiMessage : BoundUserInterfaceMessage
public abstract class CartridgeMessageEvent : EntityEventArgs
{
public NetEntity LoaderUid;
[NonSerialized]
public EntityUid Actor;
}

View File

@@ -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!;
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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))

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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)

View File

@@ -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";

View File

@@ -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"
};
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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");

View File

@@ -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);

View File

@@ -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");

View File

@@ -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]

View File

@@ -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()

View File

@@ -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);

View File

@@ -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);
}

Binary file not shown.

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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

View File

@@ -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!
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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 =

View File

@@ -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*

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!"

View File

@@ -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

View File

@@ -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