Merge remote-tracking branch 'upstream/master' into upstream

# Conflicts:
#	Content.Client/Access/AccessOverlay.cs
#	Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
#	Content.Client/Access/UI/IdCardConsoleWindow.xaml
#	Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
#	Content.Client/Chemistry/UI/InjectorStatusControl.cs
#	Content.Client/StatusIcon/StatusIconOverlay.cs
#	Content.Client/Stylesheets/StyleNano.cs
#	Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
#	Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml
#	Content.Server/Access/Systems/IdCardConsoleSystem.cs
#	Content.Server/Administration/Commands/AGhost.cs
#	Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
#	Content.Server/Connection/ConnectionManager.cs
#	Content.Server/DeviceLinking/Systems/SignalTimerSystem.cs
#	Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
#	Content.Server/GameTicking/GameTicker.RoundFlow.cs
#	Content.Server/GameTicking/GameTicker.Spawning.cs
#	Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
#	Content.Server/Resist/EscapeInventorySystem.cs
#	Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
#	Content.Shared/Access/Components/IdCardConsoleComponent.cs
#	Content.Shared/Anomaly/SharedAnomalySystem.cs
#	Content.Shared/Bed/Sleep/SharedSleepingSystem.cs
#	Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/RCD/Systems/RCDSystem.cs
#	Content.Shared/Roles/JobPrototype.cs
#	Content.Shared/StatusIcon/StatusIconPrototype.cs
#	Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
#	Resources/Audio/Machines/attributions.yml
#	Resources/Locale/en-US/rcd/components/rcd-component.ftl
#	Resources/Maps/reach.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_vending.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/security.yml
#	Resources/Prototypes/Catalog/ReagentDispensers/beverage.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/boozeomat.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/cola.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/lawdrobe.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/pwrgame.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/shamblersjuice.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/soda.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/spaceup.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/starkist.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml
#	Resources/Prototypes/DeviceLinking/sink_ports.yml
#	Resources/Prototypes/Entities/Clothing/Back/duffel.yml
#	Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml
#	Resources/Prototypes/Entities/Clothing/Neck/misc.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/gauze.yml
#	Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml
#	Resources/Prototypes/Entities/Objects/Magic/books.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml
#	Resources/Prototypes/Entities/Objects/Misc/tiles.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_assembly.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml
#	Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml
#	Resources/Prototypes/Entities/Structures/Doors/Firelocks/frame.yml
#	Resources/Prototypes/Entities/Structures/Doors/MaterialDoors/material_doors.yml
#	Resources/Prototypes/Entities/Structures/Doors/SecretDoor/secret_door.yml
#	Resources/Prototypes/Entities/Structures/Doors/Windoors/assembly.yml
#	Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml
#	Resources/Prototypes/Entities/Structures/Machines/lathe.yml
#	Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml
#	Resources/Prototypes/Entities/Structures/Storage/Tanks/base_structuretanks.yml
#	Resources/Prototypes/Entities/Structures/Walls/grille.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/shutter.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/flowercrown.yml
#	Resources/Prototypes/Recipes/Crafting/improvised.yml
#	Resources/Prototypes/Roles/Jobs/Security/detective.yml
#	Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
#	Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
#	Resources/Prototypes/Roles/Jobs/Security/warden.yml
#	Resources/Prototypes/StatusEffects/health.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
#	Resources/Prototypes/lobbyscreens.yml
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/ERTSuits/ertchaplain.rsi/equipped-OUTERCLOTHING-body-slim.png
#	Resources/Textures/Decals/bricktile.rsi/white_box.png
#	Resources/Textures/Objects/Misc/books.rsi/meta.json
#	Resources/migration.yml
This commit is contained in:
Remuchi
2024-04-13 11:29:33 +07:00
918 changed files with 18886 additions and 12471 deletions

View File

@@ -9,20 +9,20 @@ namespace Content.Client.Access;
public sealed class AccessOverlay : Overlay
{
private const string TextFontPath = "/Fonts/NotoSans/NotoSans-Regular.ttf";
private const int TextFontSize = 12;
private readonly IEntityManager _entityManager;
private readonly EntityLookupSystem _lookup;
private readonly SharedTransformSystem _xform;
private readonly SharedTransformSystem _transformSystem;
private readonly Font _font;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public AccessOverlay(IEntityManager entManager, IResourceCache cache, EntityLookupSystem lookup, SharedTransformSystem xform)
public AccessOverlay(IEntityManager entityManager, IResourceCache resourceCache, SharedTransformSystem transformSystem)
{
_entityManager = entManager;
_lookup = lookup;
_xform = xform;
_font = cache.GetFont("/Fonts/IBMPlexMono/IBMPlexMono-Regular.ttf", 12);
_entityManager = entityManager;
_transformSystem = transformSystem;
_font = resourceCache.GetFont(TextFontPath, TextFontSize);
}
protected override void Draw(in OverlayDrawArgs args)
@@ -30,52 +30,65 @@ public sealed class AccessOverlay : Overlay
if (args.ViewportControl == null)
return;
var readerQuery = _entityManager.GetEntityQuery<AccessReaderComponent>();
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var ent in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldAABB,
LookupFlags.Static | LookupFlags.Approximate))
var textBuffer = new StringBuilder();
var query = _entityManager.EntityQueryEnumerator<AccessReaderComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var accessReader, out var transform))
{
if (!readerQuery.TryGetComponent(ent, out var reader) ||
!xformQuery.TryGetComponent(ent, out var xform))
textBuffer.Clear();
var entityName = _entityManager.ToPrettyString(uid);
textBuffer.AppendLine(entityName.Prototype);
textBuffer.Append("UID: ");
textBuffer.Append(entityName.Uid.Id);
textBuffer.Append(", NUID: ");
textBuffer.Append(entityName.Nuid.Id);
textBuffer.AppendLine();
if (!accessReader.Enabled)
{
textBuffer.AppendLine("-Disabled");
continue;
}
var text = new StringBuilder();
var index = 0;
var a = $"{_entityManager.ToPrettyString(ent)}";
text.Append(a);
foreach (var list in reader.AccessLists)
if (accessReader.AccessLists.Count > 0)
{
a = $"Tag {index}";
text.AppendLine(a);
foreach (var entry in list)
var groupNumber = 0;
foreach (var accessList in accessReader.AccessLists)
{
a = $"- {entry}";
text.AppendLine(a);
groupNumber++;
foreach (var entry in accessList)
{
textBuffer.Append("+Set ");
textBuffer.Append(groupNumber);
textBuffer.Append(": ");
textBuffer.Append(entry.Id);
textBuffer.AppendLine();
}
}
index++;
}
string textStr;
if (text.Length >= 2)
{
textStr = text.ToString();
textStr = textStr[..^2];
}
else
{
textStr = "";
textBuffer.AppendLine("+Unrestricted");
}
var screenPos = args.ViewportControl.WorldToScreen(_xform.GetWorldPosition(xform));
foreach (var key in accessReader.AccessKeys)
{
textBuffer.Append("+Key ");
textBuffer.Append(key.OriginStation);
textBuffer.Append(": ");
textBuffer.Append(key.Id);
textBuffer.AppendLine();
}
args.ScreenHandle.DrawString(_font, screenPos, textStr, Color.Gold);
foreach (var tag in accessReader.DenyTags)
{
textBuffer.Append("-Tag ");
textBuffer.AppendLine(tag.Id);
}
var accessInfoText = textBuffer.ToString();
var screenPos = args.ViewportControl.WorldToScreen(_transformSystem.GetWorldPosition(transform));
args.ScreenHandle.DrawString(_font, screenPos, accessInfoText, Color.Gold);
}
}
}

View File

@@ -7,8 +7,16 @@ namespace Content.Client.Access.Commands;
public sealed class ShowAccessReadersCommand : IConsoleCommand
{
public string Command => "showaccessreaders";
public string Description => "Shows all access readers in the viewport";
public string Help => $"{Command}";
public string Description => "Toggles showing access reader permissions on the map";
public string Help => """
Overlay Info:
-Disabled | The access reader is disabled
+Unrestricted | The access reader has no restrictions
+Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set)
+Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed
-Tag [Tag Name] | A tag that is not allowed (takes priority over other allows)
""";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var collection = IoCManager.Instance;
@@ -26,10 +34,9 @@ public sealed class ShowAccessReadersCommand : IConsoleCommand
var entManager = collection.Resolve<IEntityManager>();
var cache = collection.Resolve<IResourceCache>();
var lookup = entManager.System<EntityLookupSystem>();
var xform = entManager.System<SharedTransformSystem>();
overlay.AddOverlay(new AccessOverlay(entManager, cache, lookup, xform));
overlay.AddOverlay(new AccessOverlay(entManager, cache, xform));
shell.WriteLine($"Set access reader debug overlay to true");
}
}

View File

@@ -0,0 +1,4 @@
<GridContainer xmlns="https://spacestation14.io"
Columns="5"
HorizontalAlignment="Center">
</GridContainer>

View File

@@ -0,0 +1,52 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
namespace Content.Client.Access.UI;
[GenerateTypedNameReferences]
public sealed partial class AccessLevelControl : GridContainer
{
public readonly Dictionary<ProtoId<AccessLevelPrototype>, Button> ButtonsList = new();
public AccessLevelControl()
{
RobustXamlLoader.Load(this);
}
public void Populate(List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager)
{
foreach (var access in accessLevels)
{
if (!prototypeManager.TryIndex(access, out var accessLevel))
{
Logger.Error($"Unable to find accesslevel for {access}");
continue;
}
var newButton = new Button
{
Text = accessLevel.GetAccessLevelName(),
ToggleMode = true,
};
AddChild(newButton);
ButtonsList.Add(accessLevel.ID, newButton);
}
}
public void UpdateState(
List<ProtoId<AccessLevelPrototype>> pressedList,
List<ProtoId<AccessLevelPrototype>>? enabledList = null)
{
foreach (var (accessName, button) in ButtonsList)
{
button.Pressed = pressedList.Contains(accessName);
button.Disabled = !(enabledList?.Contains(accessName) ?? true);
}
}
}

View File

@@ -64,7 +64,7 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
public void SubmitData(List<string> newAccessList)
public void SubmitData(List<ProtoId<AccessLevelPrototype>> newAccessList)
{
SendMessage(new WriteToTargetAccessReaderIdMessage(newAccessList));
}

View File

@@ -16,7 +16,6 @@ namespace Content.Client.Access.UI
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly ISawmill _logMill = default!;
private readonly AccessOverriderBoundUserInterface _owner;
private readonly Dictionary<string, Button> _accessButtons = new();
@@ -25,7 +24,7 @@ namespace Content.Client.Access.UI
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_logMill = _logManager.GetSawmill(SharedAccessOverriderSystem.Sawmill);
var logMill = _logManager.GetSawmill(SharedAccessOverriderSystem.Sawmill);
_owner = owner;
@@ -33,13 +32,13 @@ namespace Content.Client.Access.UI
{
if (!prototypeManager.TryIndex(access, out var accessLevel))
{
_logMill.Error($"Unable to find accesslevel for {access}");
logMill.Error($"Unable to find accesslevel for {access}");
continue;
}
var newButton = new Button
{
Text = GetAccessLevelName(accessLevel),
Text = accessLevel.GetAccessLevelName(),
ToggleMode = true,
};
@@ -49,14 +48,6 @@ namespace Content.Client.Access.UI
}
}
private static string GetAccessLevelName(AccessLevelPrototype prototype)
{
if (prototype.Name is { } name)
return Loc.GetString(name);
return prototype.ID;
}
public void UpdateState(AccessOverriderBoundUserInterfaceState state)
{
PrivilegedIdLabel.Text = state.PrivilegedIdName;
@@ -105,7 +96,7 @@ namespace Content.Client.Access.UI
_owner.SubmitData(
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList());
_accessButtons.Where(x => x.Value.Pressed).Select(x => new ProtoId<AccessLevelPrototype>(x.Key)).ToList());
}
}
}

View File

@@ -28,7 +28,6 @@ namespace Content.Client.Access.UI
if (EntMan.TryGetComponent<IdCardConsoleComponent>(Owner, out var idCard))
{
accessLevels = idCard.AccessLevels;
accessLevels.Sort();
}
else
{
@@ -70,7 +69,7 @@ namespace Content.Client.Access.UI
public void SubmitData(
string newFullName,
string newJobTitle,
List<string> newAccessList,
List<ProtoId<AccessLevelPrototype>> newAccessList,
string newJobPrototype,
string? newJobIcon)
{

View File

@@ -30,11 +30,7 @@
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
<OptionButton Name="JobPresetOptionButton" />
</GridContainer>
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
<!-- Access level buttons are added here by the C# code -->
</GridContainer>
<Control Name="AccessLevelControlContainer" />
<!-- WD EDIT -->
<GridContainer Name="CurrentJobIcon" Columns="2">
<Label Text="Текущая выбранная иконка для роли: " />

View File

@@ -2,7 +2,6 @@ using System.Linq;
using System.Numerics;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.ResourceManagement;
@@ -22,11 +21,10 @@ namespace Content.Client.Access.UI
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IResourceCache _resource = default!; //WD-EDIT
[Dependency] private readonly IEntityManager _entityManager = default!; //WD-EDIT
private readonly ISawmill _logMill = default!;
private readonly IdCardConsoleBoundUserInterface _owner;
private readonly Dictionary<string, Button> _accessButtons = new();
private AccessLevelControl _accessButtons = new();
private readonly Dictionary<string, TextureButton> _jobIconButtons = new(); //WD-EDIT
private readonly List<string> _jobPrototypeIds = new();
@@ -42,7 +40,6 @@ namespace Content.Client.Access.UI
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_logMill = _logManager.GetSawmill(SharedIdCardConsoleSystem.Sawmill);
_owner = owner;
@@ -65,7 +62,6 @@ namespace Content.Client.Access.UI
var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>().ToList();
jobs.Sort((x, y) => string.Compare(x.LocalizedName, y.LocalizedName, StringComparison.CurrentCulture));
List<Button> buttonsToAdd = new();
foreach (var job in jobs)
{
if (!job.OverrideConsoleVisibility.GetValueOrDefault(job.SetPreference))
@@ -79,30 +75,12 @@ namespace Content.Client.Access.UI
JobPresetOptionButton.OnItemSelected += SelectJobPreset;
foreach (var access in accessLevels)
_accessButtons.Populate(accessLevels, prototypeManager);
AccessLevelControlContainer.AddChild(_accessButtons);
foreach (var (id, button) in _accessButtons.ButtonsList)
{
if (!prototypeManager.TryIndex(access, out var accessLevel))
{
_logMill.Error($"Unable to find accesslevel for {access}");
continue;
}
var newButton = new Button
{
Text = GetAccessLevelName(accessLevel),
ToggleMode = true
};
_accessButtons.Add(accessLevel.ID, newButton);
newButton.OnPressed += _ => SubmitData();
buttonsToAdd.Add(newButton);
}
buttonsToAdd.Sort((x, y) => string.Compare(x.Text, y.Text, StringComparison.Ordinal));
foreach (var button in buttonsToAdd)
{
AccessLevelGrid.AddChild(button);
button.OnPressed += _ => SubmitData();
}
//WD-EDIT
@@ -136,17 +114,9 @@ namespace Content.Client.Access.UI
//WD-EDIT
}
private static string GetAccessLevelName(AccessLevelPrototype prototype)
{
if (prototype.Name is { } name)
return Loc.GetString(name);
return prototype.ID;
}
private void ClearAllAccess()
{
foreach (var button in _accessButtons.Values)
foreach (var button in _accessButtons.ButtonsList.Values)
{
if (button.Pressed)
{
@@ -170,7 +140,7 @@ namespace Content.Client.Access.UI
// this is a sussy way to do this
foreach (var access in job.Access)
{
if (_accessButtons.TryGetValue(access, out var button) && !button.Disabled)
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
{
button.Pressed = true;
}
@@ -185,7 +155,7 @@ namespace Content.Client.Access.UI
foreach (var access in groupPrototype.Tags)
{
if (_accessButtons.TryGetValue(access, out var button) && !button.Disabled)
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
{
button.Pressed = true;
}
@@ -236,15 +206,10 @@ namespace Content.Client.Access.UI
JobPresetOptionButton.Disabled = !interfaceEnabled;
foreach (var (accessName, button) in _accessButtons)
{
button.Disabled = !interfaceEnabled;
if (!interfaceEnabled)
continue;
button.Pressed = state.TargetIdAccessList?.Contains(accessName) ?? false;
button.Disabled = !state.AllowedModifyAccessList?.Contains(accessName) ?? true;
}
_accessButtons.UpdateState(state.TargetIdAccessList?.ToList() ??
new List<ProtoId<AccessLevelPrototype>>(),
state.AllowedModifyAccessList?.ToList() ??
new List<ProtoId<AccessLevelPrototype>>());
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
if (jobIndex >= 0)
@@ -298,7 +263,7 @@ namespace Content.Client.Access.UI
FullNameLineEdit.Text,
JobTitleLineEdit.Text,
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
_accessButtons.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty,
_lastJobIcon);
}

View File

@@ -1,9 +1,9 @@
<controls:FancyWindow
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'anomaly-scanner-ui-title'}"
MinSize="350 260"
SetSize="350 260">
MinSize="350 400"
SetSize="350 400">
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="10 0 10 10">
<RichTextLabel Name="TextDisplay"></RichTextLabel>
</BoxContainer>

View File

@@ -27,6 +27,11 @@ public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
SendMessage(new BountyPrintLabelMessage(id));
};
_menu.OnSkipButtonPressed += id =>
{
SendMessage(new BountySkipMessage(id));
};
_menu.OpenCentered();
}
@@ -37,7 +42,7 @@ public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
if (message is not CargoBountyConsoleState state)
return;
_menu?.UpdateEntries(state.Bounties);
_menu?.UpdateEntries(state.Bounties, state.UntilNextSkip);
}
protected override void Dispose(bool disposing)

View File

@@ -13,7 +13,18 @@
</BoxContainer>
<Control MinWidth="10"/>
<BoxContainer Orientation="Vertical" MinWidth="120">
<Button Name="PrintButton" Text="{Loc 'bounty-console-label-button-text'}" HorizontalExpand="False" HorizontalAlignment="Right"/>
<BoxContainer Orientation="Horizontal" MinWidth="120">
<Button Name="PrintButton"
Text="{Loc 'bounty-console-label-button-text'}"
HorizontalExpand="False"
HorizontalAlignment="Right"
StyleClasses="OpenRight"/>
<Button Name="SkipButton"
Text="{Loc 'bounty-console-skip-button-text'}"
HorizontalExpand="False"
HorizontalAlignment="Right"
StyleClasses="OpenLeft"/>
</BoxContainer>
<RichTextLabel Name="IdLabel" HorizontalAlignment="Right" Margin="0 0 5 0"/>
</BoxContainer>
</BoxContainer>

View File

@@ -1,11 +1,13 @@
using Content.Client.Message;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Random;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Serilog;
namespace Content.Client.Cargo.UI;
@@ -14,15 +16,19 @@ public sealed partial class BountyEntry : BoxContainer
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public Action? OnButtonPressed;
public Action? OnLabelButtonPressed;
public Action? OnSkipButtonPressed;
public TimeSpan EndTime;
public TimeSpan UntilNextSkip;
public BountyEntry(CargoBountyData bounty)
public BountyEntry(CargoBountyData bounty, TimeSpan untilNextSkip)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
UntilNextSkip = untilNextSkip;
if (!_prototype.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var bountyPrototype))
return;
@@ -38,6 +44,27 @@ public sealed partial class BountyEntry : BoxContainer
DescriptionLabel.SetMarkup(Loc.GetString("bounty-console-description-label", ("description", Loc.GetString(bountyPrototype.Description))));
IdLabel.SetMarkup(Loc.GetString("bounty-console-id-label", ("id", bounty.Id)));
PrintButton.OnPressed += _ => OnButtonPressed?.Invoke();
PrintButton.OnPressed += _ => OnLabelButtonPressed?.Invoke();
SkipButton.OnPressed += _ => OnSkipButtonPressed?.Invoke();
}
private void UpdateSkipButton(float deltaSeconds)
{
UntilNextSkip -= TimeSpan.FromSeconds(deltaSeconds);
if (UntilNextSkip > TimeSpan.Zero)
{
SkipButton.Label.Text = UntilNextSkip.ToString("mm\\:ss");
SkipButton.Disabled = true;
return;
}
SkipButton.Label.Text = Loc.GetString("bounty-console-skip-button-text");
SkipButton.Disabled = false;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateSkipButton(args.DeltaSeconds);
}
}

