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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,21 @@
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration;
internal sealed class AdminNameOverlay : Overlay
{
[Dependency] private readonly IConfigurationManager _config = default!;
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
@@ -18,8 +23,16 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IUserInterfaceManager _userInterfaceManager;
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)
{
IoCManager.InjectDependencies(this);
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
@@ -35,6 +48,9 @@ internal sealed class AdminNameOverlay : Overlay
{
var viewport = args.WorldAABB;
//TODO make this adjustable via GUI
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
foreach (var playerInfo in _system.PlayerList)
{
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
@@ -64,12 +80,20 @@ internal sealed class AdminNameOverlay : Overlay
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
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);
}
}

View File

@@ -197,6 +197,7 @@ public sealed partial class PlayerTab : Control
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
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),
_ => 1
};

View File

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

View File

@@ -23,6 +23,8 @@ public sealed partial class PlayerTabEntry : PanelContainer
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
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;
OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;

View File

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

View File

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

View File

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

View File

@@ -83,10 +83,10 @@ public sealed partial class PumpControl : BoxContainer
PumpDataChanged?.Invoke(_address, _data);
};
_copySettings.OnPressed += _ =>
{
PumpDataCopied?.Invoke(_data);
};
_copySettings.OnPressed += _ =>
{
PumpDataCopied?.Invoke(_data);
};
}
public void ChangeData(GasVentPumpData data)

View File

