Merge branch 'master' of https://github.com/space-wizards/space-station-14 into map-load-refactor

This commit is contained in:
ElectroJr
2025-01-12 20:19:04 +13:00
378 changed files with 272396 additions and 119785 deletions

12
.github/CODEOWNERS vendored
View File

@@ -3,17 +3,17 @@
# Sorting by path instead of by who added it one day :( # Sorting by path instead of by who added it one day :(
# this isn't how codeowners rules work pls read the first comment instead of trying to force a sorting order # this isn't how codeowners rules work pls read the first comment instead of trying to force a sorting order
/Resources/ConfigPresets/WizardsDen/ @nikthechampiongr /Resources/ConfigPresets/WizardsDen/ @nikthechampiongr @crazybrain23
/Content.*/Administration/ @DrSmugleaf @nikthechampiongr /Content.*/Administration/ @DrSmugleaf @nikthechampiongr @crazybrain23
/Resources/ServerInfo/ @nikthechampiongr /Resources/ServerInfo/ @nikthechampiongr @crazybrain23
/Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr /Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr @crazybrain23
/Resources/Prototypes/Maps/** @Emisse /Resources/Prototypes/Maps/** @Emisse
/Resources/Prototypes/Body/ @DrSmugleaf # suffering /Resources/Prototypes/Body/ @DrSmugleaf # suffering
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf /Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf /Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
/Resources/Prototypes/Guidebook/rules.yml @nikthechampiongr /Resources/Prototypes/Guidebook/rules.yml @nikthechampiongr @crazybrain23
/Content.*/Body/ @DrSmugleaf /Content.*/Body/ @DrSmugleaf
/Content.YAMLLinter @DrSmugleaf /Content.YAMLLinter @DrSmugleaf
/Content.Shared/Damage/ @DrSmugleaf /Content.Shared/Damage/ @DrSmugleaf
@@ -25,7 +25,7 @@
# SKREEEE # SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf /Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr /Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr @crazybrain23
/Pow3r/ @PJB3005 /Pow3r/ @PJB3005
/Content.Server/Power/Pow3r/ @PJB3005 /Content.Server/Power/Pow3r/ @PJB3005

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0 uses: actions/setup-dotnet@v3.2.0
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0 uses: actions/setup-dotnet@v3.2.0
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0 uses: actions/setup-dotnet@v3.2.0
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore

View File

@@ -22,7 +22,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0 uses: actions/setup-dotnet@v3.2.0
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Get Engine Tag - name: Get Engine Tag
run: | run: |

View File

@@ -51,7 +51,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0 uses: actions/setup-dotnet@v3.2.0
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0 uses: actions/setup-dotnet@v3.2.0
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build

View File

@@ -1,16 +1,21 @@
using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Client.Administration.Systems; using Content.Client.Administration.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration; namespace Content.Client.Administration;
internal sealed class AdminNameOverlay : Overlay internal sealed class AdminNameOverlay : Overlay
{ {
[Dependency] private readonly IConfigurationManager _config = default!;
private readonly AdminSystem _system; private readonly AdminSystem _system;
private readonly IEntityManager _entityManager; private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager; private readonly IEyeManager _eyeManager;
@@ -18,8 +23,16 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IUserInterfaceManager _userInterfaceManager; private readonly IUserInterfaceManager _userInterfaceManager;
private readonly Font _font; private readonly Font _font;
//TODO make this adjustable via GUI
private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
private readonly Color _antagColorClassic = Color.OrangeRed;
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager) public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager)
{ {
IoCManager.InjectDependencies(this);
_system = system; _system = system;
_entityManager = entityManager; _entityManager = entityManager;
_eyeManager = eyeManager; _eyeManager = eyeManager;
@@ -35,6 +48,9 @@ internal sealed class AdminNameOverlay : Overlay
{ {
var viewport = args.WorldAABB; var viewport = args.WorldAABB;
//TODO make this adjustable via GUI
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
foreach (var playerInfo in _system.PlayerList) foreach (var playerInfo in _system.PlayerList)
{ {
var entity = _entityManager.GetEntity(playerInfo.NetEntity); var entity = _entityManager.GetEntity(playerInfo.NetEntity);
@@ -64,12 +80,20 @@ internal sealed class AdminNameOverlay : Overlay
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center + var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec( new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f); aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
if (playerInfo.Antag)
if (classic && playerInfo.Antag)
{ {
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", uiScale, Color.OrangeRed); args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), _antagLabelClassic, uiScale, _antagColorClassic);
;
} }
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White); else if (!classic && _filter.Contains(playerInfo.RoleProto.ID))
{
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var color = playerInfo.RoleProto.Color;
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), label, uiScale, color);
}
args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White); args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
} }
} }

View File

@@ -197,6 +197,7 @@ public sealed partial class PlayerTab : Control
Header.Character => Compare(x.CharacterName, y.CharacterName), Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob), Header.Job => Compare(x.StartingJob, y.StartingJob),
Header.Antagonist => x.Antag.CompareTo(y.Antag), Header.Antagonist => x.Antag.CompareTo(y.Antag),
Header.RoleType => Compare(x.RoleProto.Name , y.RoleProto.Name),
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default), Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1 _ => 1
}; };

View File

@@ -24,6 +24,11 @@
HorizontalExpand="True" HorizontalExpand="True"
ClipText="True"/> ClipText="True"/>
<customControls:VSeparator/> <customControls:VSeparator/>
<Label Name="RoleTypeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="OverallPlaytimeLabel" <Label Name="OverallPlaytimeLabel"
SizeFlagsStretchRatio="1" SizeFlagsStretchRatio="1"
HorizontalExpand="True" HorizontalExpand="True"

View File

@@ -23,6 +23,8 @@ public sealed partial class PlayerTabEntry : PanelContainer
if (player.IdentityName != player.CharacterName) if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]"; CharacterLabel.Text += $" [{player.IdentityName}]";
AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no"); AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
BackgroundColorPanel.PanelOverride = styleBoxFlat; BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString; OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity; PlayerEntity = player.NetEntity;

View File

@@ -32,6 +32,13 @@
Text="{Loc player-tab-antagonist}" Text="{Loc player-tab-antagonist}"
MouseFilter="Pass"/> MouseFilter="Pass"/>
<cc:VSeparator/> <cc:VSeparator/>
<Label Name="RoleTypeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-roletype}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="PlaytimeLabel" <Label Name="PlaytimeLabel"
SizeFlagsStretchRatio="1" SizeFlagsStretchRatio="1"
HorizontalExpand="True" HorizontalExpand="True"

View File

@@ -19,6 +19,7 @@ public sealed partial class PlayerTabHeader : Control
CharacterLabel.OnKeyBindDown += CharacterClicked; CharacterLabel.OnKeyBindDown += CharacterClicked;
JobLabel.OnKeyBindDown += JobClicked; JobLabel.OnKeyBindDown += JobClicked;
AntagonistLabel.OnKeyBindDown += AntagonistClicked; AntagonistLabel.OnKeyBindDown += AntagonistClicked;
RoleTypeLabel.OnKeyBindDown += RoleTypeClicked;
PlaytimeLabel.OnKeyBindDown += PlaytimeClicked; PlaytimeLabel.OnKeyBindDown += PlaytimeClicked;
} }
@@ -30,6 +31,7 @@ public sealed partial class PlayerTabHeader : Control
Header.Character => CharacterLabel, Header.Character => CharacterLabel,
Header.Job => JobLabel, Header.Job => JobLabel,
Header.Antagonist => AntagonistLabel, Header.Antagonist => AntagonistLabel,
Header.RoleType => RoleTypeLabel,
Header.Playtime => PlaytimeLabel, Header.Playtime => PlaytimeLabel,
_ => throw new ArgumentOutOfRangeException(nameof(header), header, null) _ => throw new ArgumentOutOfRangeException(nameof(header), header, null)
}; };
@@ -41,6 +43,7 @@ public sealed partial class PlayerTabHeader : Control
CharacterLabel.Text = Loc.GetString("player-tab-character"); CharacterLabel.Text = Loc.GetString("player-tab-character");
JobLabel.Text = Loc.GetString("player-tab-job"); JobLabel.Text = Loc.GetString("player-tab-job");
AntagonistLabel.Text = Loc.GetString("player-tab-antagonist"); AntagonistLabel.Text = Loc.GetString("player-tab-antagonist");
RoleTypeLabel.Text = Loc.GetString("player-tab-roletype");
PlaytimeLabel.Text = Loc.GetString("player-tab-playtime"); PlaytimeLabel.Text = Loc.GetString("player-tab-playtime");
} }
@@ -75,6 +78,11 @@ public sealed partial class PlayerTabHeader : Control
HeaderClicked(args, Header.Antagonist); HeaderClicked(args, Header.Antagonist);
} }
private void RoleTypeClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.RoleType);
}
private void PlaytimeClicked(GUIBoundKeyEventArgs args) private void PlaytimeClicked(GUIBoundKeyEventArgs args)
{ {
HeaderClicked(args, Header.Playtime); HeaderClicked(args, Header.Playtime);
@@ -90,6 +98,7 @@ public sealed partial class PlayerTabHeader : Control
CharacterLabel.OnKeyBindDown -= CharacterClicked; CharacterLabel.OnKeyBindDown -= CharacterClicked;
JobLabel.OnKeyBindDown -= JobClicked; JobLabel.OnKeyBindDown -= JobClicked;
AntagonistLabel.OnKeyBindDown -= AntagonistClicked; AntagonistLabel.OnKeyBindDown -= AntagonistClicked;
RoleTypeLabel.OnKeyBindDown -= RoleTypeClicked;
PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked; PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked;
} }
} }
@@ -100,6 +109,7 @@ public sealed partial class PlayerTabHeader : Control
Character, Character,
Job, Job,
Antagonist, Antagonist,
RoleType,
Playtime Playtime
} }
} }

View File

@@ -130,8 +130,8 @@ public sealed partial class AirAlarmWindow : FancyWindow
if (!_pumps.TryGetValue(addr, out var pumpControl)) if (!_pumps.TryGetValue(addr, out var pumpControl))
{ {
var control= new PumpControl(pump, addr); var control= new PumpControl(pump, addr);
control.PumpDataChanged += AtmosDeviceDataChanged!.Invoke; control.PumpDataChanged += AtmosDeviceDataChanged;
control.PumpDataCopied += AtmosDeviceDataCopied!.Invoke; control.PumpDataCopied += AtmosDeviceDataCopied;
_pumps.Add(addr, control); _pumps.Add(addr, control);
CVentContainer.AddChild(control); CVentContainer.AddChild(control);
} }
@@ -145,8 +145,8 @@ public sealed partial class AirAlarmWindow : FancyWindow
if (!_scrubbers.TryGetValue(addr, out var scrubberControl)) if (!_scrubbers.TryGetValue(addr, out var scrubberControl))
{ {
var control = new ScrubberControl(scrubber, addr); var control = new ScrubberControl(scrubber, addr);
control.ScrubberDataChanged += AtmosDeviceDataChanged!.Invoke; control.ScrubberDataChanged += AtmosDeviceDataChanged;
control.ScrubberDataCopied += AtmosDeviceDataCopied!.Invoke; control.ScrubberDataCopied += AtmosDeviceDataCopied;
_scrubbers.Add(addr, control); _scrubbers.Add(addr, control);
CScrubberContainer.AddChild(control); CScrubberContainer.AddChild(control);
} }
@@ -161,6 +161,7 @@ public sealed partial class AirAlarmWindow : FancyWindow
{ {
var control = new SensorInfo(sensor, addr); var control = new SensorInfo(sensor, addr);
control.OnThresholdUpdate += AtmosAlarmThresholdChanged; control.OnThresholdUpdate += AtmosAlarmThresholdChanged;
control.SensorDataCopied += AtmosDeviceDataCopied;
_sensors.Add(addr, control); _sensors.Add(addr, control);
CSensorContainer.AddChild(control); CSensorContainer.AddChild(control);
} }

View File

@@ -3,6 +3,9 @@
<CollapsibleHeading Name="SensorAddress" /> <CollapsibleHeading Name="SensorAddress" />
<CollapsibleBody Margin="20 2 2 2"> <CollapsibleBody Margin="20 2 2 2">
<BoxContainer Orientation="Vertical" HorizontalExpand="True"> <BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" Margin ="0 0 0 2">
<Button Name="CCopySettings" Text="{Loc 'air-alarm-ui-thresholds-copy'}" ToolTip="{Loc 'air-alarm-ui-thresholds-copy-tooltip'}" />
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="0 0 2 0" HorizontalExpand="True"> <BoxContainer Orientation="Vertical" Margin="0 0 2 0" HorizontalExpand="True">
<RichTextLabel Name="AlarmStateLabel" /> <RichTextLabel Name="AlarmStateLabel" />
<RichTextLabel Name="PressureLabel" /> <RichTextLabel Name="PressureLabel" />