View File

@@ -10,19 +10,21 @@ namespace Content.Client.Cargo.UI;
public sealed partial class CargoBountyMenu : FancyWindow
{
public Action<string>? OnLabelButtonPressed;
public Action<string>? OnSkipButtonPressed;
public CargoBountyMenu()
{
RobustXamlLoader.Load(this);
}
public void UpdateEntries(List<CargoBountyData> bounties)
public void UpdateEntries(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
{
BountyEntriesContainer.Children.Clear();
foreach (var b in bounties)
{
var entry = new BountyEntry(b);
entry.OnButtonPressed += () => OnLabelButtonPressed?.Invoke(b.Id);
var entry = new BountyEntry(b, untilNextSkip);
entry.OnLabelButtonPressed += () => OnLabelButtonPressed?.Invoke(b.Id);
entry.OnSkipButtonPressed += () => OnSkipButtonPressed?.Invoke(b.Id);
BountyEntriesContainer.AddChild(entry);
}

View File

@@ -15,9 +15,10 @@ public sealed class InjectorStatusControl : Control
private readonly SharedSolutionContainerSystem _solutionContainers;
private readonly RichTextLabel _label;
private FixedPoint2 _prevVolume;
private FixedPoint2 _prevMaxVolume;
private InjectorToggleMode _prevToggleState;
private FixedPoint2 PrevVolume;
private FixedPoint2 PrevMaxVolume;
private FixedPoint2 PrevTransferAmount;
private InjectorToggleMode PrevToggleState;
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
{
@@ -35,14 +36,16 @@ public sealed class InjectorStatusControl : Control
return;
// only updates the UI if any of the details are different than they previously were
if (_prevVolume == solution.Volume
&& _prevMaxVolume == solution.MaxVolume
&& _prevToggleState == _parent.Comp.ToggleState)
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevTransferAmount == _parent.Comp.TransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
return;
_prevVolume = solution.Volume;
_prevMaxVolume = solution.MaxVolume;
_prevToggleState = _parent.Comp.ToggleState;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.TransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
// Update current volume and injector state
var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch

View File

@@ -96,24 +96,22 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state, appearance))
{
return;
}
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseFlush, state is VisualState.Flushing or VisualState.Charging);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == VisualState.Flushing)
if (state == VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayer)
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
@@ -125,7 +123,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.BaseFlush,
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
@@ -154,26 +152,18 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
_animationSystem.Play(uid, anim, AnimationKey);
}
}
else if (state == VisualState.Charging)
{
sprite.LayerSetState(DisposalUnitVisualLayers.BaseFlush, chargingState);
}
else if (state == VisualState.OverlayCharging)
sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, new RSI.StateId("disposal-charging"));
else
{
_animationSystem.Stop(uid, AnimationKey);
}
if (!_appearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState, appearance))
{
handleState = HandleState.Normal;
}
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal);
if (!_appearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState, appearance))
{
lightState = LightStates.Off;
}
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & LightStates.Charging) != 0);
@@ -189,7 +179,7 @@ public enum DisposalUnitVisualLayers : byte
Unanchored,
Base,
BaseCharging,
BaseFlush,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,

View File

@@ -0,0 +1,59 @@
using Content.Shared.Access;
using Content.Shared.Doors.Electronics;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Doors.Electronics;
public sealed class DoorElectronicsBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private DoorElectronicsConfigurationMenu? _window;
public DoorElectronicsBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
List<ProtoId<AccessLevelPrototype>> accessLevels = new();
foreach (var accessLevel in _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>())
{
if (accessLevel.Name != null)
{
accessLevels.Add(accessLevel.ID);
}
}
accessLevels.Sort();
_window = new DoorElectronicsConfigurationMenu(this, accessLevels, _prototypeManager);
_window.OnClose += Close;
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (DoorElectronicsConfigurationState) state;
_window?.UpdateState(castState);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
_window?.Dispose();
}
public void UpdateConfiguration(List<ProtoId<AccessLevelPrototype>> newAccessList)
{
SendMessage(new DoorElectronicsUpdateConfigurationMessage(newAccessList));
}
}

View File

@@ -0,0 +1,6 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<Control Name="AccessLevelControlContainer" />
</controls:FancyWindow>

View File

@@ -0,0 +1,41 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Content.Client.Access.UI;
using Content.Client.Doors.Electronics;
using Content.Shared.Access;
using Content.Shared.Doors.Electronics;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
namespace Content.Client.Doors.Electronics;
[GenerateTypedNameReferences]
public sealed partial class DoorElectronicsConfigurationMenu : FancyWindow
{
private readonly DoorElectronicsBoundUserInterface _owner;
private AccessLevelControl _buttonsList = new();
public DoorElectronicsConfigurationMenu(DoorElectronicsBoundUserInterface ui, List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager)
{
RobustXamlLoader.Load(this);
_owner = ui;
_buttonsList.Populate(accessLevels, prototypeManager);
AccessLevelControlContainer.AddChild(_buttonsList);
foreach (var (id, button) in _buttonsList.ButtonsList)
{
button.OnPressed += _ => _owner.UpdateConfiguration(
_buttonsList.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList());
}
}
public void UpdateState(DoorElectronicsConfigurationState state)
{
_buttonsList.UpdateState(state.AccessList);
}
}

View File

@@ -212,14 +212,16 @@ namespace Content.Client.Examine
var vBox = new BoxContainer
{
Name = "ExaminePopupVbox",
Orientation = LayoutOrientation.Vertical
Orientation = LayoutOrientation.Vertical,
MaxWidth = _examineTooltipOpen.MaxWidth
};
panel.AddChild(vBox);
var hBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 5
SeparationOverride = 5,
Margin = new Thickness(6, 0, 6, 0)
};
vBox.AddChild(hBox);
@@ -229,8 +231,7 @@ namespace Content.Client.Examine
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
SetSize = new Vector2(32, 32),
Margin = new Thickness(2, 0, 2, 0),
SetSize = new Vector2(32, 32)
};
spriteView.SetEntity(target);
hBox.AddChild(spriteView);
@@ -238,19 +239,17 @@ namespace Content.Client.Examine
if (knowTarget)
{
hBox.AddChild(new Label
{
Text = Identity.Name(target, EntityManager, player),
HorizontalExpand = true,
});
var itemName = FormattedMessage.RemoveMarkup(Identity.Name(target, EntityManager, player));
var labelMessage = FormattedMessage.FromMarkup($"[bold]{itemName}[/bold]");
var label = new RichTextLabel();
label.SetMessage(labelMessage);
hBox.AddChild(label);
}
else
{
hBox.AddChild(new Label
{
Text = "???",
HorizontalExpand = true,
});
var label = new RichTextLabel();
label.SetMessage(FormattedMessage.FromMarkup("[bold]???[/bold]"));
hBox.AddChild(label);
}
panel.Measure(Vector2Helpers.Infinity);

View File

@@ -45,6 +45,9 @@ namespace Content.Client.Input
// Not in engine because the engine doesn't understand what a flipped object is
common.AddFunction(ContentKeyFunctions.EditorFlipObject);
// Not in engine so that the RCD can rotate objects
common.AddFunction(EngineKeyFunctions.EditorRotateObject);
var human = contexts.GetContext("human");
human.AddFunction(EngineKeyFunctions.MoveUp);
human.AddFunction(EngineKeyFunctions.MoveDown);

View File

@@ -124,9 +124,7 @@
<BoxContainer
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
MinHeight="225"
>
Orientation="Vertical">
<Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/>
<BoxContainer
Orientation="Vertical"

View File

@@ -104,41 +104,12 @@ public sealed partial class LatheMenu : DefaultWindow
RecipeList.Children.Clear();
foreach (var prototype in sortedRecipesToShow)
{
StringBuilder sb = new();
var first = true;
foreach (var (id, amount) in prototype.RequiredMaterials)
{
if (!_prototypeManager.TryIndex<MaterialPrototype>(id, out var proto))
continue;
if (first)
first = false;
else
sb.Append('\n');
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, component.MaterialUseMultiplier);
var sheetVolume = _materialStorage.GetSheetVolume(proto);
var unit = Loc.GetString(proto.Unit);
// rounded in locale not here
var sheets = adjustedAmount / (float) sheetVolume;
var amountText = Loc.GetString("lathe-menu-material-amount", ("amount", sheets), ("unit", unit));
var name = Loc.GetString(proto.Name);
sb.Append(Loc.GetString("lathe-menu-tooltip-display", ("material", name), ("amount", amountText)));
}
if (!string.IsNullOrWhiteSpace(prototype.Description))
{
sb.Append('\n');
sb.Append(Loc.GetString("lathe-menu-description-display", ("description", prototype.Description)));
}
var icon = prototype.Icon == null
? _spriteSystem.GetPrototypeIcon(prototype.Result).Default
: _spriteSystem.Frame0(prototype.Icon);
var canProduce = _lathe.CanProduce(_owner, prototype, quantity);
var control = new RecipeControl(prototype, sb.ToString(), canProduce, icon);
var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, icon);
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
@@ -149,6 +120,51 @@ public sealed partial class LatheMenu : DefaultWindow
}
}
private string GenerateTooltipText(LatheRecipePrototype prototype)
{
StringBuilder sb = new();
foreach (var (id, amount) in prototype.RequiredMaterials)
{
if (!_prototypeManager.TryIndex<MaterialPrototype>(id, out var proto))
continue;
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, _entityManager.GetComponent<LatheComponent>(_owner).MaterialUseMultiplier);
var sheetVolume = _materialStorage.GetSheetVolume(proto);
var unit = Loc.GetString(proto.Unit);
var sheets = adjustedAmount / (float) sheetVolume;
var availableAmount = _materialStorage.GetMaterialAmount(_owner, id);
var missingAmount = Math.Max(0, adjustedAmount - availableAmount);
var missingSheets = missingAmount / (float) sheetVolume;
var name = Loc.GetString(proto.Name);
string tooltipText;
if (missingSheets > 0)
{
tooltipText = Loc.GetString("lathe-menu-material-amount-missing", ("amount", sheets), ("missingAmount", missingSheets), ("unit", unit), ("material", name));
}
else
{
var amountText = Loc.GetString("lathe-menu-material-amount", ("amount", sheets), ("unit", unit));
tooltipText = Loc.GetString("lathe-menu-tooltip-display", ("material", name), ("amount", amountText));
}
sb.AppendLine(tooltipText);
}
if (!string.IsNullOrWhiteSpace(prototype.Description))
sb.AppendLine(Loc.GetString("lathe-menu-description-display", ("description", prototype.Description)));
// Remove last newline
if (sb.Length > 0)
sb.Remove(sb.Length - 1, 1);
return sb.ToString();
}
public void UpdateCategories()
{
var currentCategories = new List<ProtoId<LatheCategoryPrototype>>();

View File

@@ -11,17 +11,16 @@ namespace Content.Client.Lathe.UI;
public sealed partial class RecipeControl : Control
{
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
public string TooltipText;
public RecipeControl(LatheRecipePrototype recipe, string tooltip, bool canProduce, Texture? texture = null)
public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Texture? texture = null)
{
RobustXamlLoader.Load(this);
RecipeName.Text = recipe.Name;
RecipeTexture.Texture = texture;
Button.Disabled = !canProduce;
TooltipText = tooltip;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;
Button.OnPressed += (_) =>
@@ -32,6 +31,6 @@ public sealed partial class RecipeControl : Control
private Control? SupplyTooltip(Control sender)
{
return new RecipeTooltip(TooltipText);
return new RecipeTooltip(TooltipTextSupplier());
}
}

View File

@@ -1,7 +1,8 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical"
<ScrollContainer xmlns="https://spacestation14.io"
SizeFlagsStretchRatio="8"
HorizontalExpand="True"
VerticalExpand="True">
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
</BoxContainer>
<BoxContainer Name="MaterialList" Orientation="Vertical">
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
</BoxContainer>
</ScrollContainer>

View File

@@ -12,7 +12,7 @@ namespace Content.Client.Materials.UI;
/// This widget is one row in the lathe eject menu.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MaterialStorageControl : BoxContainer
public sealed partial class MaterialStorageControl : ScrollContainer
{
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -72,7 +72,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
}
var children = new List<MaterialDisplay>();
children.AddRange(Children.OfType<MaterialDisplay>());
children.AddRange(MaterialList.Children.OfType<MaterialDisplay>());
foreach (var display in children)
{
@@ -80,7 +80,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
if (extra.Contains(mat))
{
RemoveChild(display);
MaterialList.RemoveChild(display);
continue;
}
@@ -92,7 +92,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
foreach (var mat in missing)
{
var volume = mats[mat];
AddChild(new MaterialDisplay(_owner.Value, mat, volume, canEject));
MaterialList.AddChild(new MaterialDisplay(_owner.Value, mat, volume, canEject));
}
_currentMaterials = mats;

View File

@@ -210,9 +210,9 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead");
}
else if (sensor.TotalDamage != null)
else if (sensor.DamagePercentage != null)
{
var index = MathF.Round(4f * (sensor.TotalDamage.Value / 100f));
var index = MathF.Round(4f * sensor.DamagePercentage.Value);
if (index >= 5)
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");

View File

@@ -1,7 +0,0 @@
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Client.Nutrition.EntitySystems;
public sealed class OpenableSystem : SharedOpenableSystem
{
}

View File

@@ -25,6 +25,14 @@
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-chat-window-opacity'}" Margin="8 0" />
<Slider Name="ChatWindowOpacitySlider"
MinValue="0"
MaxValue="1"
MinWidth="200" />
<Label Name="ChatWindowOpacityLabel" Margin="8 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-screen-shake-intensity'}" Margin="8 0" />
<Slider Name="ScreenShakeIntensitySlider"
@@ -41,6 +49,7 @@
<Label Text="{Loc 'ui-options-general-speech'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ShowOocPatronColor" Text="{Loc 'ui-options-show-ooc-patron-color'}" />
<CheckBox Name="ShowLoocAboveHeadCheckBox" Text="{Loc 'ui-options-show-looc-on-head'}" />
<CheckBox Name="FancySpeechBubblesCheckBox" Text="{Loc 'ui-options-fancy-speech'}" />
<CheckBox Name="FancyNameBackgroundsCheckBox" Text="{Loc 'ui-options-fancy-name-background'}" />
@@ -66,6 +75,3 @@
</controls:StripeBack>
</BoxContainer>
</tabs:MiscTab>

View File

@@ -3,11 +3,14 @@ using Content.Client.UserInterface.Screens;
using Content.Shared.CCVar;
using Content.Shared.HUD;
using Robust.Client.AutoGenerated;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Range = Robust.Client.UserInterface.Controls.Range;
@@ -16,6 +19,7 @@ namespace Content.Client.Options.UI.Tabs
[GenerateTypedNameReferences]
public sealed partial class MiscTab : Control
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -55,8 +59,11 @@ namespace Content.Client.Options.UI.Tabs
UpdateApplyButton();
};
ShowOocPatronColor.Visible = _playerManager.LocalSession?.Channel.UserData.PatronTier is { } patron;
HudThemeOption.OnItemSelected += OnHudThemeChanged;
DiscordRich.OnToggled += OnCheckBoxToggled;
ShowOocPatronColor.OnToggled += OnCheckBoxToggled;
ShowLoocAboveHeadCheckBox.OnToggled += OnCheckBoxToggled;
ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled;
ShowCombatModeIndicatorsCheckBox.OnToggled += OnCheckBoxToggled;
@@ -66,12 +73,14 @@ namespace Content.Client.Options.UI.Tabs
EnableColorNameCheckBox.OnToggled += OnCheckBoxToggled;
ColorblindFriendlyCheckBox.OnToggled += OnCheckBoxToggled;
ReducedMotionCheckBox.OnToggled += OnCheckBoxToggled;
ChatWindowOpacitySlider.OnValueChanged += OnChatWindowOpacitySliderChanged;
ScreenShakeIntensitySlider.OnValueChanged += OnScreenShakeIntensitySliderChanged;
// ToggleWalk.OnToggled += OnCheckBoxToggled;
StaticStorageUI.OnToggled += OnCheckBoxToggled;
HudThemeOption.SelectId(_hudThemeIdToIndex.GetValueOrDefault(_cfg.GetCVar(CVars.InterfaceTheme), 0));
DiscordRich.Pressed = _cfg.GetCVar(CVars.DiscordEnabled);
ShowOocPatronColor.Pressed = _cfg.GetCVar(CCVars.ShowOocPatronColor);
ShowLoocAboveHeadCheckBox.Pressed = _cfg.GetCVar(CCVars.LoocAboveHeadShow);
ShowHeldItemCheckBox.Pressed = _cfg.GetCVar(CCVars.HudHeldItemShow);
ShowCombatModeIndicatorsCheckBox.Pressed = _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow);
@@ -81,6 +90,7 @@ namespace Content.Client.Options.UI.Tabs
EnableColorNameCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableColorName);
ColorblindFriendlyCheckBox.Pressed = _cfg.GetCVar(CCVars.AccessibilityColorblindFriendly);
ReducedMotionCheckBox.Pressed = _cfg.GetCVar(CCVars.ReducedMotion);
ChatWindowOpacitySlider.Value = _cfg.GetCVar(CCVars.ChatWindowOpacity);
ScreenShakeIntensitySlider.Value = _cfg.GetCVar(CCVars.ScreenShakeIntensity) * 100f;
// ToggleWalk.Pressed = _cfg.GetCVar(CCVars.ToggleWalk);
StaticStorageUI.Pressed = _cfg.GetCVar(CCVars.StaticStorageUI);
@@ -101,6 +111,13 @@ namespace Content.Client.Options.UI.Tabs
UpdateApplyButton();
}
private void OnChatWindowOpacitySliderChanged(Range range)
{
ChatWindowOpacityLabel.Text = Loc.GetString("ui-options-chat-window-opacity-percent",
("opacity", range.Value));
UpdateApplyButton();
}
private void OnScreenShakeIntensitySliderChanged(Range obj)
{
ScreenShakeIntensityLabel.Text = Loc.GetString("ui-options-screen-shake-percent", ("intensity", ScreenShakeIntensitySlider.Value / 100f));
@@ -121,12 +138,14 @@ namespace Content.Client.Options.UI.Tabs
_cfg.SetCVar(CCVars.HudHeldItemShow, ShowHeldItemCheckBox.Pressed);
_cfg.SetCVar(CCVars.CombatModeIndicatorsPointShow, ShowCombatModeIndicatorsCheckBox.Pressed);
_cfg.SetCVar(CCVars.OpaqueStorageWindow, OpaqueStorageWindowCheckBox.Pressed);
_cfg.SetCVar(CCVars.ShowOocPatronColor, ShowOocPatronColor.Pressed);
_cfg.SetCVar(CCVars.LoocAboveHeadShow, ShowLoocAboveHeadCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatEnableColorName, EnableColorNameCheckBox.Pressed);
_cfg.SetCVar(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox.Pressed);
_cfg.SetCVar(CCVars.ReducedMotion, ReducedMotionCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider.Value);
_cfg.SetCVar(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider.Value / 100f);
// _cfg.SetCVar(CCVars.ToggleWalk, ToggleWalk.Pressed);
_cfg.SetCVar(CCVars.StaticStorageUI, StaticStorageUI.Pressed);
@@ -148,12 +167,14 @@ namespace Content.Client.Options.UI.Tabs
var isShowHeldItemSame = ShowHeldItemCheckBox.Pressed == _cfg.GetCVar(CCVars.HudHeldItemShow);
var isCombatModeIndicatorsSame = ShowCombatModeIndicatorsCheckBox.Pressed == _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow);
var isOpaqueStorageWindow = OpaqueStorageWindowCheckBox.Pressed == _cfg.GetCVar(CCVars.OpaqueStorageWindow);
var isOocPatronColorShowSame = ShowOocPatronColor.Pressed == _cfg.GetCVar(CCVars.ShowOocPatronColor);
var isLoocShowSame = ShowLoocAboveHeadCheckBox.Pressed == _cfg.GetCVar(CCVars.LoocAboveHeadShow);
var isFancyChatSame = FancySpeechBubblesCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableFancyBubbles);
var isFancyBackgroundSame = FancyNameBackgroundsCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatFancyNameBackground);
var isEnableColorNameSame = EnableColorNameCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableColorName);
var isColorblindFriendly = ColorblindFriendlyCheckBox.Pressed == _cfg.GetCVar(CCVars.AccessibilityColorblindFriendly);
var isReducedMotionSame = ReducedMotionCheckBox.Pressed == _cfg.GetCVar(CCVars.ReducedMotion);
var isChatWindowOpacitySame = Math.Abs(ChatWindowOpacitySlider.Value - _cfg.GetCVar(CCVars.ChatWindowOpacity)) < 0.01f;
var isScreenShakeIntensitySame = Math.Abs(ScreenShakeIntensitySlider.Value / 100f - _cfg.GetCVar(CCVars.ScreenShakeIntensity)) < 0.01f;
// var isToggleWalkSame = ToggleWalk.Pressed == _cfg.GetCVar(CCVars.ToggleWalk);
var isStaticStorageUISame = StaticStorageUI.Pressed == _cfg.GetCVar(CCVars.StaticStorageUI);
@@ -164,12 +185,14 @@ namespace Content.Client.Options.UI.Tabs
isShowHeldItemSame &&
isCombatModeIndicatorsSame &&
isOpaqueStorageWindow &&
isOocPatronColorShowSame &&
isLoocShowSame &&
isFancyChatSame &&
isFancyBackgroundSame &&
isEnableColorNameSame &&
isColorblindFriendly &&
isReducedMotionSame &&
isChatWindowOpacitySame &&
isScreenShakeIntensitySame &&
// isToggleWalkSame &&
isStaticStorageUISame;