@@ -72,10 +72,10 @@ public sealed partial class ScrubberControl : BoxContainer
ScrubberDataChanged?.Invoke(_address, _data);
};
_copySettings.OnPressed += _ =>
{
ScrubberDataCopied?.Invoke(_data);
};
_copySettings.OnPressed += _ =>
{
ScrubberDataCopied?.Invoke(_data);
};
foreach (var value in Enum.GetValues<Gas>())
{

View File

@@ -3,6 +3,9 @@
<CollapsibleHeading Name="SensorAddress" />
<CollapsibleBody Margin="20 2 2 2">
<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">
<RichTextLabel Name="AlarmStateLabel" />
<RichTextLabel Name="PressureLabel" />

View File

@@ -12,12 +12,14 @@ namespace Content.Client.Atmos.Monitor.UI.Widgets;
public sealed partial class SensorInfo : BoxContainer
{
public Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? OnThresholdUpdate;
public event Action<AtmosSensorData>? SensorDataCopied;
private string _address;
private ThresholdControl _pressureThreshold;
private ThresholdControl _temperatureThreshold;
private Dictionary<Gas, ThresholdControl> _gasThresholds = new();
private Dictionary<Gas, RichTextLabel> _gasLabels = new();
private Button _copySettings => CCopySettings;
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.ThresholdDataChanged += (type, alarmThreshold, arg3) =>
{
OnThresholdUpdate!(_address, type, alarmThreshold, arg3);
OnThresholdUpdate?.Invoke(_address, type, alarmThreshold, arg3);
};
_gasThresholds.Add(gas, gasThresholdControl);
@@ -72,12 +74,17 @@ public sealed partial class SensorInfo : BoxContainer
_pressureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
{
OnThresholdUpdate!(_address, type, threshold, arg3);
OnThresholdUpdate?.Invoke(_address, 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.Cargo.Systems;
@@ -10,9 +11,9 @@ public sealed class ClientPriceGunSystem : SharedPriceGunSystem
{
[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;
// 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 Content.Client.PDA;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory;
@@ -51,6 +52,15 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
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>

View File

@@ -1,15 +1,21 @@
using Content.Client.Clothing.Systems;
using Content.Client.Clothing.Systems;
using Content.Shared.Clothing.Components;
using Content.Shared.Tag;
using Content.Shared.Prototypes;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Clothing.UI;
[UsedImplicitly]
public sealed class ChameleonBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly ChameleonClothingSystem _chameleon;
private readonly TagSystem _tag;
[ViewVariables]
private ChameleonMenu? _menu;
@@ -17,6 +23,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
public ChameleonBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_chameleon = EntMan.System<ChameleonClothingSystem>();
_tag = EntMan.System<TagSystem>();
}
protected override void Open()
@@ -34,7 +41,24 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
return;
var targets = _chameleon.GetValidTargets(st.Slot);
_menu?.UpdateState(targets, st.SelectedId);
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);
}
}
private void OnIdSelected(string selectedId)

View File

@@ -27,7 +27,11 @@
<!-- Header text -->
<BoxContainer MinHeight="60" Orientation="Vertical" VerticalAlignment="Center">
<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>
<!-- Controls (the answer call button is absent when the phone is not ringing) -->
@@ -70,16 +74,23 @@
</PanelContainer>
<PanelContainer Name="HolopadContactListPanel">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256">
<BoxContainer Orientation="Vertical">
<!-- If there is no data yet, this will be displayed -->
<BoxContainer Name="FetchingAvailableHolopadsContainer" HorizontalAlignment="Center" HorizontalExpand="True" VerticalExpand="True" ReservesSpace="False">
<Label Text="{Loc 'holopad-window-fetching-contacts-list'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</BoxContainer>
<!-- Contact filter -->
<LineEdit Name="SearchLineEdit" HorizontalExpand="True" Margin="4, 4, 4, 0"
PlaceHolder="{Loc holopad-window-filter-line-placeholder}" />
<!-- Container for the contacts -->
<BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/>
</ScrollContainer>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256">
<!-- If there is no data yet, this will be displayed -->
<BoxContainer Name="FetchingAvailableHolopadsContainer" HorizontalAlignment="Center" HorizontalExpand="True" VerticalExpand="True" ReservesSpace="False">
<Label Text="{Loc 'holopad-window-fetching-contacts-list'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</BoxContainer>
<!-- Container for the contacts -->
<BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/>
</ScrollContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -171,8 +171,10 @@ public sealed partial class HolopadWindow : FancyWindow
// Caller ID text
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));
HolopadIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(holoapdId));
LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
// Sort holopads alphabetically
@@ -236,10 +238,13 @@ public sealed partial class HolopadWindow : FancyWindow
// Make / update required children
foreach (var child in ContactsList.Children)
{
if (child is not HolopadContactButton)
if (child is not HolopadContactButton contactButton)
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);
}
@@ -290,7 +295,7 @@ public sealed partial class HolopadWindow : FancyWindow
FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
CallerIdText.Visible = (_currentState == TelephoneState.Ringing);
CallerIdContainer.Visible = (_currentState == TelephoneState.Ringing);
AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
}
@@ -316,6 +321,7 @@ public sealed partial class HolopadWindow : FancyWindow
HorizontalExpand = true;
SetHeight = 32;
Margin = new Thickness(0f, 1f, 0f, 1f);
ReservesSpace = false;
}
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.UserInterface.Systems.Chat;
using Content.Client.Voting;
using Content.Shared.CCVar;
using Robust.Client;
using Robust.Client.Console;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.Lobby
{
public sealed class LobbyState : Robust.Client.State.State
{
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
@@ -49,7 +51,17 @@ namespace Content.Client.Lobby
_voteManager.SetPopupContainer(Lobby.VoteContainer);
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();
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;

View File

@@ -36,17 +36,18 @@ public sealed partial class LoadoutContainer : BoxContainer
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)
{
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
if (ent == null)
return;
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
TooltipSupplier = _ => spriteTooltip;
}
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
TooltipSupplier = _ => spriteTooltip;
}
}

View File

@@ -62,14 +62,12 @@
<Control Access="Public" Visible="False" Name="CharacterSetupState" VerticalExpand="True" />
</BoxContainer>
<!-- Right Panel -->
<PanelContainer Name="RightSide" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True"
<PanelContainer Name="RightSide" Access="Public" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True"
VerticalAlignment="Stretch">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<!-- Top row -->
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public"
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"
HorizontalExpand="True" HorizontalAlignment="Center" />
</BoxContainer>

View File

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

View File