View File

@@ -12,12 +12,14 @@ namespace Content.Client.Atmos.Monitor.UI.Widgets;
public sealed partial class SensorInfo : BoxContainer public sealed partial class SensorInfo : BoxContainer
{ {
public Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? OnThresholdUpdate; public Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? OnThresholdUpdate;
public event Action<AtmosSensorData>? SensorDataCopied;
private string _address; private string _address;
private ThresholdControl _pressureThreshold; private ThresholdControl _pressureThreshold;
private ThresholdControl _temperatureThreshold; private ThresholdControl _temperatureThreshold;
private Dictionary<Gas, ThresholdControl> _gasThresholds = new(); private Dictionary<Gas, ThresholdControl> _gasThresholds = new();
private Dictionary<Gas, RichTextLabel> _gasLabels = new(); private Dictionary<Gas, RichTextLabel> _gasLabels = new();
private Button _copySettings => CCopySettings;
public SensorInfo(AtmosSensorData data, string address) public SensorInfo(AtmosSensorData data, string address)
{ {
@@ -56,7 +58,7 @@ public sealed partial class SensorInfo : BoxContainer
gasThresholdControl.Margin = new Thickness(20, 2, 2, 2); gasThresholdControl.Margin = new Thickness(20, 2, 2, 2);
gasThresholdControl.ThresholdDataChanged += (type, alarmThreshold, arg3) => gasThresholdControl.ThresholdDataChanged += (type, alarmThreshold, arg3) =>
{ {
OnThresholdUpdate!(_address, type, alarmThreshold, arg3); OnThresholdUpdate?.Invoke(_address, type, alarmThreshold, arg3);
}; };
_gasThresholds.Add(gas, gasThresholdControl); _gasThresholds.Add(gas, gasThresholdControl);
@@ -72,12 +74,17 @@ public sealed partial class SensorInfo : BoxContainer
_pressureThreshold.ThresholdDataChanged += (type, threshold, arg3) => _pressureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
{ {
OnThresholdUpdate!(_address, type, threshold, arg3); OnThresholdUpdate?.Invoke(_address, type, threshold, arg3);
}; };
_temperatureThreshold.ThresholdDataChanged += (type, threshold, arg3) => _temperatureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
{ {
OnThresholdUpdate!(_address, type, threshold, arg3); OnThresholdUpdate?.Invoke(_address, type, threshold, arg3);
};
_copySettings.OnPressed += _ =>
{
SensorDataCopied?.Invoke(data);
}; };
} }

View File

@@ -1,3 +1,4 @@
using Content.Shared.Cargo.Components;
using Content.Shared.Timing; using Content.Shared.Timing;
using Content.Shared.Cargo.Systems; using Content.Shared.Cargo.Systems;
@@ -10,9 +11,9 @@ public sealed class ClientPriceGunSystem : SharedPriceGunSystem
{ {
[Dependency] private readonly UseDelaySystem _useDelay = default!; [Dependency] private readonly UseDelaySystem _useDelay = default!;
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user) protected override bool GetPriceOrBounty(Entity<PriceGunComponent> entity, EntityUid target, EntityUid user)
{ {
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay))) if (!TryComp(entity, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((entity, useDelay)))
return false; return false;
// It feels worse if the cooldown is predicted but the popup isn't! So only do the cooldown reset on the server. // It feels worse if the cooldown is predicted but the popup isn't! So only do the cooldown reset on the server.

View File

@@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Content.Client.PDA;
using Content.Shared.Clothing.Components; using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems; using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory; using Content.Shared.Inventory;
@@ -51,6 +52,15 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{ {
sprite.CopyFrom(otherSprite); sprite.CopyFrom(otherSprite);
} }
// Edgecase for PDAs to include visuals when UI is open
if (TryComp(uid, out PdaBorderColorComponent? borderColor)
&& proto.TryGetComponent(out PdaBorderColorComponent? otherBorderColor, _factory))
{
borderColor.BorderColor = otherBorderColor.BorderColor;
borderColor.AccentHColor = otherBorderColor.AccentHColor;
borderColor.AccentVColor = otherBorderColor.AccentVColor;
}
} }
/// <summary> /// <summary>

View File

@@ -1,15 +1,21 @@
using Content.Client.Clothing.Systems; using Content.Client.Clothing.Systems;
using Content.Shared.Clothing.Components; using Content.Shared.Clothing.Components;
using Content.Shared.Tag;
using Content.Shared.Prototypes;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Clothing.UI; namespace Content.Client.Clothing.UI;
[UsedImplicitly] [UsedImplicitly]
public sealed class ChameleonBoundUserInterface : BoundUserInterface public sealed class ChameleonBoundUserInterface : BoundUserInterface
{ {
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly ChameleonClothingSystem _chameleon; private readonly ChameleonClothingSystem _chameleon;
private readonly TagSystem _tag;
[ViewVariables] [ViewVariables]
private ChameleonMenu? _menu; private ChameleonMenu? _menu;
@@ -17,6 +23,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
public ChameleonBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) public ChameleonBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{ {
_chameleon = EntMan.System<ChameleonClothingSystem>(); _chameleon = EntMan.System<ChameleonClothingSystem>();
_tag = EntMan.System<TagSystem>();
} }
protected override void Open() protected override void Open()
@@ -34,8 +41,25 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
return; return;
var targets = _chameleon.GetValidTargets(st.Slot); var targets = _chameleon.GetValidTargets(st.Slot);
if (st.RequiredTag != null)
{
var newTargets = new List<string>();
foreach (var target in targets)
{
if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))
continue;
if (!proto.TryGetComponent(out TagComponent? tag, _factory) || !_tag.HasTag(tag, st.RequiredTag))
continue;
newTargets.Add(target);
}
_menu?.UpdateState(newTargets, st.SelectedId);
} else
{
_menu?.UpdateState(targets, st.SelectedId); _menu?.UpdateState(targets, st.SelectedId);
} }
}
private void OnIdSelected(string selectedId) private void OnIdSelected(string selectedId)
{ {

View File

@@ -27,7 +27,11 @@
<!-- Header text --> <!-- Header text -->
<BoxContainer MinHeight="60" Orientation="Vertical" VerticalAlignment="Center"> <BoxContainer MinHeight="60" Orientation="Vertical" VerticalAlignment="Center">
<Label Name="CallStatusText" Margin="10 5 10 0" ReservesSpace="False"/> <Label Name="CallStatusText" Margin="10 5 10 0" ReservesSpace="False"/>
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0" ReservesSpace="False"/> <BoxContainer Name="CallerIdContainer" Orientation="Vertical" ReservesSpace="False">
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0"/>
<Label Text="{Loc 'holopad-window-relay-label'}" Margin="10 10 10 0" ReservesSpace="False"/>
<RichTextLabel Name="HolopadIdText" HorizontalAlignment="Center" Margin="0 0 0 10"/>
</BoxContainer>
</BoxContainer> </BoxContainer>
<!-- Controls (the answer call button is absent when the phone is not ringing) --> <!-- Controls (the answer call button is absent when the phone is not ringing) -->
@@ -70,6 +74,12 @@
</PanelContainer> </PanelContainer>
<PanelContainer Name="HolopadContactListPanel"> <PanelContainer Name="HolopadContactListPanel">
<BoxContainer Orientation="Vertical">
<!-- Contact filter -->
<LineEdit Name="SearchLineEdit" HorizontalExpand="True" Margin="4, 4, 4, 0"
PlaceHolder="{Loc holopad-window-filter-line-placeholder}" />
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256"> <ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256">
<!-- If there is no data yet, this will be displayed --> <!-- If there is no data yet, this will be displayed -->
@@ -80,6 +90,7 @@
<!-- Container for the contacts --> <!-- Container for the contacts -->
<BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/> <BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/>
</ScrollContainer> </ScrollContainer>
</BoxContainer>
</PanelContainer> </PanelContainer>
</BoxContainer> </BoxContainer>

View File

@@ -171,8 +171,10 @@ public sealed partial class HolopadWindow : FancyWindow
// Caller ID text // Caller ID text
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11); var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
var holoapdId = _telephoneSystem.GetFormattedDeviceIdForEntity(telephone.LastCallerId.Item3, Color.LightGray, "Default", 11);
CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId)); CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
HolopadIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(holoapdId));
LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId)); LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
// Sort holopads alphabetically // Sort holopads alphabetically
@@ -236,10 +238,13 @@ public sealed partial class HolopadWindow : FancyWindow
// Make / update required children // Make / update required children
foreach (var child in ContactsList.Children) foreach (var child in ContactsList.Children)
{ {
if (child is not HolopadContactButton) if (child is not HolopadContactButton contactButton)
continue; continue;
var contactButton = (HolopadContactButton)child; var passesFilter = string.IsNullOrEmpty(SearchLineEdit.Text) ||
contactButton.Text?.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase) == true;
contactButton.Visible = passesFilter;
contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons); contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
} }
@@ -290,7 +295,7 @@ public sealed partial class HolopadWindow : FancyWindow
FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0); FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow); ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible; CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
CallerIdText.Visible = (_currentState == TelephoneState.Ringing); CallerIdContainer.Visible = (_currentState == TelephoneState.Ringing);
AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing); AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
} }
@@ -316,6 +321,7 @@ public sealed partial class HolopadWindow : FancyWindow
HorizontalExpand = true; HorizontalExpand = true;
SetHeight = 32; SetHeight = 32;
Margin = new Thickness(0f, 1f, 0f, 1f); Margin = new Thickness(0f, 1f, 0f, 1f);
ReservesSpace = false;
} }
public void UpdateValues(NetEntity netEntity, string label) public void UpdateValues(NetEntity netEntity, string label)

View File

@@ -5,19 +5,21 @@ using Content.Client.Lobby.UI;
using Content.Client.Message; using Content.Client.Message;
using Content.Client.UserInterface.Systems.Chat; using Content.Client.UserInterface.Systems.Chat;
using Content.Client.Voting; using Content.Client.Voting;
using Content.Shared.CCVar;
using Robust.Client; using Robust.Client;
using Robust.Client.Console; using Robust.Client.Console;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Client.Lobby namespace Content.Client.Lobby
{ {
public sealed class LobbyState : Robust.Client.State.State public sealed class LobbyState : Robust.Client.State.State
{ {
[Dependency] private readonly IBaseClient _baseClient = default!; [Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IResourceCache _resourceCache = default!;
@@ -49,7 +51,17 @@ namespace Content.Client.Lobby
_voteManager.SetPopupContainer(Lobby.VoteContainer); _voteManager.SetPopupContainer(Lobby.VoteContainer);
LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide); LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide);
Lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
var lobbyNameCvar = _cfg.GetCVar(CCVars.ServerLobbyName);
var serverName = _baseClient.GameInfo?.ServerName ?? string.Empty;
Lobby.ServerName.Text = string.IsNullOrEmpty(lobbyNameCvar)
? Loc.GetString("ui-lobby-title", ("serverName", serverName))
: lobbyNameCvar;
var width = _cfg.GetCVar(CCVars.ServerLobbyRightPanelWidth);
Lobby.RightSide.SetWidth = width;
UpdateLobbyUi(); UpdateLobbyUi();
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed; Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;

View File

@@ -36,19 +36,20 @@ public sealed partial class LoadoutContainer : BoxContainer
if (_protoManager.TryIndex(proto, out var loadProto)) if (_protoManager.TryIndex(proto, out var loadProto))
{ {
var ent = _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto); var ent = loadProto.DummyEntity ?? _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
if (ent == null)
return;
if (ent != null)
{
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace); _entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity); Sprite.SetEntity(_entity);
var spriteTooltip = new Tooltip(); var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription)); spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
TooltipSupplier = _ => spriteTooltip; TooltipSupplier = _ => spriteTooltip;
} }
} }
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {

View File

@@ -62,14 +62,12 @@
<Control Access="Public" Visible="False" Name="CharacterSetupState" VerticalExpand="True" /> <Control Access="Public" Visible="False" Name="CharacterSetupState" VerticalExpand="True" />
</BoxContainer> </BoxContainer>
<!-- Right Panel --> <!-- Right Panel -->
<PanelContainer Name="RightSide" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True" <PanelContainer Name="RightSide" Access="Public" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<BoxContainer Orientation="Vertical" HorizontalExpand="True"> <BoxContainer Orientation="Vertical" HorizontalExpand="True">
<!-- Top row --> <!-- Top row -->
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public" <BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public"
SeparationOverride="4"> SeparationOverride="4">
<Label Margin="8 0 0 0" StyleClasses="LabelHeadingBigger" VAlign="Center"
Text="{Loc 'ui-lobby-title'}" />
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center" <Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center"
HorizontalExpand="True" HorizontalAlignment="Center" /> HorizontalExpand="True" HorizontalAlignment="Center" />
</BoxContainer> </BoxContainer>

View File

@@ -141,6 +141,11 @@ namespace Content.Client.PDA
_pdaOwner = state.PdaOwnerInfo.ActualOwnerName; _pdaOwner = state.PdaOwnerInfo.ActualOwnerName;
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner", PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("actualOwnerName", _pdaOwner))); ("actualOwnerName", _pdaOwner)));
PdaOwnerLabel.Visible = true;
}
else
{
PdaOwnerLabel.Visible = false;
} }

