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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
4
Content.Client/Access/UI/AccessLevelControl.xaml
Normal file
4
Content.Client/Access/UI/AccessLevelControl.xaml
Normal file
@@ -0,0 +1,4 @@
|
||||
<GridContainer xmlns="https://spacestation14.io"
|
||||
Columns="5"
|
||||
HorizontalAlignment="Center">
|
||||
</GridContainer>
|
||||
52
Content.Client/Access/UI/AccessLevelControl.xaml.cs
Normal file
52
Content.Client/Access/UI/AccessLevelControl.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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="Текущая выбранная иконка для роли: " />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>>();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Client.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class OpenableSystem : SharedOpenableSystem
|
||||
{
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
122
Content.Client/RCD/AlignRCDConstruction.cs
Normal file
122
Content.Client/RCD/AlignRCDConstruction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
78
Content.Client/RCD/RCDConstructionGhostSystem.cs
Normal file
78
Content.Client/RCD/RCDConstructionGhostSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
47
Content.Client/RCD/RCDMenu.xaml
Normal file
47
Content.Client/RCD/RCDMenu.xaml
Normal 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>
|
||||
174
Content.Client/RCD/RCDMenu.xaml.cs
Normal file
174
Content.Client/RCD/RCDMenu.xaml.cs
Normal 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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
49
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Normal file
49
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
105
Content.Client/UserInterface/Controls/RadialContainer.cs
Normal file
105
Content.Client/UserInterface/Controls/RadialContainer.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
255
Content.Client/UserInterface/Controls/RadialMenu.cs
Normal file
255
Content.Client/UserInterface/Controls/RadialMenu.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
40
Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
Normal file
40
Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ||
|
||||
|
||||
60
Content.Server/Connection/GrantConnectBypassCommand.cs
Normal file
60
Content.Server/Connection/GrantConnectBypassCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Server.Destructible.Thresholds.Behaviors;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user