@@ -1,48 +1,8 @@
using Content.Shared.PDA;
using Content.Shared.Light;
using Robust.Client.GameObjects;
namespace Content.Client.PDA;
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()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void UpdateState(ConsoleUIState state)

View File

@@ -104,7 +104,7 @@ public sealed class TippyUIController : UIController
? -WaddleRotation
: WaddleRotation;
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step))
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step) && step.FootstepSoundCollection != null)
{
var audioParams = step.FootstepSoundCollection.Params
.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.Objectives.Controls;
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 Robust.Client.GameObjects;
using Robust.Client.Player;
@@ -15,6 +17,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using static Content.Client.CharacterInfo.CharacterInfoSystem;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -24,10 +27,25 @@ namespace Content.Client.UserInterface.Systems.Character;
[UsedImplicitly]
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 IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = 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 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;
_window.SpriteView.SetEntity(entity);
UpdateRoleType();
_window.NameLabel.Text = entityName;
_window.SubText.Text = job;
_window.Objectives.RemoveAllChildren();
@@ -173,6 +194,37 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
_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)
{
CloseWindow();

View File

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

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Shared;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
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
/// 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"
/// 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>
[Test]
public async Task SpawnAndDeleteEntityCountTest()
{
var settings = new PoolSettings { Connected = true, Dirty = true };
await using var pair = await PoolManager.GetServerClient(settings);
var mapManager = pair.Server.ResolveDependency<IMapManager>();
var mapSys = pair.Server.System<SharedMapSystem>();
var server = pair.Server;
var client = pair.Client;
@@ -261,6 +265,9 @@ namespace Content.IntegrationTests.Tests
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)
{
// TODO fix ninja
@@ -268,8 +275,8 @@ namespace Content.IntegrationTests.Tests
if (protoId == "MobHumanSpaceNinja")
continue;
var count = server.EntMan.EntityCount;
var clientCount = client.EntMan.EntityCount;
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords));
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 (!server.EntMan.EntityExists(uid))
{
if (server.EntMan.EntityCount != count)
if (Count(server.EntMan) != count)
{
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" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
continue;
}
// 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");
}
if (client.EntMan.EntityCount <= clientCount)
if (Count(client.EntMan) <= clientCount)
{
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}");
}
@@ -308,15 +315,15 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3);
// 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");
}
if (client.EntMan.EntityCount != clientCount)
if (Count(client.EntMan) != clientCount)
{
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}.");
}
}

View File

@@ -35,7 +35,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{
playerUid = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
#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
Assert.That(entManager.HasComponent<AlertsComponent>(playerUid));
#pragma warning restore NUnit2045

View File

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

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<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;
targetLabelColor = Color.White;
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderComponent))
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderEnt))
return;
var currentAccessHashsets = accessReaderComponent.AccessLists;
var currentAccessHashsets = accessReaderEnt.Value.Comp.AccessLists;
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray();
}
@@ -210,10 +210,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
return;
}
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReader))
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReaderEnt))
return;
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists);
var oldTags = ConvertAccessHashSetsToList(accessReaderEnt.Value.Comp.AccessLists);
var privilegedId = component.PrivilegedIdSlot.Item;
if (oldTags.SequenceEqual(newAccessList))
@@ -242,10 +242,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
_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);
Dirty(component.TargetAccessReaderId, accessReader);
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
Dirty(accessReaderEnt.Value);
}
/// <summary>

View File