View File

@@ -1,48 +1,8 @@
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Light;
using Robust.Client.GameObjects;
namespace Content.Client.PDA; namespace Content.Client.PDA;
public sealed class PdaSystem : SharedPdaSystem public sealed class PdaSystem : SharedPdaSystem
{ {
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PdaComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(EntityUid uid, PdaComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (Appearance.TryGetData<bool>(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
args.Sprite.LayerSetVisible(PdaVisualLayers.Flashlight, isFlashlightOn);
if (Appearance.TryGetData<bool>(uid, PdaVisuals.IdCardInserted, out var isCardInserted, args.Component))
args.Sprite.LayerSetVisible(PdaVisualLayers.IdLight, isCardInserted);
}
protected override void OnComponentInit(EntityUid uid, PdaComponent component, ComponentInit args)
{
base.OnComponentInit(uid, component, args);
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
if (component.State != null)
sprite.LayerSetState(PdaVisualLayers.Base, component.State);
sprite.LayerSetVisible(PdaVisualLayers.Flashlight, component.FlashlightOn);
sprite.LayerSetVisible(PdaVisualLayers.IdLight, component.IdSlot.StartingItem != null);
}
public enum PdaVisualLayers : byte
{
Base,
Flashlight,
IdLight
}
} }

View File

@@ -0,0 +1,30 @@
using Content.Shared.Light;
using Content.Shared.PDA;
using Robust.Client.GameObjects;
namespace Content.Client.PDA;
public sealed class PdaVisualizerSystem : VisualizerSystem<PdaVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, PdaVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (AppearanceSystem.TryGetData<string>(uid, PdaVisuals.PdaType, out var pdaType, args.Component))
args.Sprite.LayerSetState(PdaVisualLayers.Base, pdaType);
if (AppearanceSystem.TryGetData<bool>(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
args.Sprite.LayerSetVisible(PdaVisualLayers.Flashlight, isFlashlightOn);
if (AppearanceSystem.TryGetData<bool>(uid, PdaVisuals.IdCardInserted, out var isCardInserted, args.Component))
args.Sprite.LayerSetVisible(PdaVisualLayers.IdLight, isCardInserted);
}
public enum PdaVisualLayers : byte
{
Base,
Flashlight,
IdLight
}
}

View File

@@ -0,0 +1,14 @@
namespace Content.Client.PDA;
/// <summary>
/// Used for visualizing PDA visuals.
/// </summary>
[RegisterComponent]
public sealed partial class PdaVisualsComponent : Component
{
public string? BorderColor;
public string? AccentHColor;
public string? AccentVColor;
}

View File

@@ -28,6 +28,7 @@ public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindo
public SensorMonitoringWindow() public SensorMonitoringWindow()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
} }
public void UpdateState(ConsoleUIState state) public void UpdateState(ConsoleUIState state)

View File

@@ -104,7 +104,7 @@ public sealed class TippyUIController : UIController
? -WaddleRotation ? -WaddleRotation
: WaddleRotation; : WaddleRotation;
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step)) if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step) && step.FootstepSoundCollection != null)
{ {
var audioParams = step.FootstepSoundCollection.Params var audioParams = step.FootstepSoundCollection.Params
.AddVolume(-7f) .AddVolume(-7f)

View File

@@ -7,7 +7,9 @@ using Content.Client.UserInterface.Systems.Character.Controls;
using Content.Client.UserInterface.Systems.Character.Windows; using Content.Client.UserInterface.Systems.Character.Windows;
using Content.Client.UserInterface.Systems.Objectives.Controls; using Content.Client.UserInterface.Systems.Objectives.Controls;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Objectives.Systems; using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Player; using Robust.Client.Player;
@@ -15,6 +17,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using static Content.Client.CharacterInfo.CharacterInfoSystem; using static Content.Client.CharacterInfo.CharacterInfoSystem;
using static Robust.Client.UserInterface.Controls.BaseButton; using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -24,10 +27,25 @@ namespace Content.Client.UserInterface.Systems.Character;
[UsedImplicitly] [UsedImplicitly]
public sealed class CharacterUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CharacterInfoSystem> public sealed class CharacterUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CharacterInfoSystem>
{ {
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!; [UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
[UISystemDependency] private readonly SpriteSystem _sprite = default!; [UISystemDependency] private readonly SpriteSystem _sprite = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = _logMan.GetSawmill("character");
SubscribeNetworkEvent<MindRoleTypeChangedEvent>(OnRoleTypeChanged);
}
private CharacterWindow? _window; private CharacterWindow? _window;
private MenuButton? CharacterButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.CharacterButton; private MenuButton? CharacterButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.CharacterButton;
@@ -110,6 +128,9 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
var (entity, job, objectives, briefing, entityName) = data; var (entity, job, objectives, briefing, entityName) = data;
_window.SpriteView.SetEntity(entity); _window.SpriteView.SetEntity(entity);
UpdateRoleType();
_window.NameLabel.Text = entityName; _window.NameLabel.Text = entityName;
_window.SubText.Text = job; _window.SubText.Text = job;
_window.Objectives.RemoveAllChildren(); _window.Objectives.RemoveAllChildren();
@@ -173,6 +194,37 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
_window.RolePlaceholder.Visible = briefing == null && !controls.Any() && !objectives.Any(); _window.RolePlaceholder.Visible = briefing == null && !controls.Any() && !objectives.Any();
} }
private void OnRoleTypeChanged(MindRoleTypeChangedEvent ev, EntitySessionEventArgs _)
{
UpdateRoleType();
}
private void UpdateRoleType()
{
if (_window == null || !_window.IsOpen)
return;
if (!_ent.TryGetComponent<MindContainerComponent>(_player.LocalEntity, out var container)
|| container.Mind is null)
return;
if (!_ent.TryGetComponent<MindComponent>(container.Mind.Value, out var mind))
return;
var roleText = Loc.GetString("role-type-crew-aligned-name");
var color = Color.White;
if (_prototypeManager.TryIndex(mind.RoleType, out var proto))
{
roleText = Loc.GetString(proto.Name);
color = proto.Color;
}
else
_sawmill.Error($"{_player.LocalEntity} has invalid Role Type '{mind.RoleType}'. Displaying '{roleText}' instead");
_window.RoleType.Text = roleText;
_window.RoleType.FontColorOverride = color;
}
private void CharacterDetached(EntityUid uid) private void CharacterDetached(EntityUid uid)
{ {
CloseWindow(); CloseWindow();

View File

@@ -7,6 +7,7 @@
MinHeight="545"> MinHeight="545">
<ScrollContainer> <ScrollContainer>
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<Label Name="RoleType" VerticalAlignment="Top" Margin="0 6 0 10" HorizontalAlignment="Center" StyleClasses="LabelHeading" Access="Public"/>
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64"/> <SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64"/>
<BoxContainer Orientation="Vertical" VerticalAlignment="Top"> <BoxContainer Orientation="Vertical" VerticalAlignment="Top">

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Robust.Shared; using Robust.Shared;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Log; using Robust.Shared.Log;
@@ -216,14 +217,17 @@ namespace Content.IntegrationTests.Tests
/// generally not spawn unrelated / detached entities. Any entities that do get spawned should be parented to /// generally not spawn unrelated / detached entities. Any entities that do get spawned should be parented to
/// the spawned entity (e.g., in a container). If an entity needs to spawn an entity somewhere in null-space, /// the spawned entity (e.g., in a container). If an entity needs to spawn an entity somewhere in null-space,
/// it should delete that entity when it is no longer required. This test mainly exists to prevent "entity leak" /// it should delete that entity when it is no longer required. This test mainly exists to prevent "entity leak"
/// bugs, where spawning some entity starts spawning unrelated entities in null space. /// bugs, where spawning some entity starts spawning unrelated entities in null space that stick around after
/// the original entity is gone.
///
/// Note that this isn't really a strict requirement, and there are probably quite a few edge cases. Its a pretty
/// crude test to try catch issues like this, and possibly should just be disabled.
/// </remarks> /// </remarks>
[Test] [Test]
public async Task SpawnAndDeleteEntityCountTest() public async Task SpawnAndDeleteEntityCountTest()
{ {
var settings = new PoolSettings { Connected = true, Dirty = true }; var settings = new PoolSettings { Connected = true, Dirty = true };
await using var pair = await PoolManager.GetServerClient(settings); await using var pair = await PoolManager.GetServerClient(settings);
var mapManager = pair.Server.ResolveDependency<IMapManager>();
var mapSys = pair.Server.System<SharedMapSystem>(); var mapSys = pair.Server.System<SharedMapSystem>();
var server = pair.Server; var server = pair.Server;
var client = pair.Client; var client = pair.Client;
@@ -261,6 +265,9 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
foreach (var protoId in protoIds) foreach (var protoId in protoIds)
{ {
// TODO fix ninja // TODO fix ninja
@@ -268,8 +275,8 @@ namespace Content.IntegrationTests.Tests
if (protoId == "MobHumanSpaceNinja") if (protoId == "MobHumanSpaceNinja")
continue; continue;
var count = server.EntMan.EntityCount; var count = Count(server.EntMan);
var clientCount = client.EntMan.EntityCount; var clientCount = Count(client.EntMan);
EntityUid uid = default; EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords)); await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords));
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
@@ -277,30 +284,30 @@ namespace Content.IntegrationTests.Tests
// If the entity deleted itself, check that it didn't spawn other entities // If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid)) if (!server.EntMan.EntityExists(uid))
{ {
if (server.EntMan.EntityCount != count) if (Count(server.EntMan) != count)
{ {
Assert.Fail($"Server prototype {protoId} failed on deleting itself"); Assert.Fail($"Server prototype {protoId} failed on deleting itself");
} }
if (client.EntMan.EntityCount != clientCount) if (Count(client.EntMan) != clientCount)
{ {
Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" + Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" + $"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}."); $"Server was {count}.");
} }
continue; continue;
} }
// Check that the number of entities has increased. // Check that the number of entities has increased.
if (server.EntMan.EntityCount <= count) if (Count(server.EntMan) <= count)
{ {
Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase"); Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase");
} }
if (client.EntMan.EntityCount <= clientCount) if (Count(client.EntMan) <= clientCount)
{ {
Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" + Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" +
$"Expected at least {clientCount} and found {client.EntMan.EntityCount}. " + $"Expected at least {clientCount} and found {Count(client.EntMan)}. " +
$"Server was {count}"); $"Server was {count}");
} }
@@ -308,15 +315,15 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
// Check that the number of entities has gone back to the original value. // Check that the number of entities has gone back to the original value.
if (server.EntMan.EntityCount != count) if (Count(server.EntMan) != count)
{ {
Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly"); Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly");
} }
if (client.EntMan.EntityCount != clientCount) if (Count(client.EntMan) != clientCount)
{ {
Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" + Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" + $"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}."); $"Server was {count}.");
} }
} }

View File

@@ -35,7 +35,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{ {
playerUid = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault(); playerUid = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
#pragma warning disable NUnit2045 // Interdependent assertions. #pragma warning disable NUnit2045 // Interdependent assertions.
Assert.That(playerUid, Is.Not.EqualTo(default)); Assert.That(playerUid, Is.Not.EqualTo(default(EntityUid)));
// Making sure it exists // Making sure it exists
Assert.That(entManager.HasComponent<AlertsComponent>(playerUid)); Assert.That(entManager.HasComponent<AlertsComponent>(playerUid));
#pragma warning restore NUnit2045 #pragma warning restore NUnit2045

View File

@@ -65,9 +65,8 @@ namespace Content.IntegrationTests.Tests
"Cog", "Cog",
"Gate", "Gate",
"Amber", "Amber",
"Loop" "Loop",
"Elkridge"
}; };
/// <summary> /// <summary>

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>

View File