View File

@@ -19,7 +19,6 @@ namespace Content.Client.Overlays;
/// </summary>
public sealed class EntityHealthBarOverlay : Overlay
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
private readonly IEntityManager _entManager;
private readonly SharedTransformSystem _transform;
private readonly MobStateSystem _mobStateSystem;
@@ -27,17 +26,14 @@ public sealed class EntityHealthBarOverlay : Overlay
private readonly ProgressColorSystem _progressColor;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
public HashSet<string> DamageContainers = new();
private readonly ShaderInstance _shader;
public EntityHealthBarOverlay(IEntityManager entManager)
{
IoCManager.InjectDependencies(this);
_entManager = entManager;
_transform = _entManager.System<SharedTransformSystem>();
_mobStateSystem = _entManager.System<MobStateSystem>();
_mobThresholdSystem = _entManager.System<MobThresholdSystem>();
_progressColor = _entManager.System<ProgressColorSystem>();
_shader = _prototype.Index<ShaderPrototype>("unshaded").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
@@ -50,8 +46,6 @@ public sealed class EntityHealthBarOverlay : Overlay
var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3.CreateRotation(-rotation);
handle.UseShader(_shader);
var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, SpriteComponent>();
while (query.MoveNext(out var uid,
out var mobThresholdsComponent,
@@ -122,7 +116,6 @@ public sealed class EntityHealthBarOverlay : Overlay
handle.DrawRect(pixelDarken, Black.WithAlpha(128));
}
handle.UseShader(null);
handle.SetTransform(Matrix3.Identity);
}

View File

@@ -1,116 +0,0 @@
using System.Linq;
using Robust.Client.GameObjects;
using static Robust.Client.GameObjects.SpriteComponent;
using Content.Shared.Clothing;
using Content.Shared.Hands;
using Content.Shared.Paint;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client.Paint;
public sealed class PaintedVisualizerSystem : VisualizerSystem<PaintedComponent>
{
/// <summary>
/// Visualizer for Paint which applies a shader and colors the entity.
/// </summary>
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PaintedComponent, HeldVisualsUpdatedEvent>(OnHeldVisualsUpdated);
SubscribeLocalEvent<PaintedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<PaintedComponent, EquipmentVisualsUpdatedEvent>(OnEquipmentVisualsUpdated);
}
protected override void OnAppearanceChange(EntityUid uid, PaintedComponent component, ref AppearanceChangeEvent args)
{
var shader = _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance();
if (args.Sprite == null)
return;
// What is this even doing? It's not even checking what the value is.
if (!_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted))
return;
var sprite = args.Sprite;
foreach (var spriteLayer in sprite.AllLayers)
{
if (spriteLayer is not Layer layer)
continue;
if (layer.Shader == null) // If shader isn't null we dont want to replace the original shader.
{
layer.Shader = shader;
layer.Color = component.Color;
}
}
}
private void OnHeldVisualsUpdated(EntityUid uid, PaintedComponent component, HeldVisualsUpdatedEvent args)
{
if (args.RevealedLayers.Count == 0)
return;
if (!TryComp(args.User, out SpriteComponent? sprite))
return;
foreach (var revealed in args.RevealedLayers)
{
if (!sprite.LayerMapTryGet(revealed, out var layer))
continue;
sprite.LayerSetShader(layer, component.ShaderName);
sprite.LayerSetColor(layer, component.Color);
}
}
private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args)
{
if (args.RevealedLayers.Count == 0)
return;
if (!TryComp(args.Equipee, out SpriteComponent? sprite))
return;
foreach (var revealed in args.RevealedLayers)
{
if (!sprite.LayerMapTryGet(revealed, out var layer))
continue;
sprite.LayerSetShader(layer, component.ShaderName);
sprite.LayerSetColor(layer, component.Color);
}
}
private void OnShutdown(EntityUid uid, PaintedComponent component, ref ComponentShutdown args)
{
if (!TryComp(uid, out SpriteComponent? sprite))
return;
component.BeforeColor = sprite.Color;
var shader = _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance();
if (!Terminating(uid))
{
foreach (var spriteLayer in sprite.AllLayers)
{
if (spriteLayer is not Layer layer)
continue;
if (layer.Shader == shader) // If shader isn't same as one in component we need to ignore it.
{
layer.Shader = null;
if (layer.Color == component.Color) // If color isn't the same as one in component we don't want to change it.
layer.Color = component.BeforeColor;
}
}
}
}
}

View File

@@ -114,9 +114,16 @@ public partial class NavMapControl : MapGridControl
VerticalExpand = false,
Children =
{
_zoom,
_beacons,
_recenter,
new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
_zoom,
_beacons,
_recenter
}
}
}
};

View File

@@ -188,10 +188,13 @@ namespace Content.Client.Popups
PopupEntity(message, uid, type);
}
public override void PopupClient(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient == null)
return;
if (_timing.IsFirstTimePredicted)
PopupEntity(message, uid, recipient, type);
PopupEntity(message, uid, recipient.Value, type);
}
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)

View File

@@ -0,0 +1,122 @@
using System.Numerics;
using Content.Client.Gameplay;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.RCD.Components;
using Content.Shared.RCD.Systems;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Client.RCD;
public sealed class AlignRCDConstruction : PlacementMode
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly RCDSystem _rcdSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
private const float SearchBoxSize = 2f;
private const float PlaceColorBaseAlpha = 0.5f;
private EntityCoordinates _unalignedMouseCoords = default;
/// <summary>
/// This placement mode is not on the engine because it is content specific (i.e., for the RCD)
/// </summary>
public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
{
var dependencies = IoCManager.Instance!;
_entityManager = dependencies.Resolve<IEntityManager>();
_mapManager = dependencies.Resolve<IMapManager>();
_playerManager = dependencies.Resolve<IPlayerManager>();
_stateManager = dependencies.Resolve<IStateManager>();
_mapSystem = _entityManager.System<SharedMapSystem>();
_rcdSystem = _entityManager.System<RCDSystem>();
_transformSystem = _entityManager.System<SharedTransformSystem>();
ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
}
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
{
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
var gridId = MouseCoords.GetGridUid(_entityManager);
if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
return;
CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
float tileSize = mapGrid.TileSize;
GridDistancing = tileSize;
if (pManager.CurrentPermission!.IsTile)
{
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2,
CurrentTile.Y + tileSize / 2));
}
else
{
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
}
}
public override bool IsValidPosition(EntityCoordinates position)
{
var player = _playerManager.LocalSession?.AttachedEntity;
// If the destination is out of interaction range, set the placer alpha to zero
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var xform))
return false;
if (!xform.Coordinates.InRange(_entityManager, _transformSystem, position, SharedInteractionSystem.InteractionRange))
{
InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0);
return false;
}
// Otherwise restore the alpha value
else
{
InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
}
// Determine if player is carrying an RCD in their active hand
if (!_entityManager.TryGetComponent<HandsComponent>(player, out var hands))
return false;
var heldEntity = hands.ActiveHand?.HeldEntity;
if (!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
return false;
// Retrieve the map grid data for the position
if (!_rcdSystem.TryGetMapGridData(position, out var mapGridData))
return false;
// Determine if the user is hovering over a target
var currentState = _stateManager.CurrentState;
if (currentState is not GameplayStateBase screen)
return false;
var target = screen.GetClickedEntity(_unalignedMouseCoords.ToMap(_entityManager, _transformSystem));
// Determine if the RCD operation is valid or not
if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, mapGridData.Value, target, player.Value, false))
return false;
return true;
}
}

View File

@@ -0,0 +1,78 @@
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Content.Shared.RCD.Systems;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Shared.Enums;
namespace Content.Client.RCD;
public sealed class RCDConstructionGhostSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly RCDSystem _rcdSystem = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
private string _placementMode = typeof(AlignRCDConstruction).Name;
private Direction _placementDirection = default;
public override void Update(float frameTime)
{
base.Update(frameTime);
// Get current placer data
var placerEntity = _placementManager.CurrentPermission?.MobUid;
var placerProto = _placementManager.CurrentPermission?.EntityType;
var placerIsRCD = HasComp<RCDComponent>(placerEntity);
// Exit if erasing or the current placer is not an RCD (build mode is active)
if (_placementManager.Eraser || (placerEntity != null && !placerIsRCD))
return;
// Determine if player is carrying an RCD in their active hand
var player = _playerManager.LocalSession?.AttachedEntity;
if (!TryComp<HandsComponent>(player, out var hands))
return;
var heldEntity = hands.ActiveHand?.HeldEntity;
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
{
// If the player was holding an RCD, but is no longer, cancel placement
if (placerIsRCD)
_placementManager.Clear();
return;
}
// Update the direction the RCD prototype based on the placer direction
if (_placementDirection != _placementManager.Direction)
{
_placementDirection = _placementManager.Direction;
RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection));
}
// If the placer has not changed, exit
_rcdSystem.UpdateCachedPrototype(heldEntity.Value, rcd);
if (heldEntity == placerEntity && rcd.CachedPrototype.Prototype == placerProto)
return;
// Create a new placer
var newObjInfo = new PlacementInformation
{
MobUid = heldEntity.Value,
PlacementOption = _placementMode,
EntityType = rcd.CachedPrototype.Prototype,
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
IsTile = (rcd.CachedPrototype.Mode == RcdMode.ConstructTile),
UseEditorContext = false,
};
_placementManager.Clear();
_placementManager.BeginPlacing(newObjInfo);
}
}

View File

@@ -0,0 +1,47 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:rcd="clr-namespace:Content.Client.RCD"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
<!-- Entry layer (shows main categories) -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
</ui:RadialMenuTextureButton>
</ui:RadialContainer>
<!-- Walls and flooring -->
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Windows and grilles -->
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Airlocks -->
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Computer and machine frames -->
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Lighting -->
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
</ui:RadialMenu>

View File

@@ -0,0 +1,174 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Popups;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client.RCD;
[GenerateTypedNameReferences]
public sealed partial class RCDMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly SharedPopupSystem _popup;
public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
private EntityUid _owner;
public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_spriteSystem = _entManager.System<SpriteSystem>();
_popup = _entManager.System<SharedPopupSystem>();
_owner = owner;
// Find the main radial container
var main = FindControl<RadialContainer>("Main");
if (main == null)
return;
// Populate secondary radial containers
if (!_entManager.TryGetComponent<RCDComponent>(owner, out var rcd))
return;
foreach (var protoId in rcd.AvailablePrototypes)
{
if (!_protoManager.TryIndex(protoId, out var proto))
continue;
if (proto.Mode == RcdMode.Invalid)
continue;
var parent = FindControl<RadialContainer>(proto.Category);
if (parent == null)
continue;
var tooltip = Loc.GetString(proto.SetName);
if ((proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject) &&
proto.Prototype != null && _protoManager.TryIndex(proto.Prototype, out var entProto))
{
tooltip = Loc.GetString(entProto.Name);
}
tooltip = char.ToUpper(tooltip[0]) + tooltip.Remove(0, 1);
var button = new RCDMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = tooltip,
ProtoId = protoId,
};
if (proto.Sprite != null)
{
var tex = new TextureRect()
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = _spriteSystem.Frame0(proto.Sprite),
TextureScale = new Vector2(2f, 2f),
};
button.AddChild(tex);
}
parent.AddChild(button);
// Ensure that the button that transitions the menu to the associated category layer
// is visible in the main radial container (as these all start with Visible = false)
foreach (var child in main.Children)
{
var castChild = child as RadialMenuTextureButton;
if (castChild is not RadialMenuTextureButton)
continue;
if (castChild.TargetLayer == proto.Category)
{
castChild.Visible = true;
break;
}
}
}
// Set up menu actions
foreach (var child in Children)
AddRCDMenuButtonOnClickActions(child);
OnChildAdded += AddRCDMenuButtonOnClickActions;
SendRCDSystemMessageAction += bui.SendRCDSystemMessage;
}
private void AddRCDMenuButtonOnClickActions(Control control)
{
var radialContainer = control as RadialContainer;
if (radialContainer == null)
return;
foreach (var child in radialContainer.Children)
{
var castChild = child as RCDMenuButton;
if (castChild == null)
continue;
castChild.OnButtonUp += _ =>
{
SendRCDSystemMessageAction?.Invoke(castChild.ProtoId);
if (_playerManager.LocalSession?.AttachedEntity != null &&
_protoManager.TryIndex(castChild.ProtoId, out var proto))
{
var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName)));
if (proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject)
{
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
_protoManager.TryIndex(proto.Prototype, out var entProto))
name = entProto.Name;
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
}
// Popup message
_popup.PopupClient(msg, _owner, _playerManager.LocalSession.AttachedEntity);
}
Close();
};
}
}
}
public sealed class RCDMenuButton : RadialMenuTextureButton
{
public ProtoId<RCDPrototype> ProtoId { get; set; }
public RCDMenuButton()
{
}
}

View File

@@ -0,0 +1,49 @@
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Prototypes;
namespace Content.Client.RCD;
[UsedImplicitly]
public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private RCDMenu? _menu;
public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_menu = new(Owner, this);
_menu.OnClose += Close;
// Open the menu, centered on the mouse
var vpSize = _displayManager.ScreenSize;
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
}
public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId)
{
// A predicted message cannot be used here as the RCD UI is closed immediately
// after this message is sent, which will stop the server from receiving it
SendMessage(new RCDSystemMessage(protoId));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
_menu?.Dispose();
}
}

View File

@@ -263,9 +263,10 @@ public sealed partial class MapScreen : BoxContainer
while (mapComps.MoveNext(out var mapComp, out var mapXform, out var mapMetadata))
{
if (!_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId))
continue;
if (_console != null && !_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId, _console.Value))
{
continue;
}
var mapName = mapMetadata.EntityName;
if (string.IsNullOrEmpty(mapName))
@@ -310,7 +311,6 @@ public sealed partial class MapScreen : BoxContainer
};
_mapHeadings.Add(mapComp.MapId, gridContents);
foreach (var grid in _mapManager.GetAllMapGrids(mapComp.MapId))
{
_entManager.TryGetComponent(grid.Owner, out IFFComponent? iffComp);
@@ -327,8 +327,8 @@ public sealed partial class MapScreen : BoxContainer
{
AddMapObject(mapComp.MapId, gridObj);
}
else if (iffComp == null ||
(iffComp.Flags & IFFFlags.Hide) == 0x0)
else if (!_shuttles.IsBeaconMap(_mapManager.GetMapEntityId(mapComp.MapId)) && (iffComp == null ||
(iffComp.Flags & IFFFlags.Hide) == 0x0))
{
_pendingMapObjects.Add((mapComp.MapId, gridObj));
}

View File

@@ -4,6 +4,7 @@ using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using System.Numerics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.StatusIcon;
@@ -11,11 +12,13 @@ namespace Content.Client.StatusIcon;
public sealed class StatusIconOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SpriteSystem _sprite;
private readonly TransformSystem _transform;
private readonly StatusIconSystem _statusIcon;
private readonly ShaderInstance _unshadedShader;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
@@ -26,6 +29,7 @@ public sealed class StatusIconOverlay : Overlay
_sprite = _entity.System<SpriteSystem>();
_transform = _entity.System<TransformSystem>();
_statusIcon = _entity.System<StatusIconSystem>();
_unshadedShader = _prototype.Index<ShaderPrototype>("unshaded").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
@@ -103,9 +107,16 @@ public sealed class StatusIconOverlay : Overlay
}
if (proto.IsShaded)
handle.UseShader(null);
else
handle.UseShader(_unshadedShader);
var position = new Vector2(xOffset, yOffset);
handle.DrawTexture(texture, position);
}
handle.UseShader(null);
}
}
}

View File

@@ -1,22 +1,27 @@
using Content.Shared.Store;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using System.Linq;
using System.Threading;
using Serilog;
using Timer = Robust.Shared.Timing.Timer;
using Robust.Shared.Prototypes;
namespace Content.Client.Store.Ui;
[UsedImplicitly]
public sealed class StoreBoundUserInterface : BoundUserInterface
{
private IPrototypeManager _prototypeManager = default!;
[ViewVariables]
private StoreMenu? _menu;
[ViewVariables]
private string _windowName = Loc.GetString("store-ui-default-title");
[ViewVariables]
private string _search = "";
[ViewVariables]
private HashSet<ListingData> _listings = new();
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -49,6 +54,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
SendMessage(new StoreRequestUpdateInterfaceMessage());
};
_menu.SearchTextUpdated += (_, search) =>
{
_search = search.Trim().ToLowerInvariant();
UpdateListingsWithSearchFilter();
};
_menu.OnRefundAttempt += (_) =>
{
SendMessage(new StoreRequestRefundMessage());
@@ -64,10 +75,10 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
switch (state)
{
case StoreUpdateState msg:
_menu.UpdateBalance(msg.Balance);
_menu.PopulateStoreCategoryButtons(msg.Listings);
_listings = msg.Listings;
_menu.UpdateListing(msg.Listings.ToList());
_menu.UpdateBalance(msg.Balance);
UpdateListingsWithSearchFilter();
_menu.SetFooterVisibility(msg.ShowFooter);
_menu.UpdateRefund(msg.AllowRefund);
break;
@@ -89,4 +100,19 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
_menu?.Close();
_menu?.Dispose();
}
private void UpdateListingsWithSearchFilter()
{
if (_menu == null)
return;
var filteredListings = new HashSet<ListingData>(_listings);
if (!string.IsNullOrEmpty(_search))
{
filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) &&
!ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search));
}
_menu.PopulateStoreCategoryButtons(filteredListings);
_menu.UpdateListing(filteredListings.ToList());
}
}

View File