@@ -33,16 +33,16 @@ public sealed class AdminWhoCommand : IConsoleCommand
var first = true;
foreach (var admin in adminMgr.ActiveAdmins)
{
if (!first)
sb.Append('\n');
first = false;
var adminData = adminMgr.GetAdminData(admin)!;
DebugTools.AssertNotNull(adminData);
if (adminData.Stealth && !seeStealth)
continue;
if (!first)
sb.Append('\n');
first = false;
sb.Append(admin.Name);
if (adminData.Title is { } 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)
{
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)
|| _promotedPlayers.Contains(session.UserId)

View File

@@ -17,7 +17,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.PDA;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Popups;
using Content.Shared.Roles;
@@ -32,6 +31,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Systems;
@@ -48,6 +48,7 @@ public sealed class AdminSystem : EntitySystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -165,7 +166,8 @@ public sealed class AdminSystem : EntitySystem
private void OnRoleEvent(RoleEvent ev)
{
var session = _minds.GetSession(ev.Mind);
if (!ev.Antagonist || session == null)
if (!ev.RoleTypeUpdate || session == null)
return;
UpdatePlayerList(session);
@@ -239,9 +241,16 @@ public sealed class AdminSystem : EntitySystem
}
var antag = false;
RoleTypePrototype roleType = new();
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);
startingRole = _jobs.MindTryGetJobName(mindId);
}
@@ -255,7 +264,7 @@ public sealed class AdminSystem : EntitySystem
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);
}

View File

@@ -97,6 +97,6 @@ public sealed partial class ReagentProducerAnomalyComponent : Component
/// <summary>
/// Solution where the substance is generated
/// </summary>
[DataField("solutionRef")]
[ViewVariables]
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.
/// </summary>
[DataField]
public List<ProtoId<EntityPrototype>>? MindRoles;
public List<EntProtoId>? MindRoles;
/// <summary>
/// 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);
}
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>
/// Sync this air alarm's mode with the rest of the network.
/// </summary>
@@ -341,6 +354,13 @@ public sealed class AirAlarmSystem : EntitySystem
SetData(uid, addr, args.Data);
}
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
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
public const string AtmosMonitorSetAllThresholdsCmd = "atmos_monitor_set_all_thresholds";
// Packet 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 AtmosMonitorThresholdGasType = "atmos_monitor_threshold_gas";
@@ -138,7 +139,12 @@ public sealed class AtmosMonitorSystem : EntitySystem
args.Data.TryGetValue(AtmosMonitorThresholdGasType, out Gas? gas);
SetThreshold(uid, thresholdType.Value, thresholdData, gas);
}
break;
case AtmosMonitorSetAllThresholdsCmd:
if (args.Data.TryGetValue(AtmosMonitorAllThresholdData, out AtmosSensorData? allThresholdData))
{
SetAllThresholds(uid, allThresholdData);
}
break;
case AtmosDeviceNetworkSystem.SyncData:
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>
/// The solution that gases are condensed into.
/// </summary>
[DataField]
[ViewVariables]
public Entity<SolutionComponent>? Solution = null;
/// <summary>

View File

@@ -157,22 +157,22 @@ namespace Content.Server.Body.Components
/// <summary>
/// Internal solution for blood storage
/// </summary>
[DataField]
public Entity<SolutionComponent>? BloodSolution = null;
[ViewVariables]
public Entity<SolutionComponent>? BloodSolution;
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[DataField]
public Entity<SolutionComponent>? ChemicalSolution = null;
[ViewVariables]
public Entity<SolutionComponent>? ChemicalSolution;
/// <summary>
/// Temporary blood solution.
/// 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.
/// </summary>
[DataField]
public Entity<SolutionComponent>? TemporarySolution = null;
[ViewVariables]
public Entity<SolutionComponent>? TemporarySolution;
/// <summary>
/// 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>
/// The solution on this entity that these lungs act on.
/// </summary>
[DataField]
[ViewVariables]
public Entity<SolutionComponent>? Solution = null;
/// <summary>

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
using Content.Server.Popups;
using Content.Shared.Cargo.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Timing;
using Content.Shared.Cargo.Systems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Cargo.Systems;
@@ -11,12 +13,12 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
[Dependency] private readonly PricingSystem _pricingSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = 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;
// Check if we're scanning a bounty crate
if (_bountySystem.IsBountyComplete(target, out _))
{
@@ -25,10 +27,15 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
else // Otherwise appraise the price
{
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;
}
}

View File