@@ -116,10 +116,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName; targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName;
targetLabelColor = Color.White; targetLabelColor = Color.White;
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderComponent)) if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderEnt))
return; return;
var currentAccessHashsets = accessReaderComponent.AccessLists; var currentAccessHashsets = accessReaderEnt.Value.Comp.AccessLists;
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray(); currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray();
} }
@@ -210,10 +210,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
return; return;
} }
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReader)) if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReaderEnt))
return; return;
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists); var oldTags = ConvertAccessHashSetsToList(accessReaderEnt.Value.Comp.AccessLists);
var privilegedId = component.PrivilegedIdSlot.Item; var privilegedId = component.PrivilegedIdSlot.Item;
if (oldTags.SequenceEqual(newAccessList)) if (oldTags.SequenceEqual(newAccessList))
@@ -242,10 +242,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList(); var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
_adminLogger.Add(LogType.Action, LogImpact.Medium, _adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(player):player} has modified {ToPrettyString(component.TargetAccessReaderId):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); $"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReader.AccessLists = ConvertAccessListToHashSet(newAccessList); accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
Dirty(component.TargetAccessReaderId, accessReader); Dirty(accessReaderEnt.Value);
} }
/// <summary> /// <summary>

View File

@@ -33,16 +33,16 @@ public sealed class AdminWhoCommand : IConsoleCommand
var first = true; var first = true;
foreach (var admin in adminMgr.ActiveAdmins) foreach (var admin in adminMgr.ActiveAdmins)
{ {
if (!first)
sb.Append('\n');
first = false;
var adminData = adminMgr.GetAdminData(admin)!; var adminData = adminMgr.GetAdminData(admin)!;
DebugTools.AssertNotNull(adminData); DebugTools.AssertNotNull(adminData);
if (adminData.Stealth && !seeStealth) if (adminData.Stealth && !seeStealth)
continue; continue;
if (!first)
sb.Append('\n');
first = false;
sb.Append(admin.Name); sb.Append(admin.Name);
if (adminData.Title is { } title) if (adminData.Title is { } title)
sb.Append($": [{title}]"); sb.Append($": [{title}]");

View File

@@ -408,6 +408,17 @@ namespace Content.Server.Administration.Managers
} }
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(ICommonSession session) private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(ICommonSession session)
{
var result = await LoadAdminDataCore(session);
// Make sure admin didn't disconnect while data was loading.
if (session.Status != SessionStatus.InGame)
return null;
return result;
}
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminDataCore(ICommonSession session)
{ {
var promoteHost = IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal) var promoteHost = IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)
|| _promotedPlayers.Contains(session.UserId) || _promotedPlayers.Contains(session.UserId)

View File

@@ -17,7 +17,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -32,6 +31,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Systems; namespace Content.Server.Administration.Systems;
@@ -48,6 +48,7 @@ public sealed class AdminSystem : EntitySystem
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!; [Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedRoleSystem _role = default!; [Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -165,7 +166,8 @@ public sealed class AdminSystem : EntitySystem
private void OnRoleEvent(RoleEvent ev) private void OnRoleEvent(RoleEvent ev)
{ {
var session = _minds.GetSession(ev.Mind); var session = _minds.GetSession(ev.Mind);
if (!ev.Antagonist || session == null)
if (!ev.RoleTypeUpdate || session == null)
return; return;
UpdatePlayerList(session); UpdatePlayerList(session);
@@ -239,9 +241,16 @@ public sealed class AdminSystem : EntitySystem
} }
var antag = false; var antag = false;
RoleTypePrototype roleType = new();
var startingRole = string.Empty; var startingRole = string.Empty;
if (_minds.TryGetMind(session, out var mindId, out _)) if (_minds.TryGetMind(session, out var mindId, out var mindComp))
{ {
if (_proto.TryIndex(mindComp.RoleType, out var role))
roleType = role;
else
Log.Error($"{ToPrettyString(mindId)} has invalid Role Type '{mindComp.RoleType}'. Displaying '{Loc.GetString(roleType.Name)}' instead");
antag = _role.MindIsAntagonist(mindId); antag = _role.MindIsAntagonist(mindId);
startingRole = _jobs.MindTryGetJobName(mindId); startingRole = _jobs.MindTryGetJobName(mindId);
} }
@@ -255,7 +264,7 @@ public sealed class AdminSystem : EntitySystem
overallPlaytime = playTime; overallPlaytime = playTime;
} }
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId, return new PlayerInfo(name, entityName, identityName, startingRole, antag, roleType, GetNetEntity(session?.AttachedEntity), data.UserId,
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime); connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
} }

View File

@@ -97,6 +97,6 @@ public sealed partial class ReagentProducerAnomalyComponent : Component
/// <summary> /// <summary>
/// Solution where the substance is generated /// Solution where the substance is generated
/// </summary> /// </summary>
[DataField("solutionRef")] [ViewVariables]
public Entity<SolutionComponent>? Solution = null; public Entity<SolutionComponent>? Solution = null;
} }

View File

@@ -153,7 +153,7 @@ public partial struct AntagSelectionDefinition()
/// List of Mind Role Prototypes to be added to the player's mind. /// List of Mind Role Prototypes to be added to the player's mind.
/// </summary> /// </summary>
[DataField] [DataField]
public List<ProtoId<EntityPrototype>>? MindRoles; public List<EntProtoId>? MindRoles;
/// <summary> /// <summary>
/// A set of starting gear that's equipped to the player. /// A set of starting gear that's equipped to the player.

View File

@@ -131,6 +131,19 @@ public sealed class AirAlarmSystem : EntitySystem
SyncDevice(uid, address); SyncDevice(uid, address);
} }
private void SetAllThresholds(EntityUid uid, string address, AtmosSensorData data)
{
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = AtmosMonitorSystem.AtmosMonitorSetAllThresholdsCmd,
[AtmosMonitorSystem.AtmosMonitorAllThresholdData] = data
};
_deviceNet.QueuePacket(uid, address, payload);
SyncDevice(uid, address);
}
/// <summary> /// <summary>
/// Sync this air alarm's mode with the rest of the network. /// Sync this air alarm's mode with the rest of the network.
/// </summary> /// </summary>
@@ -341,6 +354,13 @@ public sealed class AirAlarmSystem : EntitySystem
SetData(uid, addr, args.Data); SetData(uid, addr, args.Data);
} }
break; break;
case AtmosSensorData sensorData:
foreach (string addr in component.SensorData.Keys)
{
SetAllThresholds(uid, addr, sensorData);
}
break;
} }
} }

View File

@@ -33,10 +33,11 @@ public sealed class AtmosMonitorSystem : EntitySystem
// Commands // Commands
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold"; public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
public const string AtmosMonitorSetAllThresholdsCmd = "atmos_monitor_set_all_thresholds";
// Packet data // Packet data
public const string AtmosMonitorThresholdData = "atmos_monitor_threshold_data"; public const string AtmosMonitorThresholdData = "atmos_monitor_threshold_data";
public const string AtmosMonitorAllThresholdData = "atmos_monitor_all_threshold_data";
public const string AtmosMonitorThresholdDataType = "atmos_monitor_threshold_type"; public const string AtmosMonitorThresholdDataType = "atmos_monitor_threshold_type";
public const string AtmosMonitorThresholdGasType = "atmos_monitor_threshold_gas"; public const string AtmosMonitorThresholdGasType = "atmos_monitor_threshold_gas";
@@ -138,7 +139,12 @@ public sealed class AtmosMonitorSystem : EntitySystem
args.Data.TryGetValue(AtmosMonitorThresholdGasType, out Gas? gas); args.Data.TryGetValue(AtmosMonitorThresholdGasType, out Gas? gas);
SetThreshold(uid, thresholdType.Value, thresholdData, gas); SetThreshold(uid, thresholdType.Value, thresholdData, gas);
} }
break;
case AtmosMonitorSetAllThresholdsCmd:
if (args.Data.TryGetValue(AtmosMonitorAllThresholdData, out AtmosSensorData? allThresholdData))
{
SetAllThresholds(uid, allThresholdData);
}
break; break;
case AtmosDeviceNetworkSystem.SyncData: case AtmosDeviceNetworkSystem.SyncData:
var payload = new NetworkPayload(); var payload = new NetworkPayload();
@@ -403,4 +409,20 @@ public sealed class AtmosMonitorSystem : EntitySystem
} }
} }
/// <summary>
/// Sets all of a monitor's thresholds at once according to the incoming
/// AtmosSensorData object's thresholds.
/// </summary>
/// <param name="uid">The entity's uid</param>
/// <param name="allThresholdData">An AtmosSensorData object from which the thresholds will be loaded.</param>
public void SetAllThresholds(EntityUid uid, AtmosSensorData allThresholdData)
{
SetThreshold(uid, AtmosMonitorThresholdType.Temperature, allThresholdData.TemperatureThreshold);
SetThreshold(uid, AtmosMonitorThresholdType.Pressure, allThresholdData.PressureThreshold);
foreach (var gas in Enum.GetValues<Gas>())
{
SetThreshold(uid, AtmosMonitorThresholdType.Gas, allThresholdData.GasThresholds[gas], gas);
}
}
} }

View File

@@ -25,7 +25,7 @@ public sealed partial class GasCondenserComponent : Component
/// <summary> /// <summary>
/// The solution that gases are condensed into. /// The solution that gases are condensed into.
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? Solution = null; public Entity<SolutionComponent>? Solution = null;
/// <summary> /// <summary>

View File

@@ -157,22 +157,22 @@ namespace Content.Server.Body.Components
/// <summary> /// <summary>
/// Internal solution for blood storage /// Internal solution for blood storage
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? BloodSolution = null; public Entity<SolutionComponent>? BloodSolution;
/// <summary> /// <summary>
/// Internal solution for reagent storage /// Internal solution for reagent storage
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? ChemicalSolution = null; public Entity<SolutionComponent>? ChemicalSolution;
/// <summary> /// <summary>
/// Temporary blood solution. /// Temporary blood solution.
/// When blood is lost, it goes to this solution, and when this /// When blood is lost, it goes to this solution, and when this
/// solution hits a certain cap, the blood is actually spilled as a puddle. /// solution hits a certain cap, the blood is actually spilled as a puddle.
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? TemporarySolution = null; public Entity<SolutionComponent>? TemporarySolution;
/// <summary> /// <summary>
/// Variable that stores the amount of status time added by having a low blood level. /// Variable that stores the amount of status time added by having a low blood level.

View File

@@ -26,7 +26,7 @@ public sealed partial class LungComponent : Component
/// <summary> /// <summary>
/// The solution on this entity that these lungs act on. /// The solution on this entity that these lungs act on.
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? Solution = null; public Entity<SolutionComponent>? Solution = null;
/// <summary> /// <summary>

View File

@@ -25,8 +25,8 @@ namespace Content.Server.Body.Components
/// <summary> /// <summary>
/// The solution inside of this stomach this transfers reagents to the body. /// The solution inside of this stomach this transfers reagents to the body.
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? Solution = null; public Entity<SolutionComponent>? Solution;
/// <summary> /// <summary>
/// What solution should this stomach push reagents into, on the body? /// What solution should this stomach push reagents into, on the body?

View File

@@ -96,6 +96,6 @@ public sealed partial class PlantHolderComponent : Component
[DataField] [DataField]
public string SoilSolutionName = "soil"; public string SoilSolutionName = "soil";
[DataField] [ViewVariables]
public Entity<SolutionComponent>? SoilSolution = null; public Entity<SolutionComponent>? SoilSolution = null;
} }

View File

@@ -1,7 +1,9 @@
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Cargo.Components;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Timing; using Content.Shared.Timing;
using Content.Shared.Cargo.Systems; using Content.Shared.Cargo.Systems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Cargo.Systems; namespace Content.Server.Cargo.Systems;
@@ -11,12 +13,12 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
[Dependency] private readonly PricingSystem _pricingSystem = default!; [Dependency] private readonly PricingSystem _pricingSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly CargoSystem _bountySystem = default!; [Dependency] private readonly CargoSystem _bountySystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user) protected override bool GetPriceOrBounty(Entity<PriceGunComponent> entity, EntityUid target, EntityUid user)
{ {
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay))) if (!TryComp(entity.Owner, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((entity.Owner, useDelay)))
return false; return false;
// Check if we're scanning a bounty crate // Check if we're scanning a bounty crate
if (_bountySystem.IsBountyComplete(target, out _)) if (_bountySystem.IsBountyComplete(target, out _))
{ {
@@ -25,10 +27,15 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
else // Otherwise appraise the price else // Otherwise appraise the price
{ {
var price = _pricingSystem.GetPrice(target); var price = _pricingSystem.GetPrice(target);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(target, EntityManager)), ("price", $"{price:F2}")), user, user); _popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result",
("object", Identity.Entity(target, EntityManager)),
("price", $"{price:F2}")),
user,
user);
} }
_useDelay.TryResetDelay((priceGunUid, useDelay)); _audio.PlayPvs(entity.Comp.AppraisalSound, entity.Owner);
_useDelay.TryResetDelay((entity.Owner, useDelay));
return true; return true;
} }
} }

View File