@@ -28,7 +28,8 @@
HorizontalAlignment="Right"
Text="Refund" />
</BoxContainer>
<PanelContainer VerticalExpand="True">
<LineEdit Name="SearchBar" Margin="4" PlaceHolder="Search" HorizontalExpand="True"/>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>

View File

@@ -1,5 +1,4 @@
using System.Linq;
using System.Threading;
using Content.Client.Actions;
using Content.Client.GameTicking.Managers;
using Content.Client.Message;
@@ -27,6 +26,7 @@ public sealed partial class StoreMenu : DefaultWindow
private StoreWithdrawWindow? _withdrawWindow;
public event EventHandler<string>? SearchTextUpdated;
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
@@ -46,6 +46,7 @@ public sealed partial class StoreMenu : DefaultWindow
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
RefreshButton.OnButtonDown += OnRefreshButtonDown;
RefundButton.OnButtonDown += OnRefundButtonDown;
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
if (Window != null)
Window.Title = name;
@@ -59,7 +60,7 @@ public sealed partial class StoreMenu : DefaultWindow
(type.Key, type.Value), type => _prototypeManager.Index<CurrencyPrototype>(type.Key));
var balanceStr = string.Empty;
foreach (var ((type, amount),proto) in currency)
foreach (var ((_, amount), proto) in currency)
{
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
@@ -81,7 +82,6 @@ public sealed partial class StoreMenu : DefaultWindow
{
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
ClearListings();
@@ -129,8 +129,8 @@ public sealed partial class StoreMenu : DefaultWindow
if (!listing.Categories.Contains(CurrentCategory))
return;
var listingName = Loc.GetString(listing.Name);
var listingDesc = Loc.GetString(listing.Description);
var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager);
var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager);
var listingPrice = listing.Cost;
var canBuy = CanBuyListing(Balance, listingPrice);
@@ -144,12 +144,6 @@ public sealed partial class StoreMenu : DefaultWindow
{
if (texture == null)
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
var proto = _prototypeManager.Index<EntityPrototype>(listing.ProductEntity);
if (listingName == string.Empty)
listingName = proto.Name;
if (listingDesc == string.Empty)
listingDesc = proto.Description;
}
else if (listing.ProductAction != null)
{
@@ -254,13 +248,16 @@ public sealed partial class StoreMenu : DefaultWindow
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
// This will reset the Current Category selection if nothing matches the search.
if (allCategories.All(category => category.ID != CurrentCategory))
CurrentCategory = string.Empty;
if (CurrentCategory == string.Empty && allCategories.Count > 0)
CurrentCategory = allCategories.First().ID;
if (allCategories.Count <= 1)
return;
CategoryListContainer.Children.Clear();
if (allCategories.Count < 1)
return;
foreach (var proto in allCategories)
{

View File

@@ -45,6 +45,7 @@ namespace Content.Client.Stylesheets
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
public const string StyleClassInventorySlotBackground = "InventorySlotBackground";
public const string StyleClassHandSlotHighlight = "HandSlotHighlight";
public const string StyleClassChatPanel = "ChatPanel";
public const string StyleClassChatSubPanel = "ChatSubPanel";
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
public const string StyleClassHotbarPanel = "HotbarPanel";
@@ -158,6 +159,8 @@ namespace Content.Client.Stylesheets
public const string StyleClassButtonColorGreen = "ButtonColorGreen";
public const string StyleClassButtonColorPurple = "ButtonColorPurple";
public static readonly Color ChatBackgroundColor = Color.FromHex("#25252ADD");
public override Stylesheet Stylesheet { get; }
public StyleNano(IResourceCache resCache) : base(resCache)
@@ -354,7 +357,7 @@ namespace Content.Client.Stylesheets
var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var topButtonBase = new StyleBoxTexture
{
Texture = buttonTex,
Texture = buttonTex,
};
topButtonBase.SetPatchMargin(StyleBox.Margin.All, 10);
topButtonBase.SetPadding(StyleBox.Margin.All, 0);
@@ -362,19 +365,19 @@ namespace Content.Client.Stylesheets
var topButtonOpenRight = new StyleBoxTexture(topButtonBase)
{
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
};
topButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0);
var topButtonOpenLeft = new StyleBoxTexture(topButtonBase)
{
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
};
topButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0);
var topButtonSquare = new StyleBoxTexture(topButtonBase)
{
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
};
topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
@@ -410,12 +413,16 @@ namespace Content.Client.Stylesheets
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var chatSubBGTex = resCache.GetTexture("/Textures/Interface/Nano/chat_sub_background.png");
var chatSubBG = new StyleBoxTexture
var chatBg = new StyleBoxFlat
{
Texture = chatSubBGTex,
BackgroundColor = ChatBackgroundColor
};
chatSubBG.SetPatchMargin(StyleBox.Margin.All, 3);
var chatSubBg = new StyleBoxFlat
{
BackgroundColor = ChatBackgroundColor,
};
chatSubBg.SetContentMarginOverride(StyleBox.Margin.All, 3);
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
var actionSearchBox = new StyleBoxTexture
@@ -494,7 +501,7 @@ namespace Content.Client.Stylesheets
// Placeholder
var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png");
var placeholder = new StyleBoxTexture {Texture = placeholderTexture};
var placeholder = new StyleBoxTexture { Texture = placeholderTexture };
placeholder.SetPatchMargin(StyleBox.Margin.All, 19);
placeholder.SetExpandMargin(StyleBox.Margin.All, -5);
placeholder.Mode = StyleBoxTexture.StretchMode.Tile;
@@ -508,7 +515,7 @@ namespace Content.Client.Stylesheets
var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(15, 15, 15)};
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
var itemListItemBackgroundTransparent = new StyleBoxFlat {BackgroundColor = Color.Transparent};
var itemListItemBackgroundTransparent = new StyleBoxFlat { BackgroundColor = Color.Transparent };
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
@@ -578,9 +585,9 @@ namespace Content.Client.Stylesheets
sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 13);
sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 13);
var sliderFillGreen = new StyleBoxTexture(sliderFillBox) {Modulate = Color.LimeGreen};
var sliderFillRed = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Red};
var sliderFillBlue = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Blue};
var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen };
var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red };
var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue };
var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White };
var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
@@ -967,6 +974,13 @@ namespace Content.Client.Stylesheets
Element<TextEdit>().Pseudo(TextEdit.StylePseudoClassPlaceholder)
.Prop("font-color", Color.Gray),
// chat subpanels (chat lineedit backing, popup backings)
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassChatPanel}, null, null),
new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, chatBg),
}),
// Chat lineedit - we don't actually draw a stylebox around the lineedit itself, we put it around the
// input + other buttons, so we must clear the default stylebox
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassChatLineEdit}, null, null),
@@ -975,13 +989,6 @@ namespace Content.Client.Stylesheets
new StyleProperty(LineEdit.StylePropertyStyleBox, new StyleBoxEmpty()),
}),
// chat subpanels (chat lineedit backing, popup backings)
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassChatSubPanel}, null, null),
new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, chatSubBG),
}),
// Action searchbox lineedit
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
new[]
@@ -1622,6 +1629,25 @@ namespace Content.Client.Stylesheets
Element<Label>().Class("Disabled")
.Prop("font-color", DisabledFore),
// Radial menu buttons
Element<TextureButton>().Class("RadialMenuButton")
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_normal.png")),
Element<TextureButton>().Class("RadialMenuButton")
.Pseudo(TextureButton.StylePseudoClassHover)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_hover.png")),
Element<TextureButton>().Class("RadialMenuCloseButton")
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_normal.png")),
Element<TextureButton>().Class("RadialMenuCloseButton")
.Pseudo(TextureButton.StylePseudoClassHover)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_hover.png")),
Element<TextureButton>().Class("RadialMenuBackButton")
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_normal.png")),
Element<TextureButton>().Class("RadialMenuBackButton")
.Pseudo(TextureButton.StylePseudoClassHover)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_hover.png")),
//PDA - Backgrounds
Element<PanelContainer>().Class("PdaContentBackground")
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)

View File

@@ -62,6 +62,8 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
SubscribeLocalEvent<TextScreenVisualsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<TextScreenTimerComponent, ComponentInit>(OnTimerInit);
UpdatesOutsidePrediction = true;
}
private void OnInit(EntityUid uid, TextScreenVisualsComponent component, ComponentInit args)
@@ -110,17 +112,11 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
if (args.AppearanceData.TryGetValue(TextScreenVisuals.Color, out var color) && color is Color)
component.Color = (Color) color;
// DefaultText: broadcast updates from comms consoles
// ScreenText: the text accompanying shuttle timers e.g. "ETA"
// DefaultText: fallback text e.g. broadcast updates from comms consoles
if (args.AppearanceData.TryGetValue(TextScreenVisuals.DefaultText, out var newDefault) && newDefault is string)
{
string?[] defaultText = SegmentText((string) newDefault, component);
component.Text = defaultText;
component.TextToDraw = defaultText;
ResetText(uid, component);
BuildTextLayers(uid, component, args.Sprite);
DrawLayers(uid, component.LayerStatesToDraw);
}
component.Text = SegmentText((string) newDefault, component);
// ScreenText: currently rendered text e.g. the "ETA" accompanying shuttle timers
if (args.AppearanceData.TryGetValue(TextScreenVisuals.ScreenText, out var text) && text is string)
{
component.TextToDraw = SegmentText((string) text, component);

View File

@@ -1,25 +0,0 @@
using Content.Shared.Toilet;
using Robust.Client.GameObjects;
namespace Content.Client.Toilet;
public sealed class ToiletVisualsSystem : VisualizerSystem<ToiletComponent>
{
protected override void OnAppearanceChange(EntityUid uid, ToiletComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null) return;
AppearanceSystem.TryGetData<bool>(uid, ToiletVisuals.LidOpen, out var lidOpen, args.Component);
AppearanceSystem.TryGetData<bool>(uid, ToiletVisuals.SeatUp, out var seatUp, args.Component);
var state = (lidOpen, seatUp) switch
{
(false, false) => "closed_toilet_seat_down",
(false, true) => "closed_toilet_seat_up",
(true, false) => "open_toilet_seat_down",
(true, true) => "open_toilet_seat_up"
};
args.Sprite.LayerSetState(0, state);
}
}

View File

@@ -0,0 +1,105 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using System.Linq;
using System.Numerics;
namespace Content.Client.UserInterface.Controls;
[Virtual]
public class RadialContainer : LayoutContainer
{
/// <summary>
/// Specifies the anglular range, in radians, in which child elements will be placed.
/// The first value denotes the angle at which the first element is to be placed, and
/// the second value denotes the angle at which the last element is to be placed.
/// Both values must be between 0 and 2 PI radians
/// </summary>
/// <remarks>
/// The top of the screen is at 0 radians, and the bottom of the screen is at PI radians
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 AngularRange
{
get
{
return _angularRange;
}
set
{
var x = value.X;
var y = value.Y;
x = x > MathF.Tau ? x % MathF.Tau : x;
y = y > MathF.Tau ? y % MathF.Tau : y;
x = x < 0 ? MathF.Tau + x : x;
y = y < 0 ? MathF.Tau + y : y;
_angularRange = new Vector2(x, y);
}
}
private Vector2 _angularRange = new Vector2(0f, MathF.Tau - float.Epsilon);
/// <summary>
/// Determines the direction in which child elements will be arranged
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
/// <summary>
/// Determines how far from the radial container's center that its child elements will be placed
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float Radius { get; set; } = 100f;
/// <summary>
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool ReserveSpaceForHiddenChildren { get; set; } = true;
/// <summary>
/// This container arranges its children, evenly separated, in a radial pattern
/// </summary>
public RadialContainer()
{
}
protected override void Draw(DrawingHandleScreen handle)
{
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
var childCount = children.Count();
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
var arc = AngularRange.Y - AngularRange.X;
arc = (arc < 0) ? MathF.Tau + arc : arc;
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
// Account for both circular arrangements and arc-based arrangements
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
// Determine the separation between child elements
var sepAngle = arc / (childCount - childMod);
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
// Adjust the positions of all the child elements
foreach (var (i, child) in children.Select((x, i) => (i, x)))
{
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
SetPosition(child, position);
}
}
/// <summary>
/// Specifies the different radial alignment modes
/// </summary>
/// <seealso cref="RadialAlignment"/>
public enum RAlignment : byte
{
Clockwise,
AntiClockwise,
}
}

View File

@@ -0,0 +1,255 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using System.Linq;
using System.Numerics;
namespace Content.Client.UserInterface.Controls;
[Virtual]
public class RadialMenu : BaseWindow
{
/// <summary>
/// Contextual button used to traverse through previous layers of the radial menu
/// </summary>
public TextureButton? ContextualButton { get; set; }
/// <summary>
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
/// </summary>
public string? BackButtonStyleClass
{
get
{
return _backButtonStyleClass;
}
set
{
_backButtonStyleClass = value;
if (_path.Count > 0 && ContextualButton != null && _backButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(_backButtonStyleClass);
}
}
/// <summary>
/// Set a style class to be applied to the contextual button when it will close the radial menu
/// </summary>
public string? CloseButtonStyleClass
{
get
{
return _closeButtonStyleClass;
}
set
{
_closeButtonStyleClass = value;
if (_path.Count == 0 && ContextualButton != null && _closeButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(_closeButtonStyleClass);
}
}
private List<Control> _path = new();
private string? _backButtonStyleClass;
private string? _closeButtonStyleClass;
/// <summary>
/// A free floating menu which enables the quick display of one or more radial containers
/// </summary>
/// <remarks>
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
/// the menu), along with a contextual button at the menu center, which will either return the user
/// to the previous layer or close the menu if there are no previous layers left to traverse.
/// To create a functional radial menu, simply parent one or more named radial containers to it,
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
/// buttons to the name of a radial conatiner will display the container in question to the user
/// whenever it is clicked in additon to any other actions assigned to the button
/// </remarks>
public RadialMenu()
{
// Hide all starting children (if any) except the first (this is the active layer)
if (ChildCount > 1)
{
for (int i = 1; i < ChildCount; i++)
GetChild(i).Visible = false;
}
// Auto generate a contextual button for moving back through visited layers
ContextualButton = new TextureButton()
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
SetSize = new Vector2(64f, 64f),
};
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
AddChild(ContextualButton);
// Hide any further add children, unless its promoted to the active layer
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
}
private Control? GetCurrentActiveLayer()
{
var children = Children.Where(x => x != ContextualButton);
if (!children.Any())
return null;
return children.First(x => x.Visible);
}
public bool TryToMoveToNewLayer(string newLayer)
{
if (newLayer == string.Empty)
return false;
var currentLayer = GetCurrentActiveLayer();
if (currentLayer == null)
return false;
var result = false;
foreach (var child in Children)
{
if (child == ContextualButton)
continue;
// Hide layers which are not of interest
if (result == true || child.Name != newLayer)
{
child.Visible = false;
}
// Show the layer of interest
else
{
child.Visible = true;
result = true;
}
}
// Update the traversal path
if (result)
_path.Add(currentLayer);
// Set the style class of the button
if (_path.Count > 0 && ContextualButton != null && BackButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(BackButtonStyleClass);
return result;
}
public void ReturnToPreviousLayer()
{
// Close the menu if the traversal path is empty
if (_path.Count == 0)
{
Close();
return;
}
var lastChild = _path[^1];
// Hide all children except the contextual button
foreach (var child in Children)
{
if (child != ContextualButton)
child.Visible = false;
}
// Make the last visited layer visible, update the path list
lastChild.Visible = true;
_path.RemoveAt(_path.Count - 1);
// Set the style class of the button
if (_path.Count == 0 && ContextualButton != null && CloseButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(CloseButtonStyleClass);
}
}
[Virtual]
public class RadialMenuButton : Button
{
/// <summary>
/// Upon clicking this button the radial menu will transition to the named layer
/// </summary>
public string? TargetLayer { get; set; }
/// <summary>
/// A simple button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuButton()
{
OnButtonUp += OnClicked;
}
private void OnClicked(ButtonEventArgs args)
{
if (TargetLayer == null || TargetLayer == string.Empty)
return;
var parent = FindParentMultiLayerContainer(this);
if (parent == null)
return;
parent.TryToMoveToNewLayer(TargetLayer);
}
private RadialMenu? FindParentMultiLayerContainer(Control control)
{
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
{
if (ancestor is RadialMenu)
return ancestor as RadialMenu;
}
return null;
}
}
[Virtual]
public class RadialMenuTextureButton : TextureButton
{
/// <summary>
/// Upon clicking this button the radial menu will be moved to the named layer
/// </summary>
public string TargetLayer { get; set; } = string.Empty;
/// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuTextureButton()
{
OnButtonUp += OnClicked;
}
private void OnClicked(ButtonEventArgs args)
{
if (TargetLayer == string.Empty)
return;
var parent = FindParentMultiLayerContainer(this);
if (parent == null)
return;
parent.TryToMoveToNewLayer(TargetLayer);
}
private RadialMenu? FindParentMultiLayerContainer(Control control)
{
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
{
if (ancestor is RadialMenu)
return ancestor as RadialMenu;
}
return null;
}
}

View File

@@ -8,6 +8,7 @@ using Content.Client.Chat.UI;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.Client.Ghost;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Screens;
using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Client.UserInterface.Systems.Gameplay;
@@ -56,7 +57,6 @@ public sealed class ChatUIController : UIController
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntityManager _entities = default!;
[UISystemDependency] private readonly ExamineSystem? _examine = default;
@@ -187,8 +187,8 @@ public sealed class ChatUIController : UIController
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
_net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
_cfg.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
_chatNameColorsEnabled = _cfg.GetCVar(CCVars.ChatEnableColorName);
_config.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
_chatNameColorsEnabled = _config.GetCVar(CCVars.ChatEnableColorName);
_speechBubbleRoot = new LayoutContainer();
@@ -251,6 +251,9 @@ public sealed class ChatUIController : UIController
{
_chatNameColors[i] = nameColors[i].ToHex();
}
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
}
private void OnUpdateChangelingChat(ChangelingUserStart ev)
@@ -271,6 +274,8 @@ public sealed class ChatUIController : UIController
var viewportContainer = UIManager.ActiveScreen!.FindControl<LayoutContainer>("ViewportContainer");
SetSpeechBubbleRoot(viewportContainer);
SetChatWindowOpacity(_config.GetCVar(CCVars.ChatWindowOpacity));
}
public void OnScreenUnload()
@@ -278,6 +283,34 @@ public sealed class ChatUIController : UIController
SetMainChat(false);
}
private void OnChatWindowOpacityChanged(float opacity)
{
SetChatWindowOpacity(opacity);
}
private void SetChatWindowOpacity(float opacity)
{
var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
var panel = chatBox?.ChatWindowPanel;
if (panel is null)
return;
Color color;
if (panel.PanelOverride is StyleBoxFlat styleBoxFlat)
color = styleBoxFlat.BackgroundColor;
else if (panel.TryGetStyleProperty<StyleBox>(PanelContainer.StylePropertyPanel, out var style)
&& style is StyleBoxFlat propStyleBoxFlat)
color = propStyleBoxFlat.BackgroundColor;
else
color = StyleNano.ChatBackgroundColor;
panel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = color.WithAlpha(opacity)
};
}
public void SetMainChat(bool setting)
{
if (UIManager.ActiveScreen == null)
@@ -823,7 +856,7 @@ public sealed class ChatUIController : UIController
ProcessChatMessage(msg);
if ((msg.Channel & ChatChannel.AdminRelated) == 0 ||
_cfg.GetCVar(CCVars.ReplayRecordAdminChat))
_config.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replayRecording.RecordClientMessage(msg);
}
@@ -882,7 +915,7 @@ public sealed class ChatUIController : UIController
break;
case ChatChannel.LOOC:
if (_cfg.GetCVar(CCVars.LoocAboveHeadShow))
if (_config.GetCVar(CCVars.LoocAboveHeadShow))
AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc);
break;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Chat;
using Content.Client.Stylesheets;
using Content.Shared.Chat;
using Content.Shared.Input;
using Robust.Client.UserInterface.Controls;
@@ -44,6 +45,7 @@ public class ChatInputBox : PanelContainer
StyleClasses = {"chatFilterOptionButton"}
};
Container.AddChild(FilterButton);
AddStyleClass(StyleNano.StyleClassChatSubPanel);
ChannelSelector.OnChannelSelect += UpdateActiveChannel;
}