@@ -1,11 +1,14 @@
using Content.Server.Administration.Logs;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Database;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class NotekeeperCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
public override void Initialize()
{
@@ -36,16 +39,19 @@ public sealed class NotekeeperCartridgeSystem : EntitySystem
if (message.Action == NotekeeperUiAction.Add)
{
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
{
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);
}
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, NotekeeperCartridgeComponent? component)
{
if (!Resolve(uid, ref component))

View File

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

View File

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

View File

@@ -512,7 +512,8 @@ public sealed partial class ExplosionSystem
List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
ExplosionPrototype type)
{
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef)
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef
|| tileDef.Indestructible)
return;
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
// 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
//
// 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 filter = Filter.Pvs(pos).AddInRange(pos, audioRange);
var sound = iterationIntensity.Count < queued.Proto.SmallSoundIterationThreshold

View File

@@ -222,8 +222,6 @@ namespace Content.Server.GameTicking
_mind.SetUserId(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_playTimeTrackings.PlayerRolesChanged(player);
@@ -233,6 +231,9 @@ namespace Content.Server.GameTicking
_mind.TransferTo(newMind, mob);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
if (lateJoin && !silent)
{
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.Movement.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
@@ -33,6 +34,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Ghost
@@ -61,6 +63,8 @@ namespace Content.Server.Ghost
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly GameTicker _gameTicker = 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<PhysicsComponent> _physicsQuery;
@@ -125,7 +129,9 @@ namespace Content.Server.Ghost
if (args.Handled)
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;
foreach (var ent in entities)
@@ -139,6 +145,9 @@ namespace Content.Server.Ghost
break;
}
if (booCounter == 0)
_popup.PopupEntity(Loc.GetString("ghost-component-boo-action-failed"), uid, uid);
args.Handled = true;
}

View File

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

View File

@@ -72,12 +72,16 @@ public sealed partial class GhostRoleComponent : Component
}
}
[DataField("allowSpeech")]
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// 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;
[DataField("allowMovement")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool AllowMovement { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
@@ -107,3 +111,4 @@ public sealed partial class GhostRoleComponent : Component
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // also FIXME Friends
public ProtoId<JobPrototype>? JobProto = null;
}

View File

@@ -9,39 +9,81 @@ namespace Content.Server.Ghost.Roles.Components;
[RegisterComponent, Access(typeof(ToggleableGhostRoleSystem))]
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;
[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;
[DataField("examineTextNoMind")]
/// <summary>
/// The text shown on the entity's Examine when it has no controlling player
/// </summary>
[DataField]
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;
[DataField("roleName")]
/// <summary>
/// The name shown on the Ghost Role list
/// </summary>
[DataField]
public string RoleName = string.Empty;
[DataField("roleDescription")]
/// <summary>
/// The description shown on the Ghost Role list
/// </summary>
[DataField]
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;
[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;
[DataField("wipeVerbPopup")]
/// /// <summary>
/// The popup message when wiping the controlling player
/// </summary>
[DataField]
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;
[DataField("stopSearchVerbPopup")]
/// /// <summary>
/// The popup message when stopping to search for a controlling player
/// </summary>
[DataField]
public string StopSearchVerbPopup = string.Empty;
/// /// <summary>
/// The prototype ID of the job that will be given to the controlling mind
/// </summary>
[DataField("job")]
public ProtoId<JobPrototype>? JobProto = null;
public ProtoId<JobPrototype>? JobProto;
}

View File

@@ -3,11 +3,13 @@
namespace Content.Server.Ghost.Roles;
/// <summary>
/// This is used for round end display of ghost roles.
/// It may also be used to ensure some ghost roles count as antagonists in future.
/// Added to mind role entities to tag that they are a ghostrole.
/// It also holds the name for the round end display
/// </summary>
[RegisterComponent]
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 Robust.Shared.Collections;
using Content.Shared.Ghost.Roles.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server.Ghost.Roles;
@@ -514,13 +513,13 @@ public sealed class GhostRoleSystem : EntitySystem
var newMind = _mindSystem.CreateMind(player.UserId,
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.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>

View File

@@ -51,10 +51,13 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
var ghostRole = EnsureComp<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(uid);
//GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
ghostRole.RoleName = Loc.GetString(component.RoleName);
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
ghostRole.RoleRules = Loc.GetString(component.RoleRules);
ghostRole.JobProto = component.JobProto;
ghostRole.MindRoles = component.MindRoles;
}
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.Labels.Components;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Speech;
using Content.Shared.Telephone;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
@@ -179,11 +180,15 @@ public sealed class HolopadSystem : SharedHolopadSystem
// AI broadcasting
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) ||
stationAiCore.Value.Comp.RemoteEntity == null ||
!TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
return;
// Execute the broadcast, but have it originate from the AI core
ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor);
// 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)
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)
continue;
@@ -391,7 +399,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
var name = Loc.GetString("holopad-hologram-name", ("name", ent));
// 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)
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
if (!TryComp<HolopadHologramComponent>(uid, out var component))
if (!TryComp<HolopadHologramComponent>(hologramUid, out var holopadHologram))
{
Del(uid);
Del(hologramUid);
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)
@@ -608,10 +623,25 @@ public sealed class HolopadSystem : SharedHolopadSystem
DeleteHologram(entity.Comp.Hologram.Value, entity);
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))
_stationAiSystem.SwitchRemoteEntityMode((entity.Owner, stationAiCore), true);
// If the AI core is still broadcasting, end its calls
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);
}