@@ -1,11 +1,14 @@
using Content.Server.Administration.Logs;
using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges; using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Database;
namespace Content.Server.CartridgeLoader.Cartridges; namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class NotekeeperCartridgeSystem : EntitySystem public sealed class NotekeeperCartridgeSystem : EntitySystem
{ {
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!; [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -36,16 +39,19 @@ public sealed class NotekeeperCartridgeSystem : EntitySystem
if (message.Action == NotekeeperUiAction.Add) if (message.Action == NotekeeperUiAction.Add)
{ {
component.Notes.Add(message.Note); component.Notes.Add(message.Note);
_adminLogger.Add(LogType.PdaInteract, LogImpact.Low,
$"{ToPrettyString(args.Actor)} added a note to PDA: '{message.Note}' contained on: {ToPrettyString(uid)}");
} }
else else
{ {
component.Notes.Remove(message.Note); component.Notes.Remove(message.Note);
_adminLogger.Add(LogType.PdaInteract, LogImpact.Low,
$"{ToPrettyString(args.Actor)} removed a note from PDA: '{message.Note}' was contained on: {ToPrettyString(uid)}");
} }
UpdateUiState(uid, GetEntity(args.LoaderUid), component); UpdateUiState(uid, GetEntity(args.LoaderUid), component);
} }
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, NotekeeperCartridgeComponent? component) private void UpdateUiState(EntityUid uid, EntityUid loaderUid, NotekeeperCartridgeComponent? component)
{ {
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref component))

View File

@@ -20,7 +20,7 @@ public sealed partial class SolutionRegenerationComponent : Component
/// <summary> /// <summary>
/// The solution to add reagents to. /// The solution to add reagents to.
/// </summary> /// </summary>
[DataField] [ViewVariables]
public Entity<SolutionComponent>? SolutionRef = null; public Entity<SolutionComponent>? SolutionRef = null;
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Content.Server.IdentityManagement; using Content.Server.IdentityManagement;
using Content.Shared.Clothing.Components; using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems; using Content.Shared.Clothing.EntitySystems;
using Content.Shared.IdentityManagement.Components; using Content.Shared.IdentityManagement.Components;
@@ -63,7 +63,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref component))
return; return;
var state = new ChameleonBoundUserInterfaceState(component.Slot, component.Default); var state = new ChameleonBoundUserInterfaceState(component.Slot, component.Default, component.RequireTag);
_uiSystem.SetUiState(uid, ChameleonUiKey.Key, state); _uiSystem.SetUiState(uid, ChameleonUiKey.Key, state);
} }
@@ -84,7 +84,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
// make sure that it is valid change // make sure that it is valid change
if (string.IsNullOrEmpty(protoId) || !_proto.TryIndex(protoId, out EntityPrototype? proto)) if (string.IsNullOrEmpty(protoId) || !_proto.TryIndex(protoId, out EntityPrototype? proto))
return; return;
if (!IsValidTarget(proto, component.Slot)) if (!IsValidTarget(proto, component.Slot, component.RequireTag))
return; return;
component.Default = protoId; component.Default = protoId;

View File

@@ -512,7 +512,8 @@ public sealed partial class ExplosionSystem
List<(Vector2i GridIndices, Tile Tile)> damagedTiles, List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
ExplosionPrototype type) ExplosionPrototype type)
{ {
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef) if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef
|| tileDef.Indestructible)
return; return;
if (!CanCreateVacuum) if (!CanCreateVacuum)

View File

@@ -356,6 +356,11 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
// + if the bomb is big enough, people outside of it too // + if the bomb is big enough, people outside of it too
// this is capped to 30 because otherwise really huge bombs // this is capped to 30 because otherwise really huge bombs
// will attempt to play regular audio for people who can't hear it anyway because the epicenter is so far away // will attempt to play regular audio for people who can't hear it anyway because the epicenter is so far away
//
// TODO EXPLOSION redo this.
// Use the Filter.Pvs range-multiplier option instead of AddInRange.
// Also the default PVS range is 25*2 = 50. So capping it at 30 makes no sense here.
// So actually maybe don't use Filter.Pvs at all and only use AddInRange?
var audioRange = Math.Min(iterationIntensity.Count * 2, MaxExplosionAudioRange); var audioRange = Math.Min(iterationIntensity.Count * 2, MaxExplosionAudioRange);
var filter = Filter.Pvs(pos).AddInRange(pos, audioRange); var filter = Filter.Pvs(pos).AddInRange(pos, audioRange);
var sound = iterationIntensity.Count < queued.Proto.SmallSoundIterationThreshold var sound = iterationIntensity.Count < queued.Proto.SmallSoundIterationThreshold

View File

@@ -222,8 +222,6 @@ namespace Content.Server.GameTicking
_mind.SetUserId(newMind, data.UserId); _mind.SetUserId(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId); var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_playTimeTrackings.PlayerRolesChanged(player); _playTimeTrackings.PlayerRolesChanged(player);
@@ -233,6 +231,9 @@ namespace Content.Server.GameTicking
_mind.TransferTo(newMind, mob); _mind.TransferTo(newMind, mob);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
if (lateJoin && !silent) if (lateJoin && !silent)
{ {
if (jobPrototype.JoinNotifyCrew) if (jobPrototype.JoinNotifyCrew)

View File

@@ -0,0 +1,37 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Ghost.Components;
/// <summary>
/// Causes this entity to react to ghost player using the "Boo!" action by speaking
/// a randomly chosen message from a specified set.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class SpookySpeakerComponent : Component
{
/// <summary>
/// ProtoId of the LocalizedDataset to use for messages.
/// </summary>
[DataField(required: true)]
public ProtoId<LocalizedDatasetPrototype> MessageSet;
/// <summary>
/// Probability that this entity will speak if activated by a Boo action.
/// This is so whole banks of entities don't trigger at the same time.
/// </summary>
[DataField]
public float SpeakChance = 0.5f;
/// <summary>
/// Minimum time that must pass after speaking before this entity can speak again.
/// </summary>
[DataField]
public TimeSpan Cooldown = TimeSpan.FromSeconds(30);
/// <summary>
/// Time when the cooldown will have elapsed and the entity can speak again.
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextSpeakTime;
}

View File

@@ -24,6 +24,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Storage.Components; using Content.Shared.Storage.Components;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
@@ -33,6 +34,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Ghost namespace Content.Server.Ghost
@@ -61,6 +63,8 @@ namespace Content.Server.Ghost
[Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private EntityQuery<GhostComponent> _ghostQuery; private EntityQuery<GhostComponent> _ghostQuery;
private EntityQuery<PhysicsComponent> _physicsQuery; private EntityQuery<PhysicsComponent> _physicsQuery;
@@ -125,7 +129,9 @@ namespace Content.Server.Ghost
if (args.Handled) if (args.Handled)
return; return;
var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius); var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius).ToList();
// Shuffle the possible targets so we don't favor any particular entities
_random.Shuffle(entities);
var booCounter = 0; var booCounter = 0;
foreach (var ent in entities) foreach (var ent in entities)
@@ -139,6 +145,9 @@ namespace Content.Server.Ghost
break; break;
} }
if (booCounter == 0)
_popup.PopupEntity(Loc.GetString("ghost-component-boo-action-failed"), uid, uid);
args.Handled = true; args.Handled = true;
} }

View File

@@ -1,10 +1,12 @@
namespace Content.Server.Ghost; using Content.Shared.Roles;
namespace Content.Server.Ghost;
/// <summary> /// <summary>
/// This is used to mark Observers properly, as they get Minds /// This is used to mark Observers properly, as they get Minds
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
public sealed partial class ObserverRoleComponent : Component public sealed partial class ObserverRoleComponent : BaseMindRoleComponent
{ {
public string Name => Loc.GetString("observer-role-name"); public string Name => Loc.GetString("observer-role-name");
} }

View File

@@ -72,12 +72,16 @@ public sealed partial class GhostRoleComponent : Component
} }
} }
[DataField("allowSpeech")] /// <summary>
[ViewVariables(VVAccess.ReadWrite)] /// The mind roles that will be added to the mob's mind entity
/// </summary>
[DataField, Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // Don't make eye contact
public List<EntProtoId> MindRoles = new() { "MindRoleGhostRoleNeutral" };
[DataField]
public bool AllowSpeech { get; set; } = true; public bool AllowSpeech { get; set; } = true;
[DataField("allowMovement")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowMovement { get; set; } public bool AllowMovement { get; set; }
[ViewVariables(VVAccess.ReadOnly)] [ViewVariables(VVAccess.ReadOnly)]
@@ -107,3 +111,4 @@ public sealed partial class GhostRoleComponent : Component
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // also FIXME Friends [Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // also FIXME Friends
public ProtoId<JobPrototype>? JobProto = null; public ProtoId<JobPrototype>? JobProto = null;
} }

View File

@@ -9,39 +9,81 @@ namespace Content.Server.Ghost.Roles.Components;
[RegisterComponent, Access(typeof(ToggleableGhostRoleSystem))] [RegisterComponent, Access(typeof(ToggleableGhostRoleSystem))]
public sealed partial class ToggleableGhostRoleComponent : Component public sealed partial class ToggleableGhostRoleComponent : Component
{ {
[DataField("examineTextMindPresent")] /// <summary>
/// The text shown on the entity's Examine when it is controlled by a player
/// </summary>
[DataField]
public string ExamineTextMindPresent = string.Empty; public string ExamineTextMindPresent = string.Empty;
[DataField("examineTextMindSearching")] /// <summary>
/// The text shown on the entity's Examine when it is waiting for a controlling player
/// </summary>
[DataField]
public string ExamineTextMindSearching = string.Empty; public string ExamineTextMindSearching = string.Empty;
[DataField("examineTextNoMind")] /// <summary>
/// The text shown on the entity's Examine when it has no controlling player
/// </summary>
[DataField]
public string ExamineTextNoMind = string.Empty; public string ExamineTextNoMind = string.Empty;
[DataField("beginSearchingText")] /// <summary>
/// The popup text when the entity (PAI/positronic brain) it is activated to seek a controlling player
/// </summary>
[DataField]
public string BeginSearchingText = string.Empty; public string BeginSearchingText = string.Empty;
[DataField("roleName")] /// <summary>
/// The name shown on the Ghost Role list
/// </summary>
[DataField]
public string RoleName = string.Empty; public string RoleName = string.Empty;
[DataField("roleDescription")] /// <summary>
/// The description shown on the Ghost Role list
/// </summary>
[DataField]
public string RoleDescription = string.Empty; public string RoleDescription = string.Empty;
[DataField("roleRules")] /// <summary>
/// The introductory message shown when trying to take the ghost role/join the raffle
/// </summary>
[DataField]
public string RoleRules = string.Empty; public string RoleRules = string.Empty;
[DataField("wipeVerbText")] /// <summary>
/// A list of mind roles that will be added to the entity's mind
/// </summary>
[DataField]
public List<EntProtoId> MindRoles;
/// <summary>
/// The displayed name of the verb to wipe the controlling player
/// </summary>
[DataField]
public string WipeVerbText = string.Empty; public string WipeVerbText = string.Empty;
[DataField("wipeVerbPopup")] /// /// <summary>
/// The popup message when wiping the controlling player
/// </summary>
[DataField]
public string WipeVerbPopup = string.Empty; public string WipeVerbPopup = string.Empty;
[DataField("stopSearchVerbText")] /// <summary>
/// The displayed name of the verb to stop searching for a controlling player
/// </summary>
[DataField]
public string StopSearchVerbText = string.Empty; public string StopSearchVerbText = string.Empty;
[DataField("stopSearchVerbPopup")] /// /// <summary>
/// The popup message when stopping to search for a controlling player
/// </summary>
[DataField]
public string StopSearchVerbPopup = string.Empty; public string StopSearchVerbPopup = string.Empty;
/// /// <summary>
/// The prototype ID of the job that will be given to the controlling mind
/// </summary>
[DataField("job")] [DataField("job")]
public ProtoId<JobPrototype>? JobProto = null; public ProtoId<JobPrototype>? JobProto;
} }

View File

@@ -3,11 +3,13 @@
namespace Content.Server.Ghost.Roles; namespace Content.Server.Ghost.Roles;
/// <summary> /// <summary>
/// This is used for round end display of ghost roles. /// Added to mind role entities to tag that they are a ghostrole.
/// It may also be used to ensure some ghost roles count as antagonists in future. /// It also holds the name for the round end display
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
{ {
[DataField("name")] public string? Name; //TODO does anything still use this? It gets populated by GhostRolesystem but I don't see anything ever reading it
[DataField] public string? Name;
} }

View File