View File

@@ -7,7 +7,8 @@
HorizontalExpand="True"
VerticalExpand="True"
MinSize="465 225">
<PanelContainer HorizontalExpand="True" VerticalExpand="True" StyleClasses="FuckyWuckyBackground" >
<PanelContainer Name="ChatWindowPanel" Access="Public" HorizontalExpand="True" VerticalExpand="True"
StyleClasses="StyleNano.StyleClassChatPanel">
<BoxContainer Orientation="Vertical" SeparationOverride="4" HorizontalExpand="True" VerticalExpand="True">
<OutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="2 2 2 2" >
<OutputPanel.StyleBoxOverride>

View File

@@ -1,4 +1,4 @@
<controls:ItemStatusPanel
<controls:ItemStatusPanel
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
@@ -18,7 +18,7 @@
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" SeparationOverride="0">
<BoxContainer Name="StatusContents" Orientation="Vertical"/>
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus"/>
<Label Name="ItemNameLabel" StyleClasses="ItemStatus"/>
</BoxContainer>
</PanelContainer>
</controls:ItemStatusPanel>

View File

@@ -46,7 +46,7 @@ public sealed class ZombieSystem : EntitySystem
args.Cancelled = true;
}
private void OnCanDisplayStatusIcons(EntityUid uid, InitialInfectedComponent component, CanDisplayStatusIconsEvent args)
private void OnCanDisplayStatusIcons(EntityUid uid, InitialInfectedComponent component, ref CanDisplayStatusIconsEvent args)
{
if (HasComp<InitialInfectedComponent>(args.User) && !HasComp<ZombieComponent>(args.User))
return;

View File

@@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Access
var reader = new AccessReaderComponent();
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new[] { "Foo" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "Bar" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Bar" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
});
// test deny
@@ -67,58 +67,58 @@ namespace Content.IntegrationTests.Tests.Access
reader.DenyTags.Add("A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new[] { "Foo" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "Foo" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "Foo" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
});
// test one list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<string> { "A" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
// test one list - two items
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<string> { "A", "B" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
// test two list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<string> { "A" });
reader.AccessLists.Add(new HashSet<string> { "B", "C" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "B", "C" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "C", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "C", "B", "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
// test deny list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<string> { "A" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
reader.DenyTags.Add("B");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
});
await pair.CleanReturnAsync();

View File

@@ -56,7 +56,7 @@ public sealed class CraftingTests : InteractionTest
// Player's hands should be full of the remaining rods, except those dropped during the failed crafting attempt.
// Spear and left over stacks should be on the floor.
await AssertEntityLookup((Rod, 2), (Cable, 8), (ShardGlass, 2), (Spear, 1));
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
}
// The following is wrapped in an if DEBUG. This is because of cursed state handling bugs. Tests don't (de)serialize
@@ -100,7 +100,7 @@ public sealed class CraftingTests : InteractionTest
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(rodStack, Has.Count.EqualTo(8));
Assert.That(wireStack, Has.Count.EqualTo(8));
Assert.That(wireStack, Has.Count.EqualTo(7));
await FindEntity(Spear, shouldSucceed: false);
});

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Popups;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
@@ -12,6 +13,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.AccessOverriderComponent;
namespace Content.Server.Access.Systems;
@@ -26,6 +28,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
public override void Initialize()
{
@@ -108,17 +111,20 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
var targetLabel = Loc.GetString("access-overrider-window-no-target");
var targetLabelColor = Color.Red;
string[]? possibleAccess = null;
string[]? currentAccess = null;
string[]? missingAccess = null;
ProtoId<AccessLevelPrototype>[]? possibleAccess = null;
ProtoId<AccessLevelPrototype>[]? currentAccess = null;
ProtoId<AccessLevelPrototype>[]? missingAccess = null;
if (component.TargetAccessReaderId is { Valid: true } accessReader)
{
targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName;
targetLabelColor = Color.White;
List<HashSet<string>> currentAccessHashsets = EntityManager.GetComponent<AccessReaderComponent>(accessReader).AccessLists;
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets)?.ToArray();
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderComponent))
return;
var currentAccessHashsets = accessReaderComponent.AccessLists;
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray();
}
if (component.PrivilegedIdSlot.Item is { Valid: true } idCard)
@@ -151,15 +157,15 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
_userInterface.TrySetUiState(uid, AccessOverriderUiKey.Key, newState);
}
private List<string> ConvertAccessHashSetsToList(List<HashSet<string>> accessHashsets)
private List<ProtoId<AccessLevelPrototype>> ConvertAccessHashSetsToList(List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets)
{
List<string> accessList = new List<string>();
List<ProtoId<AccessLevelPrototype>> accessList = new List<ProtoId<AccessLevelPrototype>>();
if (accessHashsets != null && accessHashsets.Any())
{
foreach (HashSet<string> hashSet in accessHashsets)
foreach (HashSet<ProtoId<AccessLevelPrototype>> hashSet in accessHashsets)
{
foreach (string hash in hashSet.ToArray())
foreach (ProtoId<AccessLevelPrototype> hash in hashSet.ToArray())
{
accessList.Add(hash);
}
@@ -169,15 +175,15 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
return accessList;
}
private List<HashSet<string>> ConvertAccessListToHashSet(List<string> accessList)
private List<HashSet<ProtoId<AccessLevelPrototype>>> ConvertAccessListToHashSet(List<ProtoId<AccessLevelPrototype>> accessList)
{
List<HashSet<string>> accessHashsets = new List<HashSet<string>>();
List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets = new List<HashSet<ProtoId<AccessLevelPrototype>>>();
if (accessList != null && accessList.Any())
{
foreach (string access in accessList)
foreach (ProtoId<AccessLevelPrototype> access in accessList)
{
accessHashsets.Add(new HashSet<string>() { access });
accessHashsets.Add(new HashSet<ProtoId<AccessLevelPrototype>>() { access });
}
}
@@ -188,7 +194,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
/// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
/// </summary>
private void TryWriteToTargetAccessReaderId(EntityUid uid,
List<string> newAccessList,
List<ProtoId<AccessLevelPrototype>> newAccessList,
EntityUid player,
AccessOverriderComponent? component = null)
{
@@ -211,9 +217,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
return;
}
TryComp(component.TargetAccessReaderId, out AccessReaderComponent? accessReader);
if (accessReader == null)
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReader))
return;
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists);
@@ -262,10 +266,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
if (!Resolve(uid, ref component))
return true;
if (!EntityManager.TryGetComponent<AccessReaderComponent>(uid, out var reader))
if (_accessReader.GetMainAccessReader(uid, out var accessReader))
return true;
var privilegedId = component.PrivilegedIdSlot.Item;
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader);
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, accessReader);
}
}

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using System.Linq;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
using Content.Shared.Access;
namespace Content.Server.Access.Systems;
@@ -55,11 +56,11 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
return;
var privilegedIdName = string.Empty;
string[]? possibleAccess = null;
List<ProtoId<AccessLevelPrototype>>? possibleAccess = null;
if (component.PrivilegedIdSlot.Item is { Valid: true } item)
{
privilegedIdName = EntityManager.GetComponent<MetaDataComponent>(item).EntityName;
possibleAccess = _accessReader.FindAccessTags(item).ToArray();
possibleAccess = _accessReader.FindAccessTags(item).ToList();
}
IdCardConsoleBoundUserInterfaceState newState;
@@ -84,7 +85,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
var targetIdComponent = EntityManager.GetComponent<IdCardComponent>(targetId);
var targetAccessComponent = EntityManager.GetComponent<AccessComponent>(targetId);
var jobProto = string.Empty;
var jobProto = new ProtoId<AccessLevelPrototype>(string.Empty);
var jobIcon = targetIdComponent.JobIcon; //WD-EDIT
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
@@ -101,7 +102,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
true,
targetIdComponent.FullName,
targetIdComponent.JobTitle,
targetAccessComponent.Tags.ToArray(),
targetAccessComponent.Tags.ToList(),
possibleAccess,
jobProto,
privilegedIdName,
@@ -122,8 +123,8 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
EntityUid uid,
string newFullName,
string newJobTitle,
List<string> newAccessList,
string newJobProto,
List<ProtoId<AccessLevelPrototype>> newAccessList,
ProtoId<AccessLevelPrototype> newJobProto,
string? newJobIcon,
EntityUid player,
IdCardConsoleComponent? component = null)
@@ -153,7 +154,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
return;
}
var oldTags = _access.TryGetTags(targetId) ?? new List<string>();
var oldTags = _access.TryGetTags(targetId) ?? new List<ProtoId<AccessLevelPrototype>>();
oldTags = oldTags.ToList();
var privilegedId = component.PrivilegedIdSlot.Item;
@@ -209,7 +210,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
EntityUid uid,
EntityUid targetId,
string newFullName,
string newJobTitle,
ProtoId<AccessLevelPrototype> newJobTitle,
JobPrototype? newJobProto,
string? newJobIcon) // WD EDIT
{

View File

@@ -1,77 +1,123 @@
using Content.Server.GameTicking;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Ghost;
using Content.Server.Mind;
using Content.Shared.Administration;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Map;
namespace Content.Server.Administration.Commands
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class AGhost : LocalizedCommands
{
[AdminCommand(AdminFlags.Admin)]
public sealed class AGhost : IConsoleCommand
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override string Command => "aghost";
public override string Description => LocalizationManager.GetString("aghost-description");
public override string Help => "aghost";
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
[Dependency] private readonly IEntityManager _entities = default!;
public string Command => "aghost";
public string Description => "Makes you an admin ghost.";
public string Help => "aghost";
public void Execute(IConsoleShell shell, string argStr, string[] args)
if (args.Length == 1)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("Nah");
return;
}
var mindSystem = _entities.System<SharedMindSystem>();
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
{
shell.WriteLine("You can't ghost here!");
return;
}
var metaDataSystem = _entities.System<MetaDataSystem>();
EntityCoordinates? coordinates = null;
if (player.AttachedEntity != null)
coordinates = _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates;
if (mind.VisitingEntity != default && _entities.TryGetComponent<GhostComponent>(mind.VisitingEntity, out var oldGhostComponent))
{
mindSystem.UnVisit(mindId, mind);
// If already an admin ghost, then return to body.
if (oldGhostComponent.CanGhostInteract)
return;
}
var canReturn = mind.CurrentEntity != null
&& !_entities.HasComponent<GhostComponent>(mind.CurrentEntity);
coordinates ??= player.AttachedEntity != null
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates.Value);
_entities.GetComponent<TransformComponent>(ghost).AttachToGridOrMap();
if (canReturn)
{
// TODO: Remove duplication between all this and "GamePreset.OnGhostAttempt()"...
if (!string.IsNullOrWhiteSpace(mind.CharacterName))
metaDataSystem.SetEntityName(ghost, mind.CharacterName);
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
metaDataSystem.SetEntityName(ghost, mind.Session.Name);
mindSystem.Visit(mindId, ghost, mind);
}
else
{
metaDataSystem.SetEntityName(ghost, player.Name);
mindSystem.TransferTo(mindId, ghost, mind: mind);
}
var comp = _entities.GetComponent<GhostComponent>(ghost);
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(comp, canReturn);
var names = _playerManager.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
return CompletionResult.FromHintOptions(names, LocalizationManager.GetString("shell-argument-username-optional-hint"));
}
return CompletionResult.Empty;
}
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
return;
}
var player = shell.Player;
var self = player != null;
if (player == null)
{
// If you are not a player, you require a player argument.
if (args.Length == 0)
{
shell.WriteError(LocalizationManager.GetString("shell-need-exactly-one-argument"));
return;
}
var didFind = _playerManager.TryGetSessionByUsername(args[0], out player);
if (!didFind)
{
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
return;
}
}
// If you are a player and a username is provided, a lookup is done to find the target player.
if (args.Length == 1)
{
var didFind = _playerManager.TryGetSessionByUsername(args[0], out player);
if (!didFind)
{
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
return;
}
}
var mindSystem = _entities.System<SharedMindSystem>();
var metaDataSystem = _entities.System<MetaDataSystem>();
var ghostSystem = _entities.System<SharedGhostSystem>();
var transformSystem = _entities.System<TransformSystem>();
var gameTicker = _entities.System<GameTicker>();
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
{
shell.WriteError(self
? LocalizationManager.GetString("aghost-no-mind-self")
: LocalizationManager.GetString("aghost-no-mind-other"));
return;
}
if (mind.VisitingEntity != default && _entities.TryGetComponent<GhostComponent>(mind.VisitingEntity, out var oldGhostComponent))
{
mindSystem.UnVisit(mindId, mind);
// If already an admin ghost, then return to body.
if (oldGhostComponent.CanGhostInteract)
return;
}
var canReturn = mind.CurrentEntity != null
&& !_entities.HasComponent<GhostComponent>(mind.CurrentEntity);
var coordinates = player!.AttachedEntity != null
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
: gameTicker.GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates);
transformSystem.AttachToGridOrMap(ghost, _entities.GetComponent<TransformComponent>(ghost));
if (canReturn)
{
// TODO: Remove duplication between all this and "GamePreset.OnGhostAttempt()"...
if (!string.IsNullOrWhiteSpace(mind.CharacterName))
metaDataSystem.SetEntityName(ghost, mind.CharacterName);
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
metaDataSystem.SetEntityName(ghost, mind.Session.Name);
mindSystem.Visit(mindId, ghost, mind);
}
else
{
metaDataSystem.SetEntityName(ghost, player.Name);
mindSystem.TransferTo(mindId, ghost, mind: mind);
}
var comp = _entities.GetComponent<GhostComponent>(ghost);
ghostSystem.SetCanReturnToBody(comp, canReturn);
}
}

View File

@@ -35,6 +35,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
@@ -844,14 +845,14 @@ public sealed partial class AdminVerbSystem
{
var allAccess = _prototypeManager
.EnumeratePrototypes<AccessLevelPrototype>()
.Select(p => p.ID).ToArray();
.Select(p => new ProtoId<AccessLevelPrototype>(p.ID)).ToArray();
_accessSystem.TrySetTags(entity, allAccess);
}
private void RevokeAllAccess(EntityUid entity)
{
_accessSystem.TrySetTags(entity, Array.Empty<string>());
_accessSystem.TrySetTags(entity, new List<ProtoId<AccessLevelPrototype>>());
}
public enum TricksVerbPriorities

View File

@@ -1,7 +1,6 @@
using Content.Server.Advertise.EntitySystems;
using Content.Shared.Advertise;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Advertise.Components;
@@ -37,9 +36,4 @@ public sealed partial class AdvertiseComponent : Component
[DataField]
public TimeSpan NextAdvertisementTime { get; set; } = TimeSpan.Zero;
/// <summary>
/// Whether the entity will say advertisements or not.
/// </summary>
[DataField]
public bool Enabled { get; set; } = true;
}

View File

@@ -23,115 +23,83 @@ public sealed class AdvertiseSystem : EntitySystem
/// <summary>
/// The next time the game will check if advertisements should be displayed
/// </summary>
private TimeSpan _nextCheckTime = TimeSpan.MaxValue;
private TimeSpan _nextCheckTime = TimeSpan.MinValue;
public override void Initialize()
{
SubscribeLocalEvent<AdvertiseComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AdvertiseComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ApcPowerReceiverComponent, AdvertiseEnableChangeAttemptEvent>(OnPowerReceiverEnableChangeAttempt);
SubscribeLocalEvent<VendingMachineComponent, AdvertiseEnableChangeAttemptEvent>(OnVendingEnableChangeAttempt);
SubscribeLocalEvent<ApcPowerReceiverComponent, AttemptAdvertiseEvent>(OnPowerReceiverAttemptAdvertiseEvent);
SubscribeLocalEvent<VendingMachineComponent, AttemptAdvertiseEvent>(OnVendingAttemptAdvertiseEvent);
// The component inits will lower this.
_nextCheckTime = TimeSpan.MaxValue;
_nextCheckTime = TimeSpan.MinValue;
}
private void OnMapInit(EntityUid uid, AdvertiseComponent advertise, MapInitEvent args)
private void OnMapInit(EntityUid uid, AdvertiseComponent advert, MapInitEvent args)
{
RefreshTimer(uid, advertise);
RandomizeNextAdvertTime(advert);
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
}
private void OnPowerChanged(EntityUid uid, AdvertiseComponent advertise, ref PowerChangedEvent args)
private void RandomizeNextAdvertTime(AdvertiseComponent advert)
{
SetEnabled(uid, args.Powered, advertise);
}
public void RefreshTimer(EntityUid uid, AdvertiseComponent? advertise = null)
{
if (!Resolve(uid, ref advertise))
return;
if (!advertise.Enabled)
return;
var minDuration = Math.Max(1, advertise.MinimumWait);
var maxDuration = Math.Max(minDuration, advertise.MaximumWait);
var minDuration = Math.Max(1, advert.MinimumWait);
var maxDuration = Math.Max(minDuration, advert.MaximumWait);
var waitDuration = TimeSpan.FromSeconds(_random.Next(minDuration, maxDuration));
var nextTime = _gameTiming.CurTime + waitDuration;
advertise.NextAdvertisementTime = nextTime;
_nextCheckTime = MathHelper.Min(nextTime, _nextCheckTime);
advert.NextAdvertisementTime = _gameTiming.CurTime + waitDuration;
}
public void SayAdvertisement(EntityUid uid, AdvertiseComponent? advertise = null)
public void SayAdvertisement(EntityUid uid, AdvertiseComponent? advert = null)
{
if (!Resolve(uid, ref advertise))
if (!Resolve(uid, ref advert))
return;
if (_prototypeManager.TryIndex(advertise.Pack, out var advertisements))
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Messages)), InGameICChatType.Speak, hideChat: true);
}
public void SetEnabled(EntityUid uid, bool enable, AdvertiseComponent? advertise = null)
{
if (!Resolve(uid, ref advertise))
return;
if (advertise.Enabled == enable)
return;
var attemptEvent = new AdvertiseEnableChangeAttemptEvent(enable);
RaiseLocalEvent(uid, attemptEvent);
var attemptEvent = new AttemptAdvertiseEvent(uid);
RaiseLocalEvent(uid, ref attemptEvent);
if (attemptEvent.Cancelled)
return;
advertise.Enabled = enable;
RefreshTimer(uid, advertise);
}
private static void OnPowerReceiverEnableChangeAttempt(EntityUid uid, ApcPowerReceiverComponent component, AdvertiseEnableChangeAttemptEvent args)
{
if (args.Enabling && !component.Powered)
args.Cancel();
}
private static void OnVendingEnableChangeAttempt(EntityUid uid, VendingMachineComponent component, AdvertiseEnableChangeAttemptEvent args)
{
if (args.Enabling && component.Broken)
args.Cancel();
if (_prototypeManager.TryIndex(advert.Pack, out var advertisements))
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Messages)), InGameICChatType.Speak, hideChat: true);
}
public override void Update(float frameTime)
{
var curTime = _gameTiming.CurTime;
if (_nextCheckTime > curTime)
var currentGameTime = _gameTiming.CurTime;
if (_nextCheckTime > currentGameTime)
return;
_nextCheckTime = curTime + _maximumNextCheckDuration;
// _nextCheckTime starts at TimeSpan.MinValue, so this has to SET the value, not just increment it.
_nextCheckTime = currentGameTime + _maximumNextCheckDuration;
var query = EntityQueryEnumerator<AdvertiseComponent>();
while (query.MoveNext(out var uid, out var advert))
{
if (!advert.Enabled)
continue;
// If this isn't advertising yet
if (advert.NextAdvertisementTime > curTime)
if (currentGameTime > advert.NextAdvertisementTime)
{
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
continue;
SayAdvertisement(uid, advert);
// The timer is always refreshed when it expires, to prevent mass advertising (ex: all the vending machines have no power, and get it back at the same time).
RandomizeNextAdvertTime(advert);
}
SayAdvertisement(uid, advert);
RefreshTimer(uid, advert);
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
}
}
private static void OnPowerReceiverAttemptAdvertiseEvent(EntityUid uid, ApcPowerReceiverComponent powerReceiver, ref AttemptAdvertiseEvent args)
{
args.Cancelled |= !powerReceiver.Powered;
}
private static void OnVendingAttemptAdvertiseEvent(EntityUid uid, VendingMachineComponent machine, ref AttemptAdvertiseEvent args)
{
args.Cancelled |= machine.Broken;
}
}
public sealed class AdvertiseEnableChangeAttemptEvent(bool enabling) : CancellableEntityEventArgs
[ByRefEvent]
public record struct AttemptAdvertiseEvent(EntityUid? Advertiser)
{
public bool Enabling { get; } = enabling;
public bool Cancelled = false;
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Anomaly.Components;
using Content.Server.Anomaly.Components;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.DoAfter;
@@ -21,6 +21,7 @@ public sealed partial class AnomalySystem
SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
SubscribeLocalEvent<AnomalyBehaviorChangedEvent>(OnScannerAnomalyBehaviorChanged);
}
private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
@@ -67,6 +68,17 @@ public sealed partial class AnomalySystem
}
}
private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent args)
{
var query = EntityQueryEnumerator<AnomalyScannerComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (component.ScannedAnomaly != args.Anomaly)
continue;
UpdateScannerUi(uid, component);
}
}
private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
{
UpdateScannerUi(uid, component);
@@ -132,29 +144,95 @@ public sealed partial class AnomalySystem
return msg;
}
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
msg.PushNewline();
string stateLoc;
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
TryComp<SecretDataAnomalyComponent>(anomaly, out var secret);
//Severity
if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity))
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage-unknown"));
else
stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
msg.AddMarkup(stateLoc);
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))));
//Stability
if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability))
msg.AddMarkup(Loc.GetString("anomaly-scanner-stability-unknown"));
else
{
string stateLoc;
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
else
stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
msg.AddMarkup(stateLoc);
}
msg.PushNewline();
//Point output
if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint))
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output-unknown"));
else
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))));
msg.PushNewline();
msg.PushNewline();
//Particles title
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-readout"));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
//Danger
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger))
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger-unknown"));
else
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
//Unstable
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable))
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable-unknown"));
else
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
//Containment
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment))
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment-unknown"));
else
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
msg.PushNewline();
//Transformation
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation))
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-transformation-unknown"));
else
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType))));
//Behavior
msg.PushNewline();
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-behavior-title"));
msg.PushNewline();
if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior))
msg.AddMarkup(Loc.GetString("anomaly-behavior-unknown"));
else
{
if (anomalyComp.CurrentBehavior != null)
{
var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value);
msg.AddMarkup("- " + Loc.GetString(behavior.Description));
msg.PushNewline();
var mod = Math.Floor((behavior.EarnPointModifier) * 100);
msg.AddMarkup("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod)));
}
else
{
msg.AddMarkup(Loc.GetString("anomaly-behavior-balanced"));
}
}
//The timer at the end here is actually added in the ui itself.
return msg;