View File

@@ -165,7 +165,7 @@ namespace Content.Server.Kitchen.EntitySystems
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,

View File

@@ -153,7 +153,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
else
{
_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))
{

View File

@@ -275,7 +275,7 @@ namespace Content.Server.Light.EntitySystems
if (time > light.LastThunk + ThunkDelay)
{
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

View File

@@ -106,6 +106,6 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// 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)
{
_audio.PlayEntity(component.KeypadPressSound, Filter.Pvs(uid), uid, true);
_audio.PlayPvs(component.KeypadPressSound, uid);
if (component.Status != NukeStatus.AWAIT_CODE)
return;
@@ -351,12 +351,12 @@ public sealed class NukeSystem : EntitySystem
{
component.Status = NukeStatus.AWAIT_ARM;
component.RemainingTime = component.Timer;
_audio.PlayEntity(component.AccessGrantedSound, Filter.Pvs(uid), uid, true);
_audio.PlayPvs(component.AccessGrantedSound, uid);
}
else
{
component.EnteredCode = "";
_audio.PlayEntity(component.AccessDeniedSound, Filter.Pvs(uid), uid, true);
_audio.PlayPvs(component.AccessDeniedSound, uid);
}
break;
@@ -425,7 +425,9 @@ public sealed class NukeSystem : EntitySystem
// Don't double-dip on the octave shifting
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)

View File

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

View File

@@ -45,10 +45,7 @@ public sealed class HelpProgressConditionSystem : EntitySystem
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind)
.Select(pair => pair.Item1)
.ToHashSet();
var removeList = new List<EntityUid>();
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
// cant help anyone who is tasked with helping:
// 1. thats boring
@@ -56,19 +53,26 @@ public sealed class HelpProgressConditionSystem : EntitySystem
foreach (var traitor in traitors)
{
// 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;
foreach (var objective in mind.Objectives)
{
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
@@ -78,7 +82,7 @@ public sealed class HelpProgressConditionSystem : EntitySystem
return;
}
_target.SetTarget(uid, _random.Pick(traitors), target);
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
}
private float GetProgress(EntityUid target)

View File

@@ -44,7 +44,19 @@ public sealed class KeepAliveConditionSystem : EntitySystem
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.
if (traitors.Count == 0)

View File

@@ -6,6 +6,7 @@ using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
@@ -52,8 +53,18 @@ public sealed class KillPersonConditionSystem : EntitySystem
if (target.Target != null)
return;
// no other humans to kill
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)
{
args.Cancelled = true;

View File

@@ -29,6 +29,7 @@ public sealed class StealConditionSystem : EntitySystem
private EntityQuery<ContainerManagerComponent> _containerQuery;
private HashSet<Entity<TransformComponent>> _nearestEnts = new();
private HashSet<EntityUid> _countedItems = new();
public override void Initialize()
{
@@ -104,6 +105,8 @@ public sealed class StealConditionSystem : EntitySystem
var containerStack = new Stack<ContainerManagerComponent>();
var count = 0;
_countedItems.Clear();
//check stealAreas
if (condition.CheckStealAreas)
{
@@ -174,6 +177,9 @@ public sealed class StealConditionSystem : EntitySystem
private int CheckStealTarget(EntityUid entity, StealConditionComponent condition)
{
if (_countedItems.Contains(entity))
return 0;
// check if this is the target
if (!TryComp<StealTargetComponent>(entity, out var target))
return 0;
@@ -196,6 +202,8 @@ public sealed class StealConditionSystem : EntitySystem
}
}
_countedItems.Add(entity);
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.CartridgeLoader;
using Content.Server.Chat.Managers;
@@ -36,6 +37,7 @@ namespace Content.Server.PDA
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly IdCardSystem _idCard = default!;
public override void Initialize()
{
@@ -55,19 +57,25 @@ namespace Content.Server.PDA
SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed);
SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed, after: new[] { typeof(IdCardSystem) });
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
}
private void OnEntityRenamed(ref EntityRenamedEvent ev)
{
var query = EntityQueryEnumerator<PdaComponent>();
if (HasComp<IdCardComponent>(ev.Uid))
return;
while (query.MoveNext(out var uid, out var comp))
if (_idCard.TryFindIdCard(ev.Uid, out var idCard))
{
if (comp.PdaOwner == ev.Uid)
var query = EntityQueryEnumerator<PdaComponent>();
while (query.MoveNext(out var uid, out var comp))
{
SetOwner(uid, comp, ev.Uid, ev.NewName);
if (comp.ContainedId == idCard)
{
SetOwner(uid, comp, ev.Uid, ev.NewName);
}
}
}
}
@@ -86,6 +94,9 @@ namespace Content.Server.PDA
protected override void OnItemInserted(EntityUid uid, PdaComponent pda, EntInsertedIntoContainerMessage args)
{
base.OnItemInserted(uid, pda, args);
var id = CompOrNull<IdCardComponent>(pda.ContainedId);
if (id != null)
pda.OwnerName = id.FullName;
UpdatePdaUi(uid, pda);
}