@@ -33,7 +33,6 @@ using Content.Server.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Collections; using Robust.Shared.Collections;
using Content.Shared.Ghost.Roles.Components; using Content.Shared.Ghost.Roles.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server.Ghost.Roles; namespace Content.Server.Ghost.Roles;
@@ -514,13 +513,13 @@ public sealed class GhostRoleSystem : EntitySystem
var newMind = _mindSystem.CreateMind(player.UserId, var newMind = _mindSystem.CreateMind(player.UserId,
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName); EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
_roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
_mindSystem.SetUserId(newMind, player.UserId); _mindSystem.SetUserId(newMind, player.UserId);
_mindSystem.TransferTo(newMind, mob); _mindSystem.TransferTo(newMind, mob);
_roleSystem.MindAddRoles(newMind.Owner, role.MindRoles, newMind.Comp);
if (_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
} }
/// <summary> /// <summary>

View File

@@ -51,10 +51,13 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
var ghostRole = EnsureComp<GhostRoleComponent>(uid); var ghostRole = EnsureComp<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(uid); EnsureComp<GhostTakeoverAvailableComponent>(uid);
//GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
ghostRole.RoleName = Loc.GetString(component.RoleName); ghostRole.RoleName = Loc.GetString(component.RoleName);
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription); ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
ghostRole.RoleRules = Loc.GetString(component.RoleRules); ghostRole.RoleRules = Loc.GetString(component.RoleRules);
ghostRole.JobProto = component.JobProto; ghostRole.JobProto = component.JobProto;
ghostRole.MindRoles = component.MindRoles;
} }
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args) private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)

View File

@@ -0,0 +1,51 @@
using Content.Server.Chat.Systems;
using Content.Server.Ghost.Components;
using Content.Shared.Random.Helpers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Ghost;
public sealed class SpookySpeakerSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpookySpeakerComponent, GhostBooEvent>(OnGhostBoo);
}
private void OnGhostBoo(Entity<SpookySpeakerComponent> entity, ref GhostBooEvent args)
{
// Only activate sometimes, so groups don't all trigger together
if (!_random.Prob(entity.Comp.SpeakChance))
return;
var curTime = _timing.CurTime;
// Enforce a delay between messages to prevent spam
if (curTime < entity.Comp.NextSpeakTime)
return;
if (!_proto.TryIndex(entity.Comp.MessageSet, out var messages))
return;
// Grab a random localized message from the set
var message = _random.Pick(messages);
// Chatcode moment: messages starting with '.' are considered radio messages unless prefixed with '>'
// So this is a stupid trick to make the "...Oooo"-style messages work.
message = '>' + message;
// Say the message
_chat.TrySendInGameICMessage(entity, message, InGameICChatType.Speak, hideChat: true);
// Set the delay for the next message
entity.Comp.NextSpeakTime = curTime + entity.Comp.Cooldown;
args.Handled = true;
}
}

View File

@@ -10,6 +10,7 @@ using Content.Shared.Holopad;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Labels.Components; using Content.Shared.Labels.Components;
using Content.Shared.Silicons.StationAi; using Content.Shared.Silicons.StationAi;
using Content.Shared.Speech;
using Content.Shared.Telephone; using Content.Shared.Telephone;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Content.Shared.Verbs; using Content.Shared.Verbs;
@@ -179,11 +180,15 @@ public sealed class HolopadSystem : SharedHolopadSystem
// AI broadcasting // AI broadcasting
if (TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld)) if (TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld))
{ {
// Link the AI to the holopad they are broadcasting from
LinkHolopadToUser(source, args.Actor);
if (!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore) || if (!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null || stationAiCore.Value.Comp.RemoteEntity == null ||
!TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad)) !TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
return; return;
// Execute the broadcast, but have it originate from the AI core
ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor); ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor);
// Switch the AI's perspective from free roaming to the target holopad // Switch the AI's perspective from free roaming to the target holopad
@@ -381,7 +386,10 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (TryComp<TelephoneComponent>(linkedHolopad, out var linkedHolopadTelephone) && linkedHolopadTelephone.Muted) if (TryComp<TelephoneComponent>(linkedHolopad, out var linkedHolopadTelephone) && linkedHolopadTelephone.Muted)
continue; continue;
foreach (var receiver in GetLinkedHolopads(linkedHolopad)) var receivingHolopads = GetLinkedHolopads(linkedHolopad);
var range = receivingHolopads.Count > 1 ? ChatTransmitRange.HideChat : ChatTransmitRange.GhostRangeLimit;
foreach (var receiver in receivingHolopads)
{ {
if (receiver.Comp.Hologram == null) if (receiver.Comp.Hologram == null)
continue; continue;
@@ -391,7 +399,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
var name = Loc.GetString("holopad-hologram-name", ("name", ent)); var name = Loc.GetString("holopad-hologram-name", ("name", ent));
// Force the emote, because if the user can do it, the hologram can too // Force the emote, because if the user can do it, the hologram can too
_chatSystem.TryEmoteWithChat(receiver.Comp.Hologram.Value, args.Emote, ChatTransmitRange.Normal, false, name, true, true); _chatSystem.TryEmoteWithChat(receiver.Comp.Hologram.Value, args.Emote, range, false, name, true, true);
} }
} }
} }
@@ -521,16 +529,23 @@ public sealed class HolopadSystem : SharedHolopadSystem
entity.Comp.HologramProtoId == null) entity.Comp.HologramProtoId == null)
return; return;
var uid = Spawn(entity.Comp.HologramProtoId, Transform(entity).Coordinates); var hologramUid = Spawn(entity.Comp.HologramProtoId, Transform(entity).Coordinates);
// Safeguard - spawned holograms must have this component // Safeguard - spawned holograms must have this component
if (!TryComp<HolopadHologramComponent>(uid, out var component)) if (!TryComp<HolopadHologramComponent>(hologramUid, out var holopadHologram))
{ {
Del(uid); Del(hologramUid);
return; return;
} }
entity.Comp.Hologram = new Entity<HolopadHologramComponent>(uid, component); entity.Comp.Hologram = new Entity<HolopadHologramComponent>(hologramUid, holopadHologram);
// Relay speech preferentially through the hologram
if (TryComp<SpeechComponent>(hologramUid, out var hologramSpeech) &&
TryComp<TelephoneComponent>(entity, out var entityTelephone))
{
_telephoneSystem.SetSpeakerForTelephone((entity, entityTelephone), (hologramUid, hologramSpeech));
}
} }
private void DeleteHologram(Entity<HolopadHologramComponent> hologram, Entity<HolopadComponent> attachedHolopad) private void DeleteHologram(Entity<HolopadHologramComponent> hologram, Entity<HolopadComponent> attachedHolopad)
@@ -608,10 +623,25 @@ public sealed class HolopadSystem : SharedHolopadSystem
DeleteHologram(entity.Comp.Hologram.Value, entity); DeleteHologram(entity.Comp.Hologram.Value, entity);
if (entity.Comp.User != null) if (entity.Comp.User != null)
UnlinkHolopadFromUser(entity, entity.Comp.User.Value); {
// Check if the associated holopad user is an AI
if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) &&
_stationAiSystem.TryGetStationAiCore((entity.Comp.User.Value, stationAiHeld), out var stationAiCore))
{
// Return the AI eye to free roaming
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, true);
if (TryComp<StationAiCoreComponent>(entity, out var stationAiCore)) // If the AI core is still broadcasting, end its calls
_stationAiSystem.SwitchRemoteEntityMode((entity.Owner, stationAiCore), true); if (entity.Owner != stationAiCore.Value.Owner &&
TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Value.Owner, stationAiCoreTelephone)))
{
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value.Owner, stationAiCoreTelephone));
}
}
UnlinkHolopadFromUser(entity, entity.Comp.User.Value);
}
Dirty(entity); Dirty(entity);
} }

View File

@@ -165,7 +165,7 @@ namespace Content.Server.Kitchen.EntitySystems
QueueDel(gib); QueueDel(gib);
} }
_audio.PlayEntity(component.SpikeSound, Filter.Pvs(uid), uid, true); _audio.PlayPvs(component.SpikeSound, uid);
} }
private bool TryGetPiece(EntityUid uid, EntityUid user, EntityUid used, private bool TryGetPiece(EntityUid uid, EntityUid user, EntityUid used,

View File

@@ -153,7 +153,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
else else
{ {
_battery.SetCharge(entity.Owner, battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency, battery); _battery.SetCharge(entity.Owner, battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency, battery);
if (battery.IsFullyCharged) if (_battery.IsFull(entity, battery))
{ {
if (TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver)) if (TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver))
{ {

View File

@@ -275,7 +275,7 @@ namespace Content.Server.Light.EntitySystems
if (time > light.LastThunk + ThunkDelay) if (time > light.LastThunk + ThunkDelay)
{ {
light.LastThunk = time; light.LastThunk = time;
_audio.PlayEntity(light.TurnOnSound, Filter.Pvs(uid), uid, true, AudioParams.Default.WithVolume(-10f)); _audio.PlayPvs(light.TurnOnSound, uid, light.TurnOnSound.Params.AddVolume(-10f));
} }
} }
else else

View File

@@ -106,6 +106,6 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid); _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full // repeat the doafter until battery is full
return !battery.IsFullyCharged; return !_battery.IsFull(ent, battery);
} }
} }

View File

@@ -235,7 +235,7 @@ public sealed class NukeSystem : EntitySystem
private void OnClearButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadClearMessage args) private void OnClearButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadClearMessage args)
{ {
_audio.PlayEntity(component.KeypadPressSound, Filter.Pvs(uid), uid, true); _audio.PlayPvs(component.KeypadPressSound, uid);
if (component.Status != NukeStatus.AWAIT_CODE) if (component.Status != NukeStatus.AWAIT_CODE)
return; return;
@@ -351,12 +351,12 @@ public sealed class NukeSystem : EntitySystem
{ {
component.Status = NukeStatus.AWAIT_ARM; component.Status = NukeStatus.AWAIT_ARM;
component.RemainingTime = component.Timer; component.RemainingTime = component.Timer;
_audio.PlayEntity(component.AccessGrantedSound, Filter.Pvs(uid), uid, true); _audio.PlayPvs(component.AccessGrantedSound, uid);
} }
else else
{ {
component.EnteredCode = ""; component.EnteredCode = "";
_audio.PlayEntity(component.AccessDeniedSound, Filter.Pvs(uid), uid, true); _audio.PlayPvs(component.AccessDeniedSound, uid);
} }
break; break;
@@ -425,7 +425,9 @@ public sealed class NukeSystem : EntitySystem
// Don't double-dip on the octave shifting // Don't double-dip on the octave shifting
component.LastPlayedKeypadSemitones = number == 0 ? component.LastPlayedKeypadSemitones : semitoneShift; component.LastPlayedKeypadSemitones = number == 0 ? component.LastPlayedKeypadSemitones : semitoneShift;
_audio.PlayEntity(component.KeypadPressSound, Filter.Pvs(uid), uid, true, AudioHelpers.ShiftSemitone(semitoneShift).WithVolume(-5f)); var opts = component.KeypadPressSound.Params;
opts = AudioHelpers.ShiftSemitone(opts, semitoneShift).AddVolume(-5f);
_audio.PlayPvs(component.KeypadPressSound, uid, opts);
} }
public string GenerateRandomNumberString(int length) public string GenerateRandomNumberString(int length)

View File

@@ -165,6 +165,9 @@ public sealed class DrinkSystem : SharedDrinkSystem
if (!HasComp<BodyComponent>(target)) if (!HasComp<BodyComponent>(target))
return false; return false;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
return false;
if (_openable.IsClosed(item, user)) if (_openable.IsClosed(item, user))
return true; return true;

View File

@@ -45,10 +45,7 @@ public sealed class HelpProgressConditionSystem : EntitySystem
return; return;
} }
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind) var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
.Select(pair => pair.Item1)
.ToHashSet();
var removeList = new List<EntityUid>();
// cant help anyone who is tasked with helping: // cant help anyone who is tasked with helping:
// 1. thats boring // 1. thats boring
@@ -56,19 +53,26 @@ public sealed class HelpProgressConditionSystem : EntitySystem
foreach (var traitor in traitors) foreach (var traitor in traitors)
{ {
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind // TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
if (!TryComp<MindComponent>(traitor, out var mind)) if (!TryComp<MindComponent>(traitor.Id, out var mind))
continue; continue;
foreach (var objective in mind.Objectives) foreach (var objective in mind.Objectives)
{ {
if (HasComp<HelpProgressConditionComponent>(objective)) if (HasComp<HelpProgressConditionComponent>(objective))
removeList.Add(traitor); traitors.RemoveWhere(x => x.Mind == mind);
} }
} }
foreach (var tot in removeList) // Can't have multiple objectives to help/save the same person
foreach (var objective in args.Mind.Objectives)
{ {
traitors.Remove(tot); if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(objective, out var help))
{
traitors.RemoveWhere(x => x.Id == help.Target);
}
}
} }
// no more helpable traitors // no more helpable traitors
@@ -78,7 +82,7 @@ public sealed class HelpProgressConditionSystem : EntitySystem
return; return;
} }
_target.SetTarget(uid, _random.Pick(traitors), target); _target.SetTarget(uid, _random.Pick(traitors).Id, target);
} }
private float GetProgress(EntityUid target) private float GetProgress(EntityUid target)