View File

@@ -8,13 +8,18 @@ using Content.Server.Radio.EntitySystems;
using Content.Server.Station.Systems;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Prototypes;
using Content.Shared.DoAfter;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using System.Linq;
namespace Content.Server.Anomaly;
@@ -33,13 +38,20 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly RadiationSystem _radiation = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly IEntityManager _entity = default!;
public const float MinParticleVariation = 0.8f;
public const float MaxParticleVariation = 1.2f;
[ValidatePrototypeId<WeightedRandomPrototype>]
const string WeightListProto = "AnomalyBehaviorList";
/// <inheritdoc/>
public override void Initialize()
{
@@ -54,25 +66,34 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
InitializeCommands();
}
private void OnMapInit(EntityUid uid, AnomalyComponent component, MapInitEvent args)
private void OnMapInit(Entity<AnomalyComponent> anomaly, ref MapInitEvent args)
{
component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * 3; // longer the first time
ChangeAnomalyStability(uid, Random.NextFloat(component.InitialStabilityRange.Item1 , component.InitialStabilityRange.Item2), component);
ChangeAnomalySeverity(uid, Random.NextFloat(component.InitialSeverityRange.Item1, component.InitialSeverityRange.Item2), component);
anomaly.Comp.NextPulseTime = Timing.CurTime + GetPulseLength(anomaly.Comp) * 3; // longer the first time
ChangeAnomalyStability(anomaly, Random.NextFloat(anomaly.Comp.InitialStabilityRange.Item1 , anomaly.Comp.InitialStabilityRange.Item2), anomaly.Comp);
ChangeAnomalySeverity(anomaly, Random.NextFloat(anomaly.Comp.InitialSeverityRange.Item1, anomaly.Comp.InitialSeverityRange.Item2), anomaly.Comp);
ShuffleParticlesEffect(anomaly.Comp);
anomaly.Comp.Continuity = _random.NextFloat(anomaly.Comp.MinContituty, anomaly.Comp.MaxContituty);
SetBehavior(anomaly, GetRandomBehavior());
}
public void ShuffleParticlesEffect(AnomalyComponent anomaly)
{
var particles = new List<AnomalousParticleType>
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta };
component.SeverityParticleType = Random.PickAndTake(particles);
component.DestabilizingParticleType = Random.PickAndTake(particles);
component.WeakeningParticleType = Random.PickAndTake(particles);
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta, AnomalousParticleType.Sigma };
anomaly.SeverityParticleType = Random.PickAndTake(particles);
anomaly.DestabilizingParticleType = Random.PickAndTake(particles);
anomaly.WeakeningParticleType = Random.PickAndTake(particles);
anomaly.TransformationParticleType = Random.PickAndTake(particles);
}
private void OnShutdown(EntityUid uid, AnomalyComponent component, ComponentShutdown args)
private void OnShutdown(Entity<AnomalyComponent> anomaly, ref ComponentShutdown args)
{
EndAnomaly(uid, component);
EndAnomaly(anomaly);
}
private void OnStartCollide(EntityUid uid, AnomalyComponent component, ref StartCollideEvent args)
private void OnStartCollide(Entity<AnomalyComponent> anomaly, ref StartCollideEvent args)
{
if (!TryComp<AnomalousParticleComponent>(args.OtherEntity, out var particle))
return;
@@ -80,21 +101,33 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
if (args.OtherFixtureId != particle.FixtureId)
return;
var behaviorMod = 1f;
if (anomaly.Comp.CurrentBehavior != null)
{
var b = _prototype.Index(anomaly.Comp.CurrentBehavior.Value);
behaviorMod = b.ParticleSensivity;
}
// small function to randomize because it's easier to read like this
float VaryValue(float v) => v * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
float VaryValue(float v) => v * behaviorMod * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
if (particle.ParticleType == component.DestabilizingParticleType || particle.DestabilzingOverride)
if (particle.ParticleType == anomaly.Comp.DestabilizingParticleType || particle.DestabilzingOverride)
{
ChangeAnomalyStability(uid, VaryValue(particle.StabilityPerDestabilizingHit), component);
ChangeAnomalyStability(anomaly, VaryValue(particle.StabilityPerDestabilizingHit), anomaly.Comp);
}
if (particle.ParticleType == component.SeverityParticleType || particle.SeverityOverride)
if (particle.ParticleType == anomaly.Comp.SeverityParticleType || particle.SeverityOverride)
{
ChangeAnomalySeverity(uid, VaryValue(particle.SeverityPerSeverityHit), component);
ChangeAnomalySeverity(anomaly, VaryValue(particle.SeverityPerSeverityHit), anomaly.Comp);
}
if (particle.ParticleType == component.WeakeningParticleType || particle.WeakeningOverride)
if (particle.ParticleType == anomaly.Comp.WeakeningParticleType || particle.WeakeningOverride)
{
ChangeAnomalyHealth(uid, VaryValue(particle.HealthPerWeakeningeHit), component);
ChangeAnomalyStability(uid, VaryValue(particle.StabilityPerWeakeningeHit), component);
ChangeAnomalyHealth(anomaly, VaryValue(particle.HealthPerWeakeningeHit), anomaly.Comp);
ChangeAnomalyStability(anomaly, VaryValue(particle.StabilityPerWeakeningeHit), anomaly.Comp);
}
if (particle.ParticleType == anomaly.Comp.TransformationParticleType || particle.TransmutationOverride)
{
ChangeAnomalySeverity(anomaly, VaryValue(particle.SeverityPerSeverityHit), anomaly.Comp);
if (_random.Prob(anomaly.Comp.Continuity))
SetBehavior(anomaly, GetRandomBehavior());
}
}
@@ -116,6 +149,13 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
//penalty of up to 50% based on health
multiplier *= MathF.Pow(1.5f, component.Health) - 0.5f;
//Apply behavior modifier
if (component.CurrentBehavior != null)
{
var behavior = _prototype.Index(component.CurrentBehavior.Value);
multiplier *= behavior.EarnPointModifier;
}
var severityValue = 1 / (1 + MathF.Pow(MathF.E, -7 * (component.Severity - 0.5f)));
return (int) ((component.MaxPointsPerSecond - component.MinPointsPerSecond) * severityValue * multiplier) + component.MinPointsPerSecond;
@@ -133,6 +173,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
AnomalousParticleType.Delta => Loc.GetString("anomaly-particles-delta"),
AnomalousParticleType.Epsilon => Loc.GetString("anomaly-particles-epsilon"),
AnomalousParticleType.Zeta => Loc.GetString("anomaly-particles-zeta"),
AnomalousParticleType.Sigma => Loc.GetString("anomaly-particles-sigma"),
_ => throw new ArgumentOutOfRangeException()
};
}
@@ -144,4 +185,40 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
UpdateGenerator();
UpdateVessels();
}
#region Behavior
private string GetRandomBehavior()
{
var weightList = _prototype.Index<WeightedRandomPrototype>(WeightListProto);
return weightList.Pick(_random);
}
private void SetBehavior(Entity<AnomalyComponent> anomaly, ProtoId<AnomalyBehaviorPrototype> behaviorProto)
{
if (anomaly.Comp.CurrentBehavior == behaviorProto)
return;
if (anomaly.Comp.CurrentBehavior != null)
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
//event broadcast
var ev = new AnomalyBehaviorChangedEvent(anomaly, anomaly.Comp.CurrentBehavior, behaviorProto);
anomaly.Comp.CurrentBehavior = behaviorProto;
RaiseLocalEvent(anomaly, ref ev, true);
var behavior = _prototype.Index(behaviorProto);
EntityManager.AddComponents(anomaly, behavior.Components);
}
private void RemoveBehavior(Entity<AnomalyComponent> anomaly, ProtoId<AnomalyBehaviorPrototype> behaviorProto)
{
if (anomaly.Comp.CurrentBehavior == null)
return;
var behavior = _prototype.Index(anomaly.Comp.CurrentBehavior.Value);
EntityManager.RemoveComponents(anomaly, behavior.Components);
}
#endregion
}

View File

@@ -13,58 +13,64 @@ public sealed partial class AnomalousParticleComponent : Component
/// The type of particle that the projectile
/// imbues onto the anomaly on contact.
/// </summary>
[DataField("particleType", required: true)]
[DataField(required: true)]
public AnomalousParticleType ParticleType;
/// <summary>
/// The fixture that's checked on collision.
/// </summary>
[DataField("fixtureId")]
[DataField]
public string FixtureId = "projectile";
/// <summary>
/// The amount that the <see cref="AnomalyComponent.Severity"/> increases by when hit
/// of an anomalous particle of <seealso cref="AnomalyComponent.SeverityParticleType"/>.
/// </summary>
[DataField("severityPerSeverityHit")]
[DataField]
public float SeverityPerSeverityHit = 0.025f;
/// <summary>
/// The amount that the <see cref="AnomalyComponent.Stability"/> increases by when hit
/// of an anomalous particle of <seealso cref="AnomalyComponent.DestabilizingParticleType"/>.
/// </summary>
[DataField("stabilityPerDestabilizingHit")]
[DataField]
public float StabilityPerDestabilizingHit = 0.04f;
/// <summary>
/// The amount that the <see cref="AnomalyComponent.Stability"/> increases by when hit
/// of an anomalous particle of <seealso cref="AnomalyComponent.DestabilizingParticleType"/>.
/// </summary>
[DataField("healthPerWeakeningeHit")]
[DataField]
public float HealthPerWeakeningeHit = -0.05f;
/// <summary>
/// The amount that the <see cref="AnomalyComponent.Stability"/> increases by when hit
/// of an anomalous particle of <seealso cref="AnomalyComponent.DestabilizingParticleType"/>.
/// </summary>
[DataField("stabilityPerWeakeningeHit")]
[DataField]
public float StabilityPerWeakeningeHit = -0.1f;
/// <summary>
/// If this is true then the particle will always affect the stability of the anomaly.
/// </summary>
[DataField("destabilzingOverride")]
[DataField]
public bool DestabilzingOverride = false;
/// <summary>
/// If this is true then the particle will always affect the weakeness of the anomaly.
/// </summary>
[DataField("weakeningOverride")]
[DataField]
public bool WeakeningOverride = false;
/// <summary>
/// If this is true then the particle will always affect the severity of the anomaly.
/// </summary>
[DataField("severityOverride")]
[DataField]
public bool SeverityOverride = false;
/// <summary>
/// If this is true then the particle will always affect the behaviour.
/// </summary>
[DataField]
public bool TransmutationOverride = false;
}

View File

@@ -0,0 +1,45 @@
using Content.Server.Anomaly.Effects;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// Hides some information about the anomaly when scanning it
/// </summary>
[RegisterComponent, Access(typeof(SecretDataAnomalySystem), typeof(AnomalySystem))]
public sealed partial class SecretDataAnomalyComponent : Component
{
/// <summary>
/// Minimum hidden data elements on MapInit
/// </summary>
[DataField]
public int RandomStartSecretMin = 0;
/// <summary>
/// Maximum hidden data elements on MapInit
/// </summary>
[DataField]
public int RandomStartSecretMax = 0;
/// <summary>
/// Current secret data
/// </summary>
[DataField]
public List<AnomalySecretData> Secret = new();
}
/// <summary>
/// Enum with secret data field variants
/// </summary>
[Serializable]
public enum AnomalySecretData : byte
{
Severity,
Stability,
OutputPoint,
ParticleDanger,
ParticleUnstable,
ParticleContainment,
ParticleTransformation,
Behavior,
Default
}

View File

@@ -0,0 +1,28 @@
using Content.Server.Anomaly.Effects;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// Shuffle Particle types in some situations
/// </summary>
[RegisterComponent, Access(typeof(ShuffleParticlesAnomalySystem))]
public sealed partial class ShuffleParticlesAnomalyComponent : Component
{
/// <summary>
/// Prob() chance to randomize particle types after Anomaly pulation
/// </summary>
[DataField]
public bool ShuffleOnPulse = false;
/// <summary>
/// Prob() chance to randomize particle types after APE or CHIMP projectile
/// </summary>
[DataField]
public bool ShuffleOnParticleHit = false;
/// <summary>
/// Chance to random particles
/// </summary>
[DataField]
public float Prob = 0.5f;
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using Content.Server.Anomaly.Components;
using Content.Shared.Administration.Logs;
@@ -33,7 +33,7 @@ public sealed class BluespaceAnomalySystem : EntitySystem
{
var xformQuery = GetEntityQuery<TransformComponent>();
var xform = xformQuery.GetComponent(uid);
var range = component.MaxShuffleRadius * args.Severity;
var range = component.MaxShuffleRadius * args.Severity * args.PowerModifier;
var mobs = new HashSet<Entity<MobStateComponent>>();
_lookup.GetEntitiesInRange(xform.Coordinates, range, mobs);
var allEnts = new ValueList<EntityUid>(mobs.Select(m => m.Owner)) { uid };
@@ -56,7 +56,7 @@ public sealed class BluespaceAnomalySystem : EntitySystem
{
var xform = Transform(uid);
var mapPos = _xform.GetWorldPosition(xform);
var radius = component.SupercriticalTeleportRadius;
var radius = component.SupercriticalTeleportRadius * args.PowerModifier;
var gridBounds = new Box2(mapPos - new Vector2(radius, radius), mapPos + new Vector2(radius, radius));
var mobs = new HashSet<Entity<MobStateComponent>>();
_lookup.GetEntitiesInRange(xform.Coordinates, component.MaxShuffleRadius, mobs);

View File

@@ -28,7 +28,7 @@ public sealed class ElectricityAnomalySystem : EntitySystem
private void OnPulse(Entity<ElectricityAnomalyComponent> anomaly, ref AnomalyPulseEvent args)
{
var range = anomaly.Comp.MaxElectrocuteRange * args.Stability;
var range = anomaly.Comp.MaxElectrocuteRange * args.Stability * args.PowerModifier;
int boltCount = (int)MathF.Floor(MathHelper.Lerp((float)anomaly.Comp.MinBoltCount, (float)anomaly.Comp.MaxBoltCount, args.Severity));
@@ -37,7 +37,7 @@ public sealed class ElectricityAnomalySystem : EntitySystem
private void OnSupercritical(Entity<ElectricityAnomalyComponent> anomaly, ref AnomalySupercriticalEvent args)
{
var range = anomaly.Comp.MaxElectrocuteRange * 3;
var range = anomaly.Comp.MaxElectrocuteRange * 3 * args.PowerModifier;
_emp.EmpPulse(_transform.GetMapCoordinates(anomaly), range, anomaly.Comp.EmpEnergyConsumption, anomaly.Comp.EmpDisabledDuration);
_lightning.ShootRandomLightnings(anomaly, range, anomaly.Comp.MaxBoltCount * 3, arcDepth: 3);

View File

@@ -35,7 +35,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
if (!entry.Settings.SpawnOnPulse)
continue;
SpawnEntities(component, entry, args.Stability, args.Severity);
SpawnEntities(component, entry, args.Stability, args.Severity, args.PowerModifier);
}
}
@@ -46,7 +46,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
if (!entry.Settings.SpawnOnSuperCritical)
continue;
SpawnEntities(component, entry, 1, 1);
SpawnEntities(component, entry, 1, 1, args.PowerModifier);
}
}
@@ -57,7 +57,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
if (!entry.Settings.SpawnOnShutdown || args.Supercritical)
continue;
SpawnEntities(component, entry, 1, 1);
SpawnEntities(component, entry, 1, 1, 1);
}
}
@@ -68,7 +68,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
if (!entry.Settings.SpawnOnStabilityChanged)
continue;
SpawnEntities(component, entry, args.Stability, args.Severity);
SpawnEntities(component, entry, args.Stability, args.Severity, 1);
}
}
@@ -79,17 +79,17 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
if (!entry.Settings.SpawnOnSeverityChanged)
continue;
SpawnEntities(component, entry, args.Stability, args.Severity);
SpawnEntities(component, entry, args.Stability, args.Severity, 1);
}
}
private void SpawnEntities(Entity<EntitySpawnAnomalyComponent> anomaly, EntitySpawnSettingsEntry entry, float stability, float severity)
private void SpawnEntities(Entity<EntitySpawnAnomalyComponent> anomaly, EntitySpawnSettingsEntry entry, float stability, float severity, float powerMod)
{
var xform = Transform(anomaly);
if (!TryComp(xform.GridUid, out MapGridComponent? grid))
return;
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings);
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings, powerMod);
if (tiles == null)
return;

View File