View File

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

View File

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

View File

@@ -87,8 +87,8 @@ namespace Content.Server.Power.EntitySystems
var query = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>();
while (query.MoveNext(out var uid, out var comp, out var batt))
{
if (!comp.AutoRecharge) continue;
if (batt.IsFullyCharged) continue;
if (!comp.AutoRecharge || IsFull(uid, batt))
continue;
if (comp.AutoRechargePause)
{
@@ -212,14 +212,14 @@ namespace Content.Server.Power.EntitySystems
}
/// <summary>
/// Returns whether the battery is at least 99% charged, basically full.
/// Returns whether the battery is full.
/// </summary>
public bool IsFull(EntityUid uid, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
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)
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;
if (Math.Abs(heldBattery.MaxCharge - heldBattery.CurrentCharge) < 0.01)
if (_battery.IsFull(heldEnt.Value, heldBattery))
return CellChargerStatus.Charged;
return CellChargerStatus.Charging;
@@ -247,12 +247,6 @@ internal sealed class ChargerSystem : EntitySystem
return;
_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);
}

View File

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

View File

@@ -117,7 +117,7 @@ public sealed class PortableGeneratorSystem : SharedPortableGeneratorSystem
var clogged = _generator.GetIsClogged(uid);
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))
{

View File

@@ -19,10 +19,25 @@ public sealed class JobSystem : SharedJobSystem
public override void 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)
return;

View File