View File

@@ -44,7 +44,19 @@ public sealed class KeepAliveConditionSystem : EntitySystem
return; return;
} }
var traitors = Enumerable.ToList<(EntityUid Id, MindComponent Mind)>(_traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind)); var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
// Can't have multiple objectives to help/save the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(objective, out var help))
{
traitors.RemoveWhere(x => x.Id == help.Target);
}
}
}
// You are the first/only traitor. // You are the first/only traitor.
if (traitors.Count == 0) if (traitors.Count == 0)

View File

@@ -6,6 +6,7 @@ using Content.Shared.Mind;
using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Components;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Random; using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems; namespace Content.Server.Objectives.Systems;
@@ -52,8 +53,18 @@ public sealed class KillPersonConditionSystem : EntitySystem
if (target.Target != null) if (target.Target != null)
return; return;
// no other humans to kill
var allHumans = _mind.GetAliveHumans(args.MindId); var allHumans = _mind.GetAliveHumans(args.MindId);
// Can't have multiple objectives to kill the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(objective, out var kill))
{
allHumans.RemoveWhere(x => x.Owner == kill.Target);
}
}
// no other humans to kill
if (allHumans.Count == 0) if (allHumans.Count == 0)
{ {
args.Cancelled = true; args.Cancelled = true;

View File

@@ -29,6 +29,7 @@ public sealed class StealConditionSystem : EntitySystem
private EntityQuery<ContainerManagerComponent> _containerQuery; private EntityQuery<ContainerManagerComponent> _containerQuery;
private HashSet<Entity<TransformComponent>> _nearestEnts = new(); private HashSet<Entity<TransformComponent>> _nearestEnts = new();
private HashSet<EntityUid> _countedItems = new();
public override void Initialize() public override void Initialize()
{ {
@@ -104,6 +105,8 @@ public sealed class StealConditionSystem : EntitySystem
var containerStack = new Stack<ContainerManagerComponent>(); var containerStack = new Stack<ContainerManagerComponent>();
var count = 0; var count = 0;
_countedItems.Clear();
//check stealAreas //check stealAreas
if (condition.CheckStealAreas) if (condition.CheckStealAreas)
{ {
@@ -174,6 +177,9 @@ public sealed class StealConditionSystem : EntitySystem
private int CheckStealTarget(EntityUid entity, StealConditionComponent condition) private int CheckStealTarget(EntityUid entity, StealConditionComponent condition)
{ {
if (_countedItems.Contains(entity))
return 0;
// check if this is the target // check if this is the target
if (!TryComp<StealTargetComponent>(entity, out var target)) if (!TryComp<StealTargetComponent>(entity, out var target))
return 0; return 0;
@@ -196,6 +202,8 @@ public sealed class StealConditionSystem : EntitySystem
} }
} }
_countedItems.Add(entity);
return TryComp<StackComponent>(entity, out var stack) ? stack.Count : 1; return TryComp<StackComponent>(entity, out var stack) ? stack.Count : 1;
} }
} }

View File

@@ -1,3 +1,4 @@
using Content.Server.Access.Systems;
using Content.Server.AlertLevel; using Content.Server.AlertLevel;
using Content.Server.CartridgeLoader; using Content.Server.CartridgeLoader;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
@@ -36,6 +37,7 @@ namespace Content.Server.PDA
[Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!; [Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!; [Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly IdCardSystem _idCard = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -55,22 +57,28 @@ namespace Content.Server.PDA
SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification); SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed); SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed); SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed, after: new[] { typeof(IdCardSystem) });
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged); SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
} }
private void OnEntityRenamed(ref EntityRenamedEvent ev) private void OnEntityRenamed(ref EntityRenamedEvent ev)
{
if (HasComp<IdCardComponent>(ev.Uid))
return;
if (_idCard.TryFindIdCard(ev.Uid, out var idCard))
{ {
var query = EntityQueryEnumerator<PdaComponent>(); var query = EntityQueryEnumerator<PdaComponent>();
while (query.MoveNext(out var uid, out var comp)) while (query.MoveNext(out var uid, out var comp))
{ {
if (comp.PdaOwner == ev.Uid) if (comp.ContainedId == idCard)
{ {
SetOwner(uid, comp, ev.Uid, ev.NewName); SetOwner(uid, comp, ev.Uid, ev.NewName);
} }
} }
} }
}
protected override void OnComponentInit(EntityUid uid, PdaComponent pda, ComponentInit args) protected override void OnComponentInit(EntityUid uid, PdaComponent pda, ComponentInit args)
{ {
@@ -86,6 +94,9 @@ namespace Content.Server.PDA
protected override void OnItemInserted(EntityUid uid, PdaComponent pda, EntInsertedIntoContainerMessage args) protected override void OnItemInserted(EntityUid uid, PdaComponent pda, EntInsertedIntoContainerMessage args)
{ {
base.OnItemInserted(uid, pda, args); base.OnItemInserted(uid, pda, args);
var id = CompOrNull<IdCardComponent>(pda.ContainedId);
if (id != null)
pda.OwnerName = id.FullName;
UpdatePdaUi(uid, pda); UpdatePdaUi(uid, pda);
} }

View File

@@ -49,8 +49,8 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundEnd); SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundEnd);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached); SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached); SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleAdd); SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleRemove); SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<AFKEvent>(OnAFK); SubscribeLocalEvent<AFKEvent>(OnAFK);
SubscribeLocalEvent<UnAFKEvent>(OnUnAFK); SubscribeLocalEvent<UnAFKEvent>(OnUnAFK);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
@@ -121,13 +121,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
return GetTimedRoles(contentData.Mind.Value); return GetTimedRoles(contentData.Mind.Value);
} }
private void OnRoleRemove(RoleRemovedEvent ev) private void OnRoleEvent(RoleEvent ev)
{
if (_minds.TryGetSession(ev.Mind, out var session))
_tracking.QueueRefreshTrackers(session);
}
private void OnRoleAdd(RoleAddedEvent ev)
{ {
if (_minds.TryGetSession(ev.Mind, out var session)) if (_minds.TryGetSession(ev.Mind, out var session))
_tracking.QueueRefreshTrackers(session); _tracking.QueueRefreshTrackers(session);

View File

@@ -24,12 +24,6 @@ namespace Content.Server.Power.Components
[DataField("startingCharge")] [DataField("startingCharge")]
public float CurrentCharge; public float CurrentCharge;
/// <summary>
/// True if the battery is fully charged.
/// </summary>
[ViewVariables]
public bool IsFullyCharged => MathHelper.CloseToPercent(CurrentCharge, MaxCharge);
/// <summary> /// <summary>
/// The price per one joule. Default is 1 credit for 10kJ. /// The price per one joule. Default is 1 credit for 10kJ.
/// </summary> /// </summary>

View File

@@ -87,8 +87,8 @@ namespace Content.Server.Power.EntitySystems
var query = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>(); var query = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>();
while (query.MoveNext(out var uid, out var comp, out var batt)) while (query.MoveNext(out var uid, out var comp, out var batt))
{ {
if (!comp.AutoRecharge) continue; if (!comp.AutoRecharge || IsFull(uid, batt))
if (batt.IsFullyCharged) continue; continue;
if (comp.AutoRechargePause) if (comp.AutoRechargePause)
{ {
@@ -212,14 +212,14 @@ namespace Content.Server.Power.EntitySystems
} }
/// <summary> /// <summary>
/// Returns whether the battery is at least 99% charged, basically full. /// Returns whether the battery is full.
/// </summary> /// </summary>
public bool IsFull(EntityUid uid, BatteryComponent? battery = null) public bool IsFull(EntityUid uid, BatteryComponent? battery = null)
{ {
if (!Resolve(uid, ref battery)) if (!Resolve(uid, ref battery))
return false; return false;
return battery.CurrentCharge / battery.MaxCharge >= 0.99f; return battery.CurrentCharge >= battery.MaxCharge;
} }
} }
} }

View File

@@ -223,10 +223,10 @@ internal sealed class ChargerSystem : EntitySystem
if (container.ContainedEntities.Count == 0) if (container.ContainedEntities.Count == 0)
return CellChargerStatus.Empty; return CellChargerStatus.Empty;
if (!SearchForBattery(container.ContainedEntities[0], out _, out var heldBattery)) if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery))
return CellChargerStatus.Off; return CellChargerStatus.Off;
if (Math.Abs(heldBattery.MaxCharge - heldBattery.CurrentCharge) < 0.01) if (_battery.IsFull(heldEnt.Value, heldBattery))
return CellChargerStatus.Charged; return CellChargerStatus.Charged;
return CellChargerStatus.Charging; return CellChargerStatus.Charging;
@@ -247,12 +247,6 @@ internal sealed class ChargerSystem : EntitySystem
return; return;
_battery.SetCharge(batteryUid.Value, heldBattery.CurrentCharge + component.ChargeRate * frameTime, heldBattery); _battery.SetCharge(batteryUid.Value, heldBattery.CurrentCharge + component.ChargeRate * frameTime, heldBattery);
// Just so the sprite won't be set to 99.99999% visibility
if (heldBattery.MaxCharge - heldBattery.CurrentCharge < 0.01)
{
_battery.SetCharge(batteryUid.Value, heldBattery.MaxCharge, heldBattery);
}
UpdateStatus(uid, component); UpdateStatus(uid, component);
} }

View File

@@ -29,7 +29,7 @@ public sealed partial class ChemicalFuelGeneratorAdapterComponent : Component
/// <summary> /// <summary>
/// The solution on the <see cref="SolutionContainerManagerComponent"/> to use. /// The solution on the <see cref="SolutionContainerManagerComponent"/> to use.
/// </summary> /// </summary>
[DataField("solutionRef")] [ViewVariables]
public Entity<SolutionComponent>? Solution = null; public Entity<SolutionComponent>? Solution = null;
/// <summary> /// <summary>

View File