@@ -29,12 +29,12 @@ public sealed class InjectionAnomalySystem : EntitySystem
private void OnPulse(Entity<InjectionAnomalyComponent> entity, ref AnomalyPulseEvent args)
{
PulseScalableEffect(entity, entity.Comp.InjectRadius, entity.Comp.MaxSolutionInjection * args.Severity);
PulseScalableEffect(entity, entity.Comp.InjectRadius * args.PowerModifier, entity.Comp.MaxSolutionInjection * args.Severity * args.PowerModifier);
}
private void OnSupercritical(Entity<InjectionAnomalyComponent> entity, ref AnomalySupercriticalEvent args)
{
PulseScalableEffect(entity, entity.Comp.SuperCriticalInjectRadius, entity.Comp.SuperCriticalSolutionInjection);
PulseScalableEffect(entity, entity.Comp.SuperCriticalInjectRadius * args.PowerModifier, entity.Comp.SuperCriticalSolutionInjection * args.PowerModifier);
}
private void PulseScalableEffect(Entity<InjectionAnomalyComponent> entity, float injectRadius, float maxInject)

View File

@@ -31,12 +31,12 @@ public sealed class ProjectileAnomalySystem : EntitySystem
private void OnPulse(EntityUid uid, ProjectileAnomalyComponent component, ref AnomalyPulseEvent args)
{
ShootProjectilesAtEntities(uid, component, args.Severity);
ShootProjectilesAtEntities(uid, component, args.Severity * args.PowerModifier);
}
private void OnSupercritical(EntityUid uid, ProjectileAnomalyComponent component, ref AnomalySupercriticalEvent args)
{
ShootProjectilesAtEntities(uid, component, 1.0f);
ShootProjectilesAtEntities(uid, component, args.PowerModifier);
}
private void ShootProjectilesAtEntities(EntityUid uid, ProjectileAnomalyComponent component, float severity)

View File

@@ -25,9 +25,10 @@ public sealed class PuddleCreateAnomalySystem : EntitySystem
return;
var xform = Transform(entity.Owner);
var puddleSol = _solutionContainer.SplitSolution(sol.Value, entity.Comp.MaxPuddleSize * args.Severity);
var puddleSol = _solutionContainer.SplitSolution(sol.Value, entity.Comp.MaxPuddleSize * args.Severity * args.PowerModifier);
_puddle.TrySplashSpillAt(entity.Owner, xform.Coordinates, puddleSol, out _);
}
private void OnSupercritical(Entity<PuddleCreateAnomalyComponent> entity, ref AnomalySupercriticalEvent args)
{
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var sol))

View File

@@ -1,4 +1,4 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects.Components;
@@ -24,14 +24,14 @@ public sealed class PyroclasticAnomalySystem : EntitySystem
private void OnPulse(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalyPulseEvent args)
{
var xform = Transform(uid);
var ignitionRadius = component.MaximumIgnitionRadius * args.Stability;
var ignitionRadius = component.MaximumIgnitionRadius * args.Stability * args.PowerModifier;
IgniteNearby(uid, xform.Coordinates, args.Severity, ignitionRadius);
}
private void OnSupercritical(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalySupercriticalEvent args)
{
var xform = Transform(uid);
IgniteNearby(uid, xform.Coordinates, 1, component.MaximumIgnitionRadius * 2);
IgniteNearby(uid, xform.Coordinates, 1, component.MaximumIgnitionRadius * 2 * args.PowerModifier);
}
public void IgniteNearby(EntityUid uid, EntityCoordinates coordinates, float severity, float radius)

View File

@@ -0,0 +1,40 @@
using Content.Server.Anomaly.Components;
using Robust.Shared.Random;
namespace Content.Server.Anomaly.Effects;
public sealed class SecretDataAnomalySystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
private readonly List<AnomalySecretData> _deita = new();
public override void Initialize()
{
SubscribeLocalEvent<SecretDataAnomalyComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, SecretDataAnomalyComponent anomaly, MapInitEvent args)
{
RandomizeSecret(uid,_random.Next(anomaly.RandomStartSecretMin, anomaly.RandomStartSecretMax), anomaly);
}
public void RandomizeSecret(EntityUid uid, int count, SecretDataAnomalyComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Secret.Clear();
// I also considered just adding all the enum values and pruning but that seems more wasteful.
_deita.Clear();
_deita.AddRange(Enum.GetValues<AnomalySecretData>());
var actualCount = Math.Min(count, _deita.Count);
for (int i = 0; i < actualCount; i++)
{
component.Secret.Add(_random.PickAndTake(_deita));
}
}
}

View File

@@ -0,0 +1,41 @@
using Content.Server.Anomaly.Components;
using Content.Shared.Anomaly.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Random;
namespace Content.Server.Anomaly.Effects;
public sealed class ShuffleParticlesAnomalySystem : EntitySystem
{
[Dependency] private readonly AnomalySystem _anomaly = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
SubscribeLocalEvent<ShuffleParticlesAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<ShuffleParticlesAnomalyComponent, StartCollideEvent>(OnStartCollide);
}
private void OnStartCollide(EntityUid uid, ShuffleParticlesAnomalyComponent shuffle, StartCollideEvent args)
{
if (!TryComp<AnomalyComponent>(uid, out var anomaly))
return;
if (shuffle.ShuffleOnParticleHit && _random.Prob(shuffle.Prob))
_anomaly.ShuffleParticlesEffect(anomaly);
if (!TryComp<AnomalousParticleComponent>(args.OtherEntity, out var particle))
return;
}
private void OnPulse(EntityUid uid, ShuffleParticlesAnomalyComponent shuffle, AnomalyPulseEvent args)
{
if (!TryComp<AnomalyComponent>(uid, out var anomaly))
return;
if (shuffle.ShuffleOnPulse && _random.Prob(shuffle.Prob))
{
_anomaly.ShuffleParticlesEffect(anomaly);
}
}
}

View File

@@ -34,7 +34,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
if (!entry.Settings.SpawnOnPulse)
continue;
SpawnTiles(component, entry, args.Stability, args.Severity);
SpawnTiles(component, entry, args.Stability, args.Severity, args.PowerModifier);
}
}
@@ -45,7 +45,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
if (!entry.Settings.SpawnOnSuperCritical)
continue;
SpawnTiles(component, entry, 1, 1);
SpawnTiles(component, entry, 1, 1, args.PowerModifier);
}
}
@@ -56,7 +56,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
if (!entry.Settings.SpawnOnShutdown || args.Supercritical)
continue;
SpawnTiles(component, entry, 1, 1);
SpawnTiles(component, entry, 1, 1, 1);
}
}
@@ -67,7 +67,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
if (!entry.Settings.SpawnOnStabilityChanged)
continue;
SpawnTiles(component, entry, args.Stability, args.Severity);
SpawnTiles(component, entry, args.Stability, args.Severity, 1);
}
}
@@ -78,17 +78,17 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
if (!entry.Settings.SpawnOnSeverityChanged)
continue;
SpawnTiles(component, entry, args.Stability, args.Severity);
SpawnTiles(component, entry, args.Stability, args.Severity, 1);
}
}
private void SpawnTiles(Entity<TileSpawnAnomalyComponent> anomaly, TileSpawnSettingsEntry entry, float stability, float severity)
private void SpawnTiles(Entity<TileSpawnAnomalyComponent> anomaly, TileSpawnSettingsEntry entry, float stability, float severity, float powerMod)
{
var xform = Transform(anomaly);
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return;
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings);
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings, powerMod);
if (tiles == null)
return;

View File

@@ -432,14 +432,16 @@ namespace Content.Server.Atmos.EntitySystems
if (!overlay.Chunks.TryGetValue(gIndex, out var value))
continue;
if (previousChunks != null &&
previousChunks.Contains(gIndex) &&
value.LastUpdate > LastSessionUpdate)
// If the chunk was updated since we last sent it, send it again
if (value.LastUpdate > LastSessionUpdate)
{
dataToSend.Add(value);
continue;
}
dataToSend.Add(value);
// Always send it if we didn't previously send it
if (previousChunks == null || !previousChunks.Contains(gIndex))
dataToSend.Add(value);
}
previouslySent[netGrid] = gridChunks;

View File

@@ -53,7 +53,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
private void OnFilterUpdated(EntityUid uid, GasFilterComponent filter, ref AtmosDeviceUpdateEvent args)
{
if (!filter.Enabled
|| !_nodeContainer.TryGetNodes(uid, filter.InletName, filter.OutletName, filter.FilterName, out PipeNode? inletNode, out PipeNode? filterNode, out PipeNode? outletNode)
|| !_nodeContainer.TryGetNodes(uid, filter.InletName, filter.FilterName, filter.OutletName, out PipeNode? inletNode, out PipeNode? filterNode, out PipeNode? outletNode)
|| outletNode.Air.Pressure >= Atmospherics.MaxOutputPressure) // No need to transfer if target is full.
{
_ambientSoundSystem.SetAmbience(uid, false);

View File

@@ -1,4 +1,5 @@
using Content.Shared.Cargo;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Cargo.Components;
@@ -32,4 +33,16 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
/// </summary>
[DataField]
public HashSet<string> CheckedBounties = new();
/// <summary>
/// The time at which players will be able to skip the next bounty.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextSkipTime = TimeSpan.Zero;
/// <summary>
/// The time between skipping bounties.
/// </summary>
[DataField]
public TimeSpan SkipDelay = TimeSpan.FromMinutes(15);
}

View File

@@ -4,7 +4,7 @@ using Content.Server.Cargo.Components;
using Content.Server.Labels;
using Content.Server.NameIdentifier;
using Content.Server.Paper;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
@@ -35,6 +35,7 @@ public sealed partial class CargoSystem
{
SubscribeLocalEvent<CargoBountyConsoleComponent, BoundUIOpenedEvent>(OnBountyConsoleOpened);
SubscribeLocalEvent<CargoBountyConsoleComponent, BountyPrintLabelMessage>(OnPrintLabelMessage);
SubscribeLocalEvent<CargoBountyConsoleComponent, BountySkipMessage>(OnSkipBountyMessage);
SubscribeLocalEvent<CargoBountyLabelComponent, PriceCalculationEvent>(OnGetBountyPrice);
SubscribeLocalEvent<EntitySoldEvent>(OnSold);
SubscribeLocalEvent<StationCargoBountyDatabaseComponent, MapInitEvent>(OnMapInit);
@@ -50,7 +51,8 @@ public sealed partial class CargoSystem
!TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDb))
return;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties));
var untilNextSkip = bountyDb.NextSkipTime - _timing.CurTime;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, untilNextSkip));
}
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
@@ -70,6 +72,37 @@ public sealed partial class CargoSystem
_audio.PlayPvs(component.PrintSound, uid);
}
private void OnSkipBountyMessage(EntityUid uid, CargoBountyConsoleComponent component, BountySkipMessage args)
{
if (_station.GetOwningStation(uid) is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
return;
if (_timing.CurTime < db.NextSkipTime)
return;
if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
return;
if (args.Session.AttachedEntity is not { Valid: true } mob)
return;
if (TryComp<AccessReaderComponent>(uid, out var accessReaderComponent) &&
!_accessReaderSystem.IsAllowed(mob, uid, accessReaderComponent))
{
_audio.PlayPvs(component.DenySound, uid);
return;
}
if (!TryRemoveBounty(station, bounty.Value))
return;
FillBountyDatabase(station);
db.NextSkipTime = _timing.CurTime + db.SkipDelay;
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
_audio.PlayPvs(component.SkipSound, uid);
}
public void SetupBountyLabel(EntityUid uid, EntityUid stationId, CargoBountyData bounty, PaperComponent? paper = null, CargoBountyLabelComponent? label = null)
{
if (!Resolve(uid, ref paper, ref label) || !_protoMan.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var prototype))
@@ -431,7 +464,8 @@ public sealed partial class CargoSystem
!TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
continue;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties), ui: ui);
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip), ui: ui);
}
}

View File

@@ -312,8 +312,7 @@ namespace Content.Server.Chat.Managers
var prefs = _preferencesManager.GetPreferences(player.UserId);
colorOverride = prefs.AdminOOCColor;
}
if (player.Channel.UserData.PatronTier is { } patron &&
PatronOocColors.TryGetValue(patron, out var patronColor))
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
{
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)), ("rep", reputation));
}

View File

@@ -0,0 +1,60 @@
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Base class for components that inject a solution into a target's bloodstream in response to an event.
/// </summary>
public abstract partial class BaseSolutionInjectOnEventComponent : Component
{
/// <summary>
/// How much solution to remove from this entity per target when transferring.
/// </summary>
/// <remarks>
/// Note that this amount is per target, so the total amount removed will be
/// multiplied by the number of targets hit.
/// </remarks>
[DataField]
public FixedPoint2 TransferAmount = FixedPoint2.New(1);
[ViewVariables(VVAccess.ReadWrite)]
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
/// <summary>
/// Proportion of the <see cref="TransferAmount"/> that will actually be injected
/// into the target's bloodstream. The rest is lost.
/// 0 means none of the transferred solution will enter the bloodstream.
/// 1 means the entire amount will enter the bloodstream.
/// </summary>
[DataField("transferEfficiency")]
private float _transferEfficiency = 1f;
/// <summary>
/// Solution to inject from.
/// </summary>
[DataField]
public string Solution = "default";
/// <summary>
/// Whether this will inject through hardsuits or not.
/// </summary>
[DataField]
public bool PierceArmor = true;
/// <summary>
/// Contents of popup message to display to the attacker when injection
/// fails due to the target wearing a hardsuit.
/// </summary>
/// <remarks>
/// Passed values: $weapon and $target
/// </remarks>
[DataField]
public LocId BlockedByHardsuitPopupMessage = "melee-inject-failed-hardsuit";
/// <summary>
/// If anything covers any of these slots then the injection fails.
/// </summary>
[DataField]
public SlotFlags BlockSlots = SlotFlags.NONE;
}

View File

@@ -1,31 +1,8 @@
using Content.Shared.FixedPoint;
namespace Content.Server.Chemistry.Components;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public sealed partial class MeleeChemicalInjectorComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("transferAmount")]
public FixedPoint2 TransferAmount { get; set; } = FixedPoint2.New(1);
[ViewVariables(VVAccess.ReadWrite)]
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
[DataField("transferEfficiency")]
private float _transferEfficiency = 1f;
/// <summary>
/// Whether this will inject through hardsuits or not.
/// </summary>
[DataField("pierceArmor"), ViewVariables(VVAccess.ReadWrite)]
public bool PierceArmor = true;
/// <summary>
/// Solution to inject from.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("solution")]
public string Solution { get; set; } = "default";
}
}
/// <summary>
/// Used for melee weapon entities that should try to inject a
/// contained solution into a target when used to hit it.
/// </summary>
[RegisterComponent]
public sealed partial class MeleeChemicalInjectorComponent : BaseSolutionInjectOnEventComponent { }

View File

@@ -1,28 +0,0 @@
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Projectiles;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// On colliding with an entity that has a bloodstream will dump its solution onto them.
/// </summary>
[RegisterComponent]
public sealed partial class SolutionInjectOnCollideComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("transferAmount")]
public FixedPoint2 TransferAmount = FixedPoint2.New(1);
[ViewVariables(VVAccess.ReadWrite)]
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
[DataField("transferEfficiency")]
private float _transferEfficiency = 1f;
/// <summary>
/// If anything covers any of these slots then the injection fails.
/// </summary>
[DataField("blockSlots"), ViewVariables(VVAccess.ReadWrite)]
public SlotFlags BlockSlots = SlotFlags.MASK;
}

View File

@@ -0,0 +1,8 @@
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Used for embeddable entities that should try to inject a
/// contained solution into a target when they become embedded in it.
/// </summary>
[RegisterComponent]
public sealed partial class SolutionInjectOnEmbedComponent : BaseSolutionInjectOnEventComponent { }

View File

@@ -0,0 +1,8 @@
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Used for projectile entities that should try to inject a
/// contained solution into a target when they hit it.
/// </summary>
[RegisterComponent]
public sealed partial class SolutionInjectOnProjectileHitComponent : BaseSolutionInjectOnEventComponent { }

View File

@@ -1,49 +0,0 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Projectiles;
namespace Content.Server.Chemistry.EntitySystems;
public sealed class SolutionInjectOnCollideSystem : EntitySystem
{
[Dependency] private readonly SolutionContainerSystem _solutionContainersSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionInjectOnCollideComponent, ProjectileHitEvent>(HandleInjection);
}
private void HandleInjection(Entity<SolutionInjectOnCollideComponent> ent, ref ProjectileHitEvent args)
{
var component = ent.Comp;
var target = args.Target;
if (!TryComp<BloodstreamComponent>(target, out var bloodstream) ||
!_solutionContainersSystem.TryGetInjectableSolution(ent.Owner, out var solution, out _))
{
return;
}
if (component.BlockSlots != 0x0)
{
var containerEnumerator = _inventorySystem.GetSlotEnumerator(target, component.BlockSlots);
// TODO add a helper method for this?
if (containerEnumerator.MoveNext(out _))
return;
}
var solRemoved = _solutionContainersSystem.SplitSolution(solution.Value, component.TransferAmount);
var solRemovedVol = solRemoved.Volume;
var solToInject = solRemoved.SplitSolution(solRemovedVol * component.TransferEfficiency);
_bloodstreamSystem.TryAddToChemicals(target, solToInject, bloodstream);
}
}

View File

@@ -0,0 +1,148 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Collections;
namespace Content.Server.Chemistry.EntitySystems;
/// <summary>
/// System for handling the different inheritors of <see cref="BaseSolutionInjectOnEventComponent"/>.
/// Subscribes to relevent events and performs solution injections when they are raised.
/// </summary>
public sealed class SolutionInjectOnCollideSystem : EntitySystem
{
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
}
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
{
DoInjection((entity.Owner, entity.Comp), args.Target, args.Shooter);
}
private void HandleEmbed(Entity<SolutionInjectOnEmbedComponent> entity, ref EmbedEvent args)
{
DoInjection((entity.Owner, entity.Comp), args.Embedded, args.Shooter);
}
private void HandleMeleeHit(Entity<MeleeChemicalInjectorComponent> entity, ref MeleeHitEvent args)
{
// MeleeHitEvent is weird, so we have to filter to make sure we actually
// hit something and aren't just examining the weapon.
if (args.IsHit)
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
}
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
{
TryInjectTargets(injectorEntity, [target], source);
}
/// <summary>
/// Filters <paramref name="targets"/> for valid targets and tries to inject a portion of <see cref="BaseSolutionInjectOnEventComponent.Solution"/> into
/// each valid target's bloodstream.
/// </summary>
/// <remarks>
/// Targets are invalid if any of the following are true:
/// <list type="bullet">
/// <item>The target does not have a bloodstream.</item>
/// <item><see cref="BaseSolutionInjectOnEventComponent.PierceArmor"/> is false and the target is wearing a hardsuit.</item>
/// <item><see cref="BaseSolutionInjectOnEventComponent.BlockSlots"/> is not NONE and the target has an item equipped in any of the specified slots.</item>
/// </list>
/// </remarks>
/// <returns>true if at least one target was successfully injected, otherwise false</returns>
private bool TryInjectTargets(Entity<BaseSolutionInjectOnEventComponent> injector, IReadOnlyList<EntityUid> targets, EntityUid? source = null)
{
// Make sure we have at least one target
if (targets.Count == 0)
return false;
// Get the solution to inject
if (!_solutionContainer.TryGetSolution(injector.Owner, injector.Comp.Solution, out var injectorSolution))
return false;
// Build a list of bloodstreams to inject into
var targetBloodstreams = new ValueList<Entity<BloodstreamComponent>>();
foreach (var target in targets)
{
if (Deleted(target))
continue;
// Yuck, this is way to hardcodey for my tastes
// TODO blocking injection with a hardsuit should probably done with a cancellable event or something
if (!injector.Comp.PierceArmor && _inventory.TryGetSlotEntity(target, "outerClothing", out var suit) && _tag.HasTag(suit.Value, "Hardsuit"))
{
// Only show popup to attacker
if (source != null)
_popup.PopupEntity(Loc.GetString(injector.Comp.BlockedByHardsuitPopupMessage, ("weapon", injector.Owner), ("target", target)), target, source.Value, PopupType.SmallCaution);
continue;
}
// Check if the target has anything equipped in a slot that would block injection
if (injector.Comp.BlockSlots != SlotFlags.NONE)
{
var blocked = false;
var containerEnumerator = _inventory.GetSlotEnumerator(target, injector.Comp.BlockSlots);
while (containerEnumerator.MoveNext(out var container))
{
if (container.ContainedEntity != null)
{
blocked = true;
break;
}
}
if (blocked)
continue;
}
// Make sure the target has a bloodstream
if (!TryComp<BloodstreamComponent>(target, out var bloodstream))
continue;
// Checks passed; add this target's bloodstream to the list
targetBloodstreams.Add((target, bloodstream));
}
// Make sure we got at least one bloodstream
if (targetBloodstreams.Count == 0)
return false;
// Extract total needed solution from the injector
var removedSolution = _solutionContainer.SplitSolution(injectorSolution.Value, injector.Comp.TransferAmount * targetBloodstreams.Count);
// Adjust solution amount based on transfer efficiency
var solutionToInject = removedSolution.SplitSolution(removedSolution.Volume * injector.Comp.TransferEfficiency);
// Calculate how much of the adjusted solution each target will get
var volumePerBloodstream = solutionToInject.Volume * (1f / targetBloodstreams.Count);
var anySuccess = false;
foreach (var targetBloodstream in targetBloodstreams)
{
// Take our portion of the adjusted solution for this target
var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream);
// Inject our portion into the target's bloodstream
if (_bloodstream.TryAddToChemicals(targetBloodstream.Owner, individualInjection, targetBloodstream.Comp))
anySuccess = true;
}
// Huzzah!
return anySuccess;
}
}