@@ -1,11 +1,13 @@
namespace Content.Server.Roles;
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Adds a briefing to the character info menu, does nothing else.
/// </summary>
[RegisterComponent]
public sealed partial class RoleBriefingComponent : Component
public sealed partial class RoleBriefingComponent : BaseMindRoleComponent
{
[DataField("briefing"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
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.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Roles;
public sealed class RoleSystem : SharedRoleSystem
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public string? MindGetBriefing(EntityUid? mindId)
{
if (mindId == null)
@@ -37,6 +43,32 @@ public sealed class RoleSystem : SharedRoleSystem
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>

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.Roles;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Containers;
@@ -8,6 +10,9 @@ namespace Content.Server.Silicons.Borgs;
/// <inheritdoc/>
public sealed partial class BorgSystem
{
[Dependency] private readonly SharedRoleSystem _roles = default!;
public void InitializeMMI()
{
SubscribeLocalEvent<MMIComponent, ComponentInit>(OnMMIInit);
@@ -41,8 +46,13 @@ public sealed partial class BorgSystem
Dirty(uid, component);
if (_mind.TryGetMind(ent, out var mindId, out var 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);
}
@@ -75,7 +85,12 @@ public sealed partial class BorgSystem
RemComp(uid, component);
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);
}
_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.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared.Access.Systems;
using Content.Shared.Alert;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;

View File

@@ -193,7 +193,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
private void EnsureSubvertedSiliconRole(EntityUid mindId)
{
if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
_roles.MindAddRole(mindId, "MindRoleSubvertedSilicon");
_roles.MindAddRole(mindId, "MindRoleSubvertedSilicon", silent: true);
}
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;
// 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;
// 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;
// open and bolt airlocks

View File

@@ -16,6 +16,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Prototypes;
using Content.Server.Shuttles.Components;
namespace Content.Server.Storage.EntitySystems;
@@ -47,6 +48,8 @@ public sealed class BluespaceLockerSystem : EntitySystem
if (component.BehaviorProperties.BluespaceEffectOnInit)
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)

View File

@@ -7,8 +7,11 @@ using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Labels.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Power;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Speech;
using Content.Shared.Telephone;
using Robust.Server.GameObjects;
@@ -19,8 +22,6 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using System.Linq;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Silicons.Borgs.Components;
namespace Content.Server.Telephone;
@@ -104,12 +105,17 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource));
RaiseLocalEvent(args.MessageSource, nameEv);
var name = Loc.GetString("speech-name-relay",
("speaker", Name(entity)),
("originalName", nameEv.VoiceName));
// Determine if speech should be relayed via the telephone itself or a designated speaker
var speaker = entity.Comp.Speaker != null ? entity.Comp.Speaker.Value.Owner : entity.Owner;
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;
_chat.TrySendInGameICMessage(entity, args.Message, volume, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false);
_chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false);
}
#endregion
@@ -151,7 +157,7 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
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:
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout))
EndTelephoneCalls(entity);
@@ -214,7 +220,15 @@ public sealed class TelephoneSystem : SharedTelephoneSystem
source.Comp.LinkedTelephones.Add(receiver);
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.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)
{
string? presumedName = null;

View File

@@ -439,4 +439,9 @@ public enum LogType
/// A ghost warped to an entity through the ghost warp menu.
/// </summary>
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;
}
public bool GetMainAccessReader(EntityUid uid, [NotNullWhen(true)] out AccessReaderComponent? component)
public bool GetMainAccessReader(EntityUid uid, [NotNullWhen(true)] out Entity<AccessReaderComponent>? ent)
{
component = null;
if (!TryComp(uid, out AccessReaderComponent? accessReader))
ent = null;
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
return false;
component = accessReader;
ent = (uid, accessReader);
if (component.ContainerAccessProvider == null)
if (ent.Value.Comp.ContainerAccessProvider == null)
return true;
if (!_containerSystem.TryGetContainer(uid, component.ContainerAccessProvider, out var container))
if (!_containerSystem.TryGetContainer(uid, ent.Value.Comp.ContainerAccessProvider, out var container))
return true;
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;
}
}

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.
// 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.
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;
if (TryFindIdCard(ev.Uid, out var idCard))

View File

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

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