@@ -117,7 +117,7 @@ public sealed class PortableGeneratorSystem : SharedPortableGeneratorSystem
var clogged = _generator.GetIsClogged(uid); var clogged = _generator.GetIsClogged(uid);
var sound = empty ? component.StartSoundEmpty : component.StartSound; var sound = empty ? component.StartSoundEmpty : component.StartSound;
_audio.PlayEntity(sound, Filter.Pvs(uid), uid, true); _audio.PlayPvs(sound, uid);
if (!clogged && !empty && _random.Prob(component.StartChance)) if (!clogged && !empty && _random.Prob(component.StartChance))
{ {

View File

@@ -19,10 +19,25 @@ public sealed class JobSystem : SharedJobSystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<MindComponent, MindRoleAddedEvent>(MindOnDoGreeting); SubscribeLocalEvent<RoleAddedEvent>(OnRoleAddedEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleRemovedEvent);
} }
private void MindOnDoGreeting(EntityUid mindId, MindComponent component, ref MindRoleAddedEvent args) private void OnRoleAddedEvent(RoleAddedEvent args)
{
MindOnDoGreeting(args.MindId, args.Mind, args);
if (args.RoleTypeUpdate)
_roles.RoleUpdateMessage(args.Mind);
}
private void OnRoleRemovedEvent(RoleRemovedEvent args)
{
if (args.RoleTypeUpdate)
_roles.RoleUpdateMessage(args.Mind);
}
private void MindOnDoGreeting(EntityUid mindId, MindComponent component, RoleAddedEvent args)
{ {
if (args.Silent) if (args.Silent)
return; return;

View File

@@ -1,11 +1,13 @@
namespace Content.Server.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary> /// <summary>
/// Adds a briefing to the character info menu, does nothing else. /// Adds a briefing to the character info menu, does nothing else.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
public sealed partial class RoleBriefingComponent : Component public sealed partial class RoleBriefingComponent : BaseMindRoleComponent
{ {
[DataField("briefing"), ViewVariables(VVAccess.ReadWrite)] [DataField]
public string Briefing; public string Briefing;
} }

View File

@@ -1,10 +1,16 @@
using Content.Server.Chat.Managers;
using Content.Shared.Chat;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Roles; namespace Content.Server.Roles;
public sealed class RoleSystem : SharedRoleSystem public sealed class RoleSystem : SharedRoleSystem
{ {
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public string? MindGetBriefing(EntityUid? mindId) public string? MindGetBriefing(EntityUid? mindId)
{ {
if (mindId == null) if (mindId == null)
@@ -37,6 +43,32 @@ public sealed class RoleSystem : SharedRoleSystem
return ev.Briefing; return ev.Briefing;
} }
public void RoleUpdateMessage(MindComponent mind)
{
if (mind.Session is null)
return;
if (!_proto.TryIndex(mind.RoleType, out var proto))
return;
var roleText = Loc.GetString(proto.Name);
var color = proto.Color;
var session = mind.Session;
//TODO add audio? Would need to be optional so it does not play on role changes that already come with their own audio
// _audio.PlayGlobal(Sound, session);
var message = Loc.GetString("role-type-update-message", ("color", color), ("role", roleText));
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
session.Channel);
}
} }
/// <summary> /// <summary>

View File

@@ -1,5 +1,7 @@
using Content.Shared.Containers.ItemSlots; using Content.Server.Roles;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Silicons.Borgs.Components; using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@@ -8,6 +10,9 @@ namespace Content.Server.Silicons.Borgs;
/// <inheritdoc/> /// <inheritdoc/>
public sealed partial class BorgSystem public sealed partial class BorgSystem
{ {
[Dependency] private readonly SharedRoleSystem _roles = default!;
public void InitializeMMI() public void InitializeMMI()
{ {
SubscribeLocalEvent<MMIComponent, ComponentInit>(OnMMIInit); SubscribeLocalEvent<MMIComponent, ComponentInit>(OnMMIInit);
@@ -41,8 +46,13 @@ public sealed partial class BorgSystem
Dirty(uid, component); Dirty(uid, component);
if (_mind.TryGetMind(ent, out var mindId, out var mind)) if (_mind.TryGetMind(ent, out var mindId, out var mind))
{
_mind.TransferTo(mindId, uid, true, mind: mind); _mind.TransferTo(mindId, uid, true, mind: mind);
if (!_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
_roles.MindAddRole(mindId, "MindRoleSiliconBrain", silent: true);
}
_appearance.SetData(uid, MMIVisuals.BrainPresent, true); _appearance.SetData(uid, MMIVisuals.BrainPresent, true);
} }
@@ -75,7 +85,12 @@ public sealed partial class BorgSystem
RemComp(uid, component); RemComp(uid, component);
if (_mind.TryGetMind(linked, out var mindId, out var mind)) if (_mind.TryGetMind(linked, out var mindId, out var mind))
{
if (_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
_roles.MindRemoveRole<SiliconBrainRoleComponent>(mindId);
_mind.TransferTo(mindId, uid, true, mind: mind); _mind.TransferTo(mindId, uid, true, mind: mind);
}
_appearance.SetData(linked, MMIVisuals.BrainPresent, false); _appearance.SetData(linked, MMIVisuals.BrainPresent, false);
} }

View File

@@ -5,7 +5,6 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.EntitySystems; using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems; using Content.Server.Hands.Systems;
using Content.Server.PowerCell; using Content.Server.PowerCell;
using Content.Shared.Access.Systems;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;

View File

@@ -193,7 +193,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
private void EnsureSubvertedSiliconRole(EntityUid mindId) private void EnsureSubvertedSiliconRole(EntityUid mindId)
{ {
if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId)) if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
_roles.MindAddRole(mindId, "MindRoleSubvertedSilicon"); _roles.MindAddRole(mindId, "MindRoleSubvertedSilicon", silent: true);
} }
private void RemoveSubvertedSiliconRole(EntityUid mindId) private void RemoveSubvertedSiliconRole(EntityUid mindId)

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Speech.Components;
/// <summary>
/// Makes this entity speak like a sheep or a goat in all chat messages it sends.
/// </summary>
[RegisterComponent]
public sealed partial class BleatingAccentComponent : Component { }

View File

@@ -0,0 +1,28 @@
using System.Text.RegularExpressions;
using Content.Server.Speech.Components;
namespace Content.Server.Speech.EntitySystems;
public sealed partial class BleatingAccentSystem : EntitySystem
{
private static readonly Regex BleatRegex = new("([mbdlpwhrkcnytfo])([aiu])", RegexOptions.IgnoreCase);
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BleatingAccentComponent, AccentGetEvent>(OnAccentGet);
}
private void OnAccentGet(Entity<BleatingAccentComponent> entity, ref AccentGetEvent args)
{
args.Message = Accentuate(args.Message);
}
public static string Accentuate(string message)
{
// Repeats the vowel in certain consonant-vowel pairs
// So you taaaalk liiiike thiiiis
return BleatRegex.Replace(message, "$1$2$2$2$2");
}
}

View File

@@ -94,11 +94,11 @@ public sealed class GreytideVirusRule : StationEventSystem<GreytideVirusRuleComp
continue; continue;
// use the access reader from the door electronics if they exist // use the access reader from the door electronics if they exist
if (!_access.GetMainAccessReader(airlockUid, out var accessComp)) if (!_access.GetMainAccessReader(airlockUid, out var accessEnt))
continue; continue;
// check access // check access
if (!_access.AreAccessTagsAllowed(accessIds, accessComp) || _access.AreAccessTagsAllowed(virusComp.Blacklist, accessComp)) if (!_access.AreAccessTagsAllowed(accessIds, accessEnt.Value.Comp) || _access.AreAccessTagsAllowed(virusComp.Blacklist, accessEnt.Value.Comp))
continue; continue;
// open and bolt airlocks // open and bolt airlocks

View File

@@ -16,6 +16,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Server.Shuttles.Components;
namespace Content.Server.Storage.EntitySystems; namespace Content.Server.Storage.EntitySystems;
@@ -47,6 +48,8 @@ public sealed class BluespaceLockerSystem : EntitySystem
if (component.BehaviorProperties.BluespaceEffectOnInit) if (component.BehaviorProperties.BluespaceEffectOnInit)
BluespaceEffect(uid, component, component, true); BluespaceEffect(uid, component, component, true);
EnsureComp<ArrivalsBlacklistComponent>(uid); // To stop people getting to arrivals terminal
} }
public void BluespaceEffect(EntityUid effectTargetUid, BluespaceLockerComponent effectSourceComponent, BluespaceLockerComponent? effectTargetComponent, bool bypassLimit = false) public void BluespaceEffect(EntityUid effectTargetUid, BluespaceLockerComponent effectSourceComponent, BluespaceLockerComponent? effectTargetComponent, bool bypassLimit = false)

View File

@@ -7,8 +7,11 @@ using Content.Server.Speech;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Labels.Components;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Speech; using Content.Shared.Speech;
using Content.Shared.Telephone; using Content.Shared.Telephone;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -19,8 +22,6 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Replays; using Robust.Shared.Replays;
using System.Linq; using System.Linq;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Silicons.Borgs.Components;
namespace Content.Server.Telephone; namespace Content.Server.Telephone;
@@ -104,12 +105,17 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource)); var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource));
RaiseLocalEvent(args.MessageSource, nameEv); RaiseLocalEvent(args.MessageSource, nameEv);
var name = Loc.GetString("speech-name-relay", // Determine if speech should be relayed via the telephone itself or a designated speaker
("speaker", Name(entity)), var speaker = entity.Comp.Speaker != null ? entity.Comp.Speaker.Value.Owner : entity.Owner;
("originalName", nameEv.VoiceName));
var name = Loc.GetString("chat-telephone-name-relay",
("originalName", nameEv.VoiceName),
("speaker", Name(speaker)));
var range = args.TelephoneSource.Comp.LinkedTelephones.Count > 1 ? ChatTransmitRange.HideChat : ChatTransmitRange.GhostRangeLimit;
var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper; var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper;
_chat.TrySendInGameICMessage(entity, args.Message, volume, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false);
_chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false);
} }
#endregion #endregion
@@ -151,7 +157,7 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
break; break;
// Try to hang up if their has been no recent in-call activity // Try to hang up if there has been no recent in-call activity
case TelephoneState.InCall: case TelephoneState.InCall:
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout)) if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout))
EndTelephoneCalls(entity); EndTelephoneCalls(entity);
@@ -214,7 +220,15 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
source.Comp.LinkedTelephones.Add(receiver); source.Comp.LinkedTelephones.Add(receiver);
source.Comp.Muted = options?.MuteSource == true; source.Comp.Muted = options?.MuteSource == true;
receiver.Comp.LastCallerId = GetNameAndJobOfCallingEntity(user); // This will be networked when the state changes var callerInfo = GetNameAndJobOfCallingEntity(user);
// Base the name of the device on its label
string? deviceName = null;
if (TryComp<LabelComponent>(source, out var label))
deviceName = label.CurrentLabel;
receiver.Comp.LastCallerId = (callerInfo.Item1, callerInfo.Item2, deviceName); // This will be networked when the state changes
receiver.Comp.LinkedTelephones.Add(source); receiver.Comp.LinkedTelephones.Add(source);
receiver.Comp.Muted = options?.MuteReceiver == true; receiver.Comp.Muted = options?.MuteReceiver == true;
@@ -401,6 +415,11 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
} }
} }
public void SetSpeakerForTelephone(Entity<TelephoneComponent> entity, Entity<SpeechComponent>? speaker)
{
entity.Comp.Speaker = speaker;
}
private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid) private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid)
{ {
string? presumedName = null; string? presumedName = null;

View File

@@ -439,4 +439,9 @@ public enum LogType
/// A ghost warped to an entity through the ghost warp menu. /// A ghost warped to an entity through the ghost warp menu.
/// </summary> /// </summary>
GhostWarp = 95, GhostWarp = 95,
/// <summary>
/// A player interacted with a PDA or its cartridge component
/// </summary>
PdaInteract = 96,
} }

View File

@@ -113,25 +113,25 @@ public sealed class AccessReaderSystem : EntitySystem
return false; return false;
} }
public bool GetMainAccessReader(EntityUid uid, [NotNullWhen(true)] out AccessReaderComponent? component) public bool GetMainAccessReader(EntityUid uid, [NotNullWhen(true)] out Entity<AccessReaderComponent>? ent)
{ {
component = null; ent = null;
if (!TryComp(uid, out AccessReaderComponent? accessReader)) if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
return false; return false;
component = accessReader; ent = (uid, accessReader);
if (component.ContainerAccessProvider == null) if (ent.Value.Comp.ContainerAccessProvider == null)
return true; return true;
if (!_containerSystem.TryGetContainer(uid, component.ContainerAccessProvider, out var container)) if (!_containerSystem.TryGetContainer(uid, ent.Value.Comp.ContainerAccessProvider, out var container))
return true; return true;
foreach (var entity in container.ContainedEntities) foreach (var entity in container.ContainedEntities)
{ {
if (TryComp(entity, out AccessReaderComponent? containedReader)) if (TryComp<AccessReaderComponent>(entity, out var containedReader))
{ {
component = containedReader; ent = (entity, containedReader);
return true; return true;
} }
} }

View File

@@ -33,7 +33,8 @@ public abstract class SharedIdCardSystem : EntitySystem
// When a player gets renamed their id card is renamed as well to match. // When a player gets renamed their id card is renamed as well to match.
// Unfortunately since TryFindIdCard will succeed if the entity is also a card this means that the card will // Unfortunately since TryFindIdCard will succeed if the entity is also a card this means that the card will
// keep renaming itself unless we return early. // keep renaming itself unless we return early.
if (HasComp<IdCardComponent>(ev.Uid)) // We also do not include the PDA itself being renamed, as that triggers the same event (e.g. for chameleon PDAs).
if (HasComp<IdCardComponent>(ev.Uid) || HasComp<PdaComponent>(ev.Uid))
return; return;
if (TryFindIdCard(ev.Uid, out var idCard)) if (TryFindIdCard(ev.Uid, out var idCard))

View File

@@ -1,3 +1,4 @@
using Content.Shared.Mind;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -10,6 +11,7 @@ public sealed record PlayerInfo(
string IdentityName, string IdentityName,
string StartingJob, string StartingJob,
bool Antag, bool Antag,
RoleTypePrototype RoleProto,
NetEntity? NetEntity, NetEntity? NetEntity,
NetUserId SessionId, NetUserId SessionId,
bool Connected, bool Connected,

View File

@@ -28,7 +28,7 @@ public sealed partial class UdderComponent : Component
/// <summary> /// <summary>
/// The solution to add reagent to. /// The solution to add reagent to.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)] [ViewVariables(VVAccess.ReadOnly)]
public Entity<SolutionComponent>? Solution = null; public Entity<SolutionComponent>? Solution = null;
/// <summary> /// <summary>

View File

@@ -28,7 +28,7 @@ public sealed partial class WoolyComponent : Component
/// <summary> /// <summary>
/// The solution to add reagent to. /// The solution to add reagent to.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)] [ViewVariables(VVAccess.ReadOnly)]
public Entity<SolutionComponent>? Solution; public Entity<SolutionComponent>? Solution;
/// <summary> /// <summary>

Some files were not shown because too many files have changed in this diff Show More