View File

@@ -1,234 +0,0 @@
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
namespace Content.Server.Chemistry.EntitySystems
{
[UsedImplicitly]
public sealed class SolutionTransferSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
/// <summary>
/// Default transfer amounts for the set-transfer verb.
/// </summary>
public static readonly List<int> DefaultTransferAmounts = new() { 1, 5, 10, 25, 50, 100, 250, 500, 1000 };
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage);
}
private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> entity, ref TransferAmountSetValueMessage message)
{
var newTransferAmount = FixedPoint2.Clamp(message.Value, entity.Comp.MinimumTransferAmount, entity.Comp.MaximumTransferAmount);
entity.Comp.TransferAmount = newTransferAmount;
if (message.Session.AttachedEntity is { Valid: true } user)
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), entity.Owner, user);
}
private void AddSetTransferVerbs(Entity<SolutionTransferComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
var (uid, component) = entity;
if (!args.CanAccess || !args.CanInteract || !component.CanChangeTransferAmount || args.Hands == null)
return;
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return;
// Custom transfer verb
AlternativeVerb custom = new();
custom.Text = Loc.GetString("comp-solution-transfer-verb-custom-amount");
custom.Category = VerbCategory.SetTransferAmount;
custom.Act = () => _userInterfaceSystem.TryOpen(uid, TransferAmountUiKey.Key, actor.PlayerSession);
custom.Priority = 1;
args.Verbs.Add(custom);
// Add specific transfer verbs according to the container's size
var priority = 0;
var user = args.User;
foreach (var amount in DefaultTransferAmounts)
{
if (amount < component.MinimumTransferAmount.Int() || amount > component.MaximumTransferAmount.Int())
continue;
AlternativeVerb verb = new();
verb.Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount));
verb.Category = VerbCategory.SetTransferAmount;
verb.Act = () =>
{
component.TransferAmount = FixedPoint2.New(amount);
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user);
};
// we want to sort by size, not alphabetically by the verb text.
verb.Priority = priority;
priority--;
args.Verbs.Add(verb);
}
}
private void OnAfterInteract(Entity<SolutionTransferComponent> entity, ref AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null)
return;
var target = args.Target!.Value;
var (uid, component) = entity;
//Special case for reagent tanks, because normally clicking another container will give solution, not take it.
if (component.CanReceive && !EntityManager.HasComponent<RefillableSolutionComponent>(target) // target must not be refillable (e.g. Reagent Tanks)
&& _solutionContainerSystem.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable
&& EntityManager.TryGetComponent(uid, out RefillableSolutionComponent? refillComp)
&& _solutionContainerSystem.TryGetRefillableSolution((uid, refillComp, null), out var ownerSoln, out var ownerRefill))
{
var transferAmount = component.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank.
if (EntityManager.TryGetComponent(uid, out RefillableSolutionComponent? refill) && refill.MaxRefill != null) // uid is the entity receiving solution from target.
{
transferAmount = FixedPoint2.Min(transferAmount, (FixedPoint2) refill.MaxRefill); // if the receiver has a smaller transfer limit, use that instead
}
var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount);
if (transferred > 0)
{
var toTheBrim = ownerRefill.AvailableVolume == 0;
var msg = toTheBrim
? "comp-solution-transfer-fill-fully"
: "comp-solution-transfer-fill-normal";
_popupSystem.PopupEntity(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User);
args.Handled = true;
return;
}
}
// if target is refillable, and owner is drainable
if (component.CanSend && _solutionContainerSystem.TryGetRefillableSolution(target, out targetSoln, out var targetRefill)
&& _solutionContainerSystem.TryGetDrainableSolution(uid, out ownerSoln, out var ownerDrain))
{
var transferAmount = component.TransferAmount;
if (EntityManager.TryGetComponent(target, out RefillableSolutionComponent? refill) && refill.MaxRefill != null)
{
transferAmount = FixedPoint2.Min(transferAmount, (FixedPoint2) refill.MaxRefill);
}
var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount);
if (transferred > 0)
{
var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target));
_popupSystem.PopupEntity(message, uid, args.User);
args.Handled = true;
}
}
}
/// <summary>
/// Transfer from a solution to another.
/// </summary>
/// <returns>The actual amount transferred.</returns>
public FixedPoint2 Transfer(EntityUid user,
EntityUid sourceEntity,
Entity<SolutionComponent> source,
EntityUid targetEntity,
Entity<SolutionComponent> target,
FixedPoint2 amount)
{
var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity);
// Check if the source is cancelling the transfer
RaiseLocalEvent(sourceEntity, transferAttempt, broadcast: true);
if (transferAttempt.Cancelled)
{
_popupSystem.PopupEntity(transferAttempt.CancelReason!, sourceEntity, user);
return FixedPoint2.Zero;
}
var sourceSolution = source.Comp.Solution;
if (sourceSolution.Volume == 0)
{
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
return FixedPoint2.Zero;
}
// Check if the target is cancelling the transfer
RaiseLocalEvent(targetEntity, transferAttempt, broadcast: true);
if (transferAttempt.Cancelled)
{
_popupSystem.PopupEntity(transferAttempt.CancelReason!, sourceEntity, user);
return FixedPoint2.Zero;
}
var targetSolution = target.Comp.Solution;
if (targetSolution.AvailableVolume == 0)
{
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
return FixedPoint2.Zero;
}
var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
var solution = _solutionContainerSystem.Drain(sourceEntity, source, actualAmount);
_solutionContainerSystem.Refill(targetEntity, target, solution);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{EntityManager.ToPrettyString(user):player} transferred {string.Join(", ", solution.Contents)} to {EntityManager.ToPrettyString(targetEntity):entity}, which now contains {SolutionContainerSystem.ToPrettyString(targetSolution)}");
return actualAmount;
}
}
/// <summary>
/// Raised when attempting to transfer from one solution to another.
/// </summary>
public sealed class SolutionTransferAttemptEvent : CancellableEntityEventArgs
{
public SolutionTransferAttemptEvent(EntityUid from, EntityUid to)
{
From = from;
To = to;
}
public EntityUid From { get; }
public EntityUid To { get; }
/// <summary>
/// Why the transfer has been cancelled.
/// </summary>
public string? CancelReason { get; private set; }
/// <summary>
/// Cancels the transfer.
/// </summary>
public void Cancel(string reason)
{
base.Cancel();
CancelReason = reason;
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.GameTicking;
@@ -13,6 +13,7 @@ using Content.Shared._White;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Server.Connection
@@ -21,6 +22,18 @@ namespace Content.Server.Connection
{
void Initialize();
Task<bool> HavePrivilegedJoin(NetUserId userId); // WD-EDIT
/// <summary>
/// Temporarily allow a user to bypass regular connection requirements.
/// </summary>
/// <remarks>
/// The specified user will be allowed to bypass regular player cap,
/// whitelist and panic bunker restrictions for <paramref name="duration"/>.
/// Bans are not bypassed.
/// </remarks>
/// <param name="user">The user to give a temporary bypass.</param>
/// <param name="duration">How long the bypass should last for.</param>
void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
}
/// <summary>
@@ -35,19 +48,36 @@ namespace Content.Server.Connection
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
private ISawmill _sawmill = default!;
//WD-EDIT
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SponsorsManager _sponsorsManager = default!;
//WD-EDIT
public void Initialize()
{
_sawmill = _logManager.GetSawmill("connections");
_netMgr.Connecting += NetMgrOnConnecting;
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
// Approval-based IP bans disabled because they don't play well with Happy Eyeballs.
// _netMgr.HandleApprovalCallback = HandleApproval;
}
public void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration)
{
ref var time = ref CollectionsMarshal.GetValueRefOrAddDefault(_temporaryBypasses, user, out _);
var newTime = _gameTiming.RealTime + duration;
// Make sure we only update the time if we wouldn't shrink it.
if (newTime > time)
time = newTime;
}
/*
private async Task<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
{
@@ -117,6 +147,20 @@ namespace Content.Server.Connection
hwId = null;
}
var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
if (bans.Count > 0 && bans.Any(x=> x.ExpirationTime == null)) // Miracle Edit
{
var firstBan = bans[0];
var message = firstBan.FormatBanMessage(_cfg, _loc);
return (ConnectionDenyReason.Ban, message, bans);
}
if (HasTemporaryBypass(userId))
{
_sawmill.Verbose("User {UserId} has temporary bypass, skipping further connection checks", userId);
return null;
}
var adminData = await _dbManager.GetAdminDataForAsync(e.UserId);
if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null)
@@ -175,14 +219,6 @@ namespace Content.Server.Connection
}
//WD-EDIT
var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false, _cfg.GetCVar(CCVars.AdminLogsServerName));
if (bans.Count > 0 && bans.Any(x=> x.ExpirationTime == null)) //Miracle edit
{
var firstBan = bans[0];
var message = firstBan.FormatBanMessage(_cfg, _loc);
return (ConnectionDenyReason.Ban, message, bans);
}
if (_cfg.GetCVar(CCVars.WhitelistEnabled))
{
var min = _cfg.GetCVar(CCVars.WhitelistMinPlayers);
@@ -203,6 +239,11 @@ namespace Content.Server.Connection
return null;
}
private bool HasTemporaryBypass(NetUserId user)
{
return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;
}
private async Task<NetUserId?> AssignUserIdCallback(string name)
{
if (!_cfg.GetCVar(CCVars.GamePersistGuests))
@@ -227,8 +268,7 @@ namespace Content.Server.Connection
var adminData = await _dbManager.GetAdminDataForAsync(userId);
var havePriorityJoin = _sponsorsManager.TryGetInfo(userId, out var sponsorData) && sponsorData.HavePriorityJoin;
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
var wasInGame = _gameTicker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
status == PlayerGameStatus.JoinedGame;
return adminData != null ||
havePriorityJoin ||

View File

@@ -0,0 +1,60 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Connection;
[AdminCommand(AdminFlags.Admin)]
public sealed class GrantConnectBypassCommand : LocalizedCommands
{
private static readonly TimeSpan DefaultDuration = TimeSpan.FromHours(1);
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IConnectionManager _connectionManager = default!;
public override string Command => "grant_connect_bypass";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length is not (1 or 2))
{
shell.WriteError(Loc.GetString("cmd-grant_connect_bypass-invalid-args"));
return;
}
var argPlayer = args[0];
var info = await _playerLocator.LookupIdByNameOrIdAsync(argPlayer);
if (info == null)
{
shell.WriteError(Loc.GetString("cmd-grant_connect_bypass-unknown-user", ("user", argPlayer)));
return;
}
var duration = DefaultDuration;
if (args.Length > 1)
{
var argDuration = args[2];
if (!uint.TryParse(argDuration, out var minutes))
{
shell.WriteLine(Loc.GetString("cmd-grant_connect_bypass-invalid-duration", ("duration", argDuration)));
return;
}
duration = TimeSpan.FromMinutes(minutes);
}
_connectionManager.AddTemporaryConnectBypass(info.UserId, duration);
shell.WriteLine(Loc.GetString("cmd-grant_connect_bypass-success", ("user", argPlayer)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHint(Loc.GetString("cmd-grant_connect_bypass-arg-user"));
if (args.Length == 2)
return CompletionResult.FromHint(Loc.GetString("cmd-grant_connect_bypass-arg-duration"));
return CompletionResult.Empty;
}
}

View File

@@ -1,39 +0,0 @@
using Content.Shared.Construction;
using Content.Shared.Examine;
using Content.Shared.Toilet;
using JetBrains.Annotations;
namespace Content.Server.Construction.Conditions
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class ToiletLidClosed : IGraphCondition
{
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (!entityManager.TryGetComponent(uid, out ToiletComponent? toilet))
return false;
return !toilet.LidOpen;
}
public bool DoExamine(ExaminedEvent args)
{
var entity = args.Examined;
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ToiletComponent? toilet)) return false;
if (!toilet.LidOpen) return false;
args.PushMarkup(Loc.GetString("construction-examine-condition-toilet-lid-closed") + "\n");
return true;
}
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
{
yield return new ConstructionGuideEntry()
{
Localization = "construction-step-condition-toilet-lid-closed"
};
}
}
}

View File

@@ -5,6 +5,11 @@ namespace Content.Server.Damage.Components;
[RegisterComponent, Access(typeof(DamagePopupSystem))]
public sealed partial class DamagePopupComponent : Component
{
/// <summary>
/// Bool that will be used to determine if the popup type can be changed with a left click.
/// </summary>
[DataField("allowTypeChange")] [ViewVariables(VVAccess.ReadWrite)]
public bool AllowTypeChange = false;
/// <summary>
/// Enum that will be used to determine the type of damage popup displayed.
/// </summary>

View File

@@ -1,7 +1,8 @@
using System.Linq;
using Content.Server.Damage.Components;
using Content.Server.Popups;
using Content.Shared.Damage;
using Robust.Shared.Player;
using Content.Shared.Interaction;
namespace Content.Server.Damage.Systems;
@@ -13,6 +14,7 @@ public sealed class DamagePopupSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<DamagePopupComponent, DamageChangedEvent>(OnDamageChange);
SubscribeLocalEvent<DamagePopupComponent, InteractHandEvent>(OnInteractHand);
}
private void OnDamageChange(EntityUid uid, DamagePopupComponent component, DamageChangedEvent args)
@@ -38,4 +40,20 @@ public sealed class DamagePopupSystem : EntitySystem
}
}
private void OnInteractHand(EntityUid uid, DamagePopupComponent component, InteractHandEvent args)
{
if (component.AllowTypeChange)
{
if (component.Type == Enum.GetValues(typeof(DamagePopupType)).Cast<DamagePopupType>().Last())
{
component.Type = Enum.GetValues(typeof(DamagePopupType)).Cast<DamagePopupType>().First();
}
else
{
component.Type = (DamagePopupType) (int) component.Type + 1;
}
_popupSystem.PopupEntity("Target set to type: " + component.Type.ToString(), uid);
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Destructible.Thresholds.Behaviors;

View File

@@ -24,6 +24,12 @@ public sealed partial class SignalTimerComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string Label = string.Empty;
/// <summary>
/// Default max width of a label (how many letters can this render?)
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int MaxLength = 5;
/// <summary>
/// The port that gets signaled when the timer triggers.
/// </summary>

View File

@@ -44,6 +44,7 @@ public sealed class SignalTimerSystem : EntitySystem
private void OnInit(EntityUid uid, SignalTimerComponent component, ComponentInit args)
{
_appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, component.Label);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
_signalSystem.EnsureSinkPorts(uid, component.Trigger);
}
@@ -74,11 +75,6 @@ public sealed class SignalTimerSystem : EntitySystem
{
RemComp<ActiveSignalTimerComponent>(uid);
if (TryComp<AppearanceComponent>(uid, out var appearance))
{
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, signalTimer.Label, appearance);
}
var announceMessage = signalTimer.Label;
if (string.IsNullOrWhiteSpace(announceMessage)) { announceMessage = Loc.GetString("label-none"); }
@@ -152,10 +148,15 @@ public sealed class SignalTimerSystem : EntitySystem
if (!IsMessageValid(uid, args))
return;
component.Label = args.Text[..Math.Min(5, args.Text.Length)];
component.Label = args.Text[..Math.Min(component.MaxLength, args.Text.Length)];
if (!HasComp<ActiveSignalTimerComponent>(uid))
{
// could maybe move the defaulttext update out of this block,
// if you delved deep into appearance update batching
_appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, component.Label);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
}
}
/// <summary>
@@ -183,7 +184,14 @@ public sealed class SignalTimerSystem : EntitySystem
if (!IsMessageValid(uid, args))
return;
OnStartTimer(uid, component);
// feedback received: pressing the timer button while a timer is running should cancel the timer.
if (HasComp<ActiveSignalTimerComponent>(uid))
{
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, _gameTiming.CurTime);
Trigger(uid, component);
}
else
OnStartTimer(uid, component);
}
private void OnSignalReceived(EntityUid uid, SignalTimerComponent component, ref SignalReceivedEvent args)

View File

@@ -136,8 +136,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
// This is not an interaction, activation, or alternative verb type because unfortunately most users are
// unwilling to accept that this is where they belong and don't want to accidentally climb inside.
if (!component.MobsCanEnter ||
!args.CanAccess ||
if (!args.CanAccess ||
!args.CanInteract ||
component.Container.ContainedEntities.Contains(args.User) ||
!_actionBlockerSystem.CanMove(args.User))
@@ -628,10 +627,10 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
switch (state)
{
case DisposalsPressureState.Flushed:
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Flushing, appearance);
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayFlushing, appearance);
break;
case DisposalsPressureState.Pressurizing:
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Charging, appearance);
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayCharging, appearance);
break;
case DisposalsPressureState.Ready:
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Anchored, appearance);

View File

@@ -0,0 +1,69 @@
using System.Linq;
using Content.Server.Doors.Electronics;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Doors.Electronics;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Server.Doors.Electronics;
public sealed class DoorElectronicsSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoorElectronicsComponent, DoorElectronicsUpdateConfigurationMessage>(OnChangeConfiguration);
SubscribeLocalEvent<DoorElectronicsComponent, AccessReaderConfigurationChangedEvent>(OnAccessReaderChanged);
SubscribeLocalEvent<DoorElectronicsComponent, BoundUIOpenedEvent>(OnBoundUIOpened);
}
public void UpdateUserInterface(EntityUid uid, DoorElectronicsComponent component)
{
var accesses = new List<ProtoId<AccessLevelPrototype>>();
if (TryComp<AccessReaderComponent>(uid, out var accessReader))
{
foreach (var accessList in accessReader.AccessLists)
{
var access = accessList.FirstOrDefault();
accesses.Add(access);
}
}
var state = new DoorElectronicsConfigurationState(accesses);
_uiSystem.TrySetUiState(uid, DoorElectronicsConfigurationUiKey.Key, state);
}
private void OnChangeConfiguration(
EntityUid uid,
DoorElectronicsComponent component,
DoorElectronicsUpdateConfigurationMessage args)
{
var accessReader = EnsureComp<AccessReaderComponent>(uid);
_accessReader.SetAccesses(uid, accessReader, args.AccessList);
}
private void OnAccessReaderChanged(
EntityUid uid,
DoorElectronicsComponent component,
AccessReaderConfigurationChangedEvent args)
{
UpdateUserInterface(uid, component);
}
private void OnBoundUIOpened(
EntityUid uid,
DoorElectronicsComponent component,
BoundUIOpenedEvent args)
{
UpdateUserInterface(uid, component);
}
}

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