Merge branch 'master' into races

This commit is contained in:
Tornado Tech
2024-04-04 11:09:52 +10:00
903 changed files with 18059 additions and 9853 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
@@ -28,7 +29,6 @@ namespace Content.Client.Access.UI
if (EntMan.TryGetComponent<IdCardConsoleComponent>(Owner, out var idCard))
{
accessLevels = idCard.AccessLevels;
accessLevels.Sort();
}
else
{
@@ -65,7 +65,7 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
public void SubmitData(string newFullName, string newJobTitle, List<string> newAccessList, string newJobPrototype)
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
{
if (newFullName.Length > MaxFullNameLength)
newFullName = newFullName[..MaxFullNameLength];

View File

@@ -30,10 +30,6 @@
<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" />
</BoxContainer>
</DefaultWindow>

View File

@@ -20,7 +20,7 @@ namespace Content.Client.Access.UI
private readonly IdCardConsoleBoundUserInterface _owner;
private readonly Dictionary<string, Button> _accessButtons = new();
private AccessLevelControl _accessButtons = new();
private readonly List<string> _jobPrototypeIds = new();
private string? _lastFullName;
@@ -66,36 +66,18 @@ 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<AccessLevelPrototype>(access, out var accessLevel))
{
_logMill.Error($"Unable to find accesslevel for {access}");
continue;
}
var newButton = new Button
{
Text = GetAccessLevelName(accessLevel),
ToggleMode = true,
};
AccessLevelGrid.AddChild(newButton);
_accessButtons.Add(accessLevel.ID, newButton);
newButton.OnPressed += _ => SubmitData();
button.OnPressed += _ => SubmitData();
}
}
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)
{
@@ -119,7 +101,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;
}
@@ -134,7 +116,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;
}
@@ -184,15 +166,10 @@ namespace Content.Client.Access.UI
JobPresetOptionButton.Disabled = !interfaceEnabled;
foreach (var (accessName, button) in _accessButtons)
{
button.Disabled = !interfaceEnabled;
if (interfaceEnabled)
{
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)
@@ -215,7 +192,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);
}
}

View File

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

View File

@@ -17,6 +17,7 @@ public sealed class InjectorStatusControl : Control
private FixedPoint2 PrevVolume;
private FixedPoint2 PrevMaxVolume;
private FixedPoint2 PrevTransferAmount;
private InjectorToggleMode PrevToggleState;
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
@@ -37,11 +38,13 @@ public sealed class InjectorStatusControl : Control
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevTransferAmount == _parent.Comp.TransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.TransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
// Update current volume and injector state

View File

@@ -155,6 +155,9 @@ namespace Content.Client.Construction.UI
if (recipe.Hide)
continue;
if (!recipe.CrystallPunkAllowed) //CrystallPunk clearing recipes
continue;
if (_playerManager.LocalSession == null
|| _playerManager.LocalEntity == null
|| (recipe.EntityWhitelist != null && !recipe.EntityWhitelist.IsValid(_playerManager.LocalEntity.Value)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,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!;
@@ -63,7 +63,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)
{
@@ -71,7 +71,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
if (extra.Contains(mat))
{
RemoveChild(display);
MaterialList.RemoveChild(display);
continue;
}
@@ -83,7 +83,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
foreach (var mat in missing)
{
var volume = mats[mat];
AddChild(new MaterialDisplay(_owner.Value, mat, volume, canEject));
MaterialList.AddChild(new MaterialDisplay(_owner.Value, mat, volume, canEject));
}
_currentMaterials = mats;

View File

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

View File

@@ -25,6 +25,14 @@
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-chat-window-opacity'}" Margin="8 0" />
<Slider Name="ChatWindowOpacitySlider"
MinValue="0"
MaxValue="1"
MinWidth="200" />
<Label Name="ChatWindowOpacityLabel" Margin="8 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-screen-shake-intensity'}" Margin="8 0" />
<Slider Name="ScreenShakeIntensitySlider"
@@ -65,6 +73,3 @@
</controls:StripeBack>
</BoxContainer>
</tabs:MiscTab>

View File

@@ -66,6 +66,7 @@ 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;
@@ -81,6 +82,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 +103,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));
@@ -127,6 +136,7 @@ namespace Content.Client.Options.UI.Tabs
_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);
@@ -154,6 +164,7 @@ namespace Content.Client.Options.UI.Tabs
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);
@@ -170,6 +181,7 @@ namespace Content.Client.Options.UI.Tabs
isEnableColorNameSame &&
isColorblindFriendly &&
isReducedMotionSame &&
isChatWindowOpacitySame &&
isScreenShakeIntensitySame &&
// isToggleWalkSame &&
isStaticStorageUISame;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,137 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
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!;
private readonly SpriteSystem _spriteSystem;
public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_spriteSystem = _entManager.System<SpriteSystem>();
// 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 name = Loc.GetString(proto.SetName);
name = char.ToUpper(name[0]) + name.Remove(0, 1);
var button = new RCDMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = name,
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);
Close();
};
}
}
}
public sealed class RCDMenuButton : RadialMenuTextureButton
{
public ProtoId<RCDPrototype> ProtoId { get; set; }
public RCDMenuButton()
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,6 +45,7 @@ namespace Content.Client.Stylesheets
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
public const string StyleClassInventorySlotBackground = "InventorySlotBackground";
public const string StyleClassHandSlotHighlight = "HandSlotHighlight";
public const string StyleClassChatPanel = "ChatPanel";
public const string StyleClassChatSubPanel = "ChatSubPanel";
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
public const string StyleClassHotbarPanel = "HotbarPanel";
@@ -144,6 +145,8 @@ namespace Content.Client.Stylesheets
public const string StyleClassButtonColorRed = "ButtonColorRed";
public const string StyleClassButtonColorGreen = "ButtonColorGreen";
public static readonly Color ChatBackgroundColor = Color.FromHex("#25252ADD");
public override Stylesheet Stylesheet { get; }
public StyleNano(IResourceCache resCache) : base(resCache)
@@ -290,7 +293,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);
@@ -298,19 +301,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);
@@ -346,12 +349,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, 2);
var chatSubBg = new StyleBoxFlat
{
BackgroundColor = ChatBackgroundColor,
};
chatSubBg.SetContentMarginOverride(StyleBox.Margin.All, 2);
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
var actionSearchBox = new StyleBoxTexture
@@ -368,9 +375,9 @@ namespace Content.Client.Stylesheets
};
tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2);
var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)};
var tabContainerBoxActive = new StyleBoxFlat { BackgroundColor = new Color(64, 64, 64) };
tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)};
var tabContainerBoxInactive = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 32) };
tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var progressBarBackground = new StyleBoxFlat
@@ -409,21 +416,21 @@ 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;
var itemListBackgroundSelected = new StyleBoxFlat {BackgroundColor = new Color(75, 75, 86)};
var itemListBackgroundSelected = new StyleBoxFlat { BackgroundColor = new Color(75, 75, 86) };
itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
var itemListItemBackgroundDisabled = new StyleBoxFlat {BackgroundColor = new Color(10, 10, 12)};
var itemListItemBackgroundDisabled = new StyleBoxFlat { BackgroundColor = new Color(10, 10, 12) };
itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(55, 55, 68)};
var itemListItemBackground = new StyleBoxFlat { BackgroundColor = new Color(55, 55, 68) };
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);
@@ -489,9 +496,9 @@ namespace Content.Client.Stylesheets
sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12);
sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12);
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);
@@ -850,6 +857,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),
@@ -858,13 +872,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[]
@@ -1468,6 +1475,25 @@ namespace Content.Client.Stylesheets
Element<Label>().Class("Disabled")
.Prop("font-color", DisabledFore),
// Radial menu buttons
Element<TextureButton>().Class("RadialMenuButton")
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_normal.png")),
Element<TextureButton>().Class("RadialMenuButton")
.Pseudo(TextureButton.StylePseudoClassHover)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_hover.png")),
Element<TextureButton>().Class("RadialMenuCloseButton")
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_normal.png")),
Element<TextureButton>().Class("RadialMenuCloseButton")
.Pseudo(TextureButton.StylePseudoClassHover)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_hover.png")),
Element<TextureButton>().Class("RadialMenuBackButton")
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_normal.png")),
Element<TextureButton>().Class("RadialMenuBackButton")
.Pseudo(TextureButton.StylePseudoClassHover)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_hover.png")),
//PDA - Backgrounds
Element<PanelContainer>().Class("PdaContentBackground")
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,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;
@@ -54,7 +55,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!;
[UISystemDependency] private readonly ExamineSystem? _examine = default;
[UISystemDependency] private readonly GhostSystem? _ghost = default;
@@ -179,8 +179,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();
@@ -232,6 +232,9 @@ public sealed class ChatUIController : UIController
{
_chatNameColors[i] = nameColors[i].ToHex();
}
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
}
public void OnScreenLoad()
@@ -240,6 +243,8 @@ public sealed class ChatUIController : UIController
var viewportContainer = UIManager.ActiveScreen!.FindControl<LayoutContainer>("ViewportContainer");
SetSpeechBubbleRoot(viewportContainer);
SetChatWindowOpacity(_config.GetCVar(CCVars.ChatWindowOpacity));
}
public void OnScreenUnload()
@@ -247,6 +252,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)
@@ -770,7 +803,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);
}
@@ -830,7 +863,7 @@ public sealed class ChatUIController : UIController
break;
case ChatChannel.LOOC:
if (_cfg.GetCVar(CCVars.LoocAboveHeadShow))
if (_config.GetCVar(CCVars.LoocAboveHeadShow))
AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc);
break;
}

View File

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

View File

@@ -7,11 +7,8 @@
HorizontalExpand="True"
VerticalExpand="True"
MinSize="465 225">
<PanelContainer HorizontalExpand="True" VerticalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#25252ADD" />
</PanelContainer.PanelOverride>
<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="8 8 8 4" />
<controls:ChatInputBox HorizontalExpand="True" Name="ChatInput" Access="Public" Margin="2"/>

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using System.Linq;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
using Content.Shared.Access;
namespace Content.Server.Access.Systems;
@@ -54,11 +55,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;
@@ -82,7 +83,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);
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
&& keyStorage.Key is {} key
&& _record.TryGetRecord<GeneralStationRecord>(key, out var record))
@@ -96,7 +97,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
true,
targetIdComponent.FullName,
targetIdComponent.JobTitle,
targetAccessComponent.Tags.ToArray(),
targetAccessComponent.Tags.ToList(),
possibleAccess,
jobProto,
privilegedIdName,
@@ -113,8 +114,8 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
private void TryWriteToTargetId(EntityUid uid,
string newFullName,
string newJobTitle,
List<string> newAccessList,
string newJobProto,
List<ProtoId<AccessLevelPrototype>> newAccessList,
ProtoId<AccessLevelPrototype> newJobProto,
EntityUid player,
IdCardConsoleComponent? component = null)
{
@@ -140,7 +141,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;
@@ -189,7 +190,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader);
}
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, ProtoId<AccessLevelPrototype> newJobTitle, JobPrototype? newJobProto)
{
if (!TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|| keyStorage.Key is not { } key

View File

@@ -1,72 +1,122 @@
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;
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>();
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
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates);
_entities.GetComponent<TransformComponent>(ghost).AttachToGridOrMap();
if (canReturn)
{
// TODO: Remove duplication between all this and "GamePreset.OnGhostAttempt()"...
if (!string.IsNullOrWhiteSpace(mind.CharacterName))
metaDataSystem.SetEntityName(ghost, mind.CharacterName);
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
metaDataSystem.SetEntityName(ghost, mind.Session.Name);
mindSystem.Visit(mindId, ghost, mind);
}
else
{
metaDataSystem.SetEntityName(ghost, player.Name);
mindSystem.TransferTo(mindId, ghost, mind: mind);
}
var comp = _entities.GetComponent<GhostComponent>(ghost);
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(comp, canReturn);
var names = _playerManager.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
return CompletionResult.FromHintOptions(names, LocalizationManager.GetString("shell-argument-username-optional-hint"));
}
return CompletionResult.Empty;
}
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
return;
}
var player = shell.Player;
var self = player != null;
if (player == null)
{
// If you are not a player, you require a player argument.
if (args.Length == 0)
{
shell.WriteError(LocalizationManager.GetString("shell-need-exactly-one-argument"));
return;
}
var didFind = _playerManager.TryGetSessionByUsername(args[0], out player);
if (!didFind)
{
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
return;
}
}
// If you are a player and a username is provided, a lookup is done to find the target player.
if (args.Length == 1)
{
var didFind = _playerManager.TryGetSessionByUsername(args[0], out player);
if (!didFind)
{
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
return;
}
}
var mindSystem = _entities.System<SharedMindSystem>();
var metaDataSystem = _entities.System<MetaDataSystem>();
var ghostSystem = _entities.System<SharedGhostSystem>();
var transformSystem = _entities.System<TransformSystem>();
var gameTicker = _entities.System<GameTicker>();
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
{
shell.WriteError(self
? LocalizationManager.GetString("aghost-no-mind-self")
: LocalizationManager.GetString("aghost-no-mind-other"));
return;
}
if (mind.VisitingEntity != default && _entities.TryGetComponent<GhostComponent>(mind.VisitingEntity, out var oldGhostComponent))
{
mindSystem.UnVisit(mindId, mind);
// If already an admin ghost, then return to body.
if (oldGhostComponent.CanGhostInteract)
return;
}
var canReturn = mind.CurrentEntity != null
&& !_entities.HasComponent<GhostComponent>(mind.CurrentEntity);
var coordinates = player!.AttachedEntity != null
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
: gameTicker.GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates);
transformSystem.AttachToGridOrMap(ghost, _entities.GetComponent<TransformComponent>(ghost));
if (canReturn)
{
// TODO: Remove duplication between all this and "GamePreset.OnGhostAttempt()"...
if (!string.IsNullOrWhiteSpace(mind.CharacterName))
metaDataSystem.SetEntityName(ghost, mind.CharacterName);
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
metaDataSystem.SetEntityName(ghost, mind.Session.Name);
mindSystem.Visit(mindId, ghost, mind);
}
else
{
metaDataSystem.SetEntityName(ghost, player.Name);
mindSystem.TransferTo(mindId, ghost, mind: mind);
}
var comp = _entities.GetComponent<GhostComponent>(ghost);
ghostSystem.SetCanReturnToBody(comp, canReturn);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,5 +61,18 @@ namespace Content.Server.Atmos.Components
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float FirestackFade = -0.1f;
/// <summary>
/// Set FirestackFade on Ingite to this value
/// </summary>
[DataField]
public float? FirestackFadeOnIgnite = null;
/// <summary>
/// CrystallPunk moment
/// determines how extinction "FirestackFade" will fade out. it can be used to make "parabolas" of object ignition and decay.
/// </summary>
[DataField]
public float FirestackFadeFade = 0;
}
}

View File

@@ -27,6 +27,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Content.Server.CrystallPunk.Temperature;
namespace Content.Server.Atmos.EntitySystems
{
@@ -296,6 +297,12 @@ namespace Content.Server.Atmos.EntitySystems
_ignitionSourceSystem.SetIgnited(uid, false);
//CrystallPunk bonfire moment
var ev = new OnFireChangedEvent(flammable.OnFire);
RaiseLocalEvent(uid, ref ev);
//CrystallPunk bonfire moment end
UpdateAppearance(uid, flammable);
}
@@ -317,9 +324,19 @@ namespace Content.Server.Atmos.EntitySystems
else
_adminLogger.Add(LogType.Flammable, $"{ToPrettyString(uid):target} set on fire by {ToPrettyString(ignitionSource):actor}");
flammable.OnFire = true;
//CrystallPunk bonfire moment
var ev = new OnFireChangedEvent(flammable.OnFire);
RaiseLocalEvent(uid, ref ev);
//CrystallPunk bonfire moment end
}
UpdateAppearance(uid, flammable);
//CrystallPunk bonfire moment
if (flammable.FirestackFadeOnIgnite != null)
flammable.FirestackFade = flammable.FirestackFadeOnIgnite.Value;
//CrystallPunk bonfire moment end
}
private void OnDamageChanged(EntityUid uid, IgniteOnHeatDamageComponent component, DamageChangedEvent args)
@@ -434,6 +451,11 @@ namespace Content.Server.Atmos.EntitySystems
_damageableSystem.TryChangeDamage(uid, flammable.Damage * damageScale, interruptsDoAfters: false);
AdjustFireStacks(uid, flammable.FirestackFade * (flammable.Resisting ? 10f : 1f), flammable);
//CrystallPunk bonfire moment
if (flammable.FirestackFadeFade != 0)
flammable.FirestackFade += flammable.FirestackFadeFade * frameTime;
//CrystallPunk bonfire moment end
}
else
{

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
using Content.Server.CrystallPunk.Temperature;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Audio;
@@ -11,6 +12,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
base.Initialize();
SubscribeLocalEvent<AmbientOnPoweredComponent, PowerChangedEvent>(HandlePowerChange);
SubscribeLocalEvent<AmbientOnPoweredComponent, PowerNetBatterySupplyEvent>(HandlePowerSupply);
SubscribeLocalEvent<CPFlammableAmbientSoundComponent, OnFireChangedEvent>(OnFireChanged); //CrystallPunk bonfire moment
}
private void HandlePowerSupply(EntityUid uid, AmbientOnPoweredComponent component, ref PowerNetBatterySupplyEvent args)
@@ -22,4 +24,11 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
{
SetAmbience(uid, args.Powered);
}
//CrystallPunk bonfire moment
private void OnFireChanged(Entity<CPFlammableAmbientSoundComponent> ent, ref OnFireChangedEvent args)
{
SetAmbience(ent, args.OnFire);
}
//CrystallPunk bonfire moment end
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Dispenser;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.Audio;
using Robust.Server.GameObjects;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,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);
}
@@ -66,11 +67,6 @@ public sealed class SignalTimerSystem : EntitySystem
{
RemComp<ActiveSignalTimerComponent>(uid);
if (TryComp<AppearanceComponent>(uid, out var appearance))
{
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, signalTimer.Label, appearance);
}
_audio.PlayPvs(signalTimer.DoneSound, uid);
_signalSystem.InvokePort(uid, signalTimer.TriggerPort);
@@ -139,10 +135,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>
@@ -166,7 +167,15 @@ public sealed class SignalTimerSystem : EntitySystem
{
if (!IsMessageValid(uid, args))
return;
OnStartTimer(uid, component);
// feedback received: pressing the timer button while a timer is running should cancel the timer.
if (HasComp<ActiveSignalTimerComponent>(uid))
{
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, _gameTiming.CurTime);
Trigger(uid, component);
}
else
OnStartTimer(uid, component);
}
private void OnSignalReceived(EntityUid uid, SignalTimerComponent component, ref SignalReceivedEvent args)

View File

@@ -135,8 +135,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))
@@ -630,10 +629,10 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
switch (state)
{
case DisposalsPressureState.Flushed:
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Flushing, appearance);
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayFlushing, appearance);
break;
case DisposalsPressureState.Pressurizing:
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Charging, appearance);
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayCharging, appearance);
break;
case DisposalsPressureState.Ready:
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Anchored, appearance);

View File

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

View File

@@ -73,6 +73,7 @@ public sealed class FireExtinguisherSystem : EntitySystem
args.Handled = true;
// TODO: why is this copy paste shit here just have fire extinguisher cancel transfer when safety is on
var transfer = containerSolution.AvailableVolume;
if (TryComp<SolutionTransferComponent>(entity.Owner, out var solTrans))
{

View File

@@ -1,5 +1,5 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
@@ -11,6 +11,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory.Events;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Spillable;
using Content.Shared.Throwing;
@@ -29,6 +30,7 @@ public sealed partial class PuddleSystem
// Openable handles the event if it's closed
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<SpillableComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
@@ -114,6 +116,9 @@ public sealed partial class PuddleSystem
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
return;
// block access to the solution while worn
AddComp<BlockSolutionAccessComponent>(entity);
if (solution.Volume == 0)
return;
@@ -122,6 +127,14 @@ public sealed partial class PuddleSystem
TrySplashSpillAt(entity.Owner, Transform(args.Equipee).Coordinates, drainedSolution, out _);
}
private void OnGotUnequipped(Entity<SpillableComponent> entity, ref GotUnequippedEvent args)
{
if (!entity.Comp.SpillWorn)
return;
RemCompDeferred<BlockSolutionAccessComponent>(entity);
}
private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
{
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))

View File

@@ -4,6 +4,7 @@ using Content.Server.Discord;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
@@ -194,26 +195,18 @@ namespace Content.Server.GameTicking
SendServerMessage(Loc.GetString("game-ticker-start-round"));
// Just in case it hasn't been loaded previously we'll try loading it.
LoadMaps();
// map has been selected so update the lobby info text
// applies to players who didn't ready up
UpdateInfoText();
StartGamePresetRules();
RoundLengthMetric.Set(0);
var startingEvent = new RoundStartingEvent(RoundId);
RaiseLocalEvent(startingEvent);
var readyPlayers = new List<ICommonSession>();
var readyPlayerProfiles = new Dictionary<NetUserId, HumanoidCharacterProfile>();
var autoDeAdmin = _cfg.GetCVar(CCVars.AdminDeadminOnJoin);
foreach (var (userId, status) in _playerGameStatuses)
{
if (LobbyEnabled && status != PlayerGameStatus.ReadyToPlay) continue;
if (!_playerManager.TryGetSessionById(userId, out var session)) continue;
if (autoDeAdmin && _adminManager.IsAdmin(session))
{
_adminManager.DeAdmin(session);
}
#if DEBUG
DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??");
#endif
@@ -235,6 +228,20 @@ namespace Content.Server.GameTicking
readyPlayerProfiles.Add(userId, profile);
}
// Just in case it hasn't been loaded previously we'll try loading it.
LoadMaps();
// map has been selected so update the lobby info text
// applies to players who didn't ready up
UpdateInfoText();
StartGamePresetRules();
RoundLengthMetric.Set(0);
var startingEvent = new RoundStartingEvent(RoundId);
RaiseLocalEvent(startingEvent);
var origReadyPlayers = readyPlayers.ToArray();
if (!StartPreset(origReadyPlayers, force))

View File

@@ -154,10 +154,6 @@ namespace Content.Server.GameTicking
return;
}
// Automatically de-admin players who are joining.
if (_cfg.GetCVar(CCVars.AdminDeadminOnJoin) && _adminManager.IsAdmin(player))
_adminManager.DeAdmin(player);
// We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station);
RaiseLocalEvent(bev);

View File

@@ -763,10 +763,6 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
_mind.SetUserId(newMind, nukieSession.Session.UserId);
_roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto });
// Automatically de-admin players who are being made nukeops
if (_cfg.GetCVar(CCVars.AdminDeadminOnJoin) && _adminManager.IsAdmin(nukieSession.Session))
_adminManager.DeAdmin(nukieSession.Session);
_mind.TransferTo(newMind, mob);
}
//Otherwise, spawn as a ghost role

View File

@@ -1,12 +1,12 @@
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Glue;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Item;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;

View File

@@ -75,6 +75,9 @@ namespace Content.Server.Hands.Systems
private void OnExploded(Entity<HandsComponent> ent, ref BeforeExplodeEvent args)
{
if (ent.Comp.DisableExplosionRecursion)
return;
foreach (var hand in ent.Comp.Hands.Values)
{
if (hand.HeldEntity is { } uid)

View File

@@ -15,12 +15,6 @@ public sealed partial class ScramImplantComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float TeleportRadius = 100f;
/// <summary>
/// How many times to check for a valid tile to teleport to
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public int TeleportAttempts = 20;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
}

View File

@@ -14,13 +14,14 @@ using Content.Shared.Popups;
using Content.Shared.Preferences;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
using System.Numerics;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
namespace Content.Server.Implants;
@@ -28,7 +29,6 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
{
[Dependency] private readonly CuffableSystem _cuffable = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly StoreSystem _store = default!;
@@ -37,8 +37,11 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
[Dependency] private readonly SharedTransformSystem _xform = default!;
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
[Dependency] private readonly PullingSystem _pullingSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
private EntityQuery<PhysicsComponent> _physicsQuery;
private HashSet<Entity<MapGridComponent>> _targetGrids = [];
public override void Initialize()
{
@@ -107,41 +110,92 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
_pullingSystem.TryStopPull(ent, pull);
var xform = Transform(ent);
var entityCoords = xform.Coordinates.ToMap(EntityManager, _xform);
var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius);
// try to find a valid position to teleport to, teleport to whatever works if we can't
var targetCoords = new MapCoordinates();
for (var i = 0; i < implant.TeleportAttempts; i++)
if (targetCoords != null)
{
var distance = implant.TeleportRadius * MathF.Sqrt(_random.NextFloat()); // to get an uniform distribution
targetCoords = entityCoords.Offset(_random.NextAngle().ToVec() * distance);
// prefer teleporting to grids
if (!_mapManager.TryFindGridAt(targetCoords, out var gridUid, out var grid))
continue;
// the implant user probably does not want to be in your walls
var valid = true;
foreach (var entity in grid.GetAnchoredEntities(targetCoords))
{
if (!_physicsQuery.TryGetComponent(entity, out var body))
continue;
if (body.BodyType != BodyType.Static ||
!body.Hard ||
(body.CollisionLayer & (int) CollisionGroup.Impassable) == 0)
continue;
valid = false;
break;
}
if (valid)
break;
_xform.SetCoordinates(ent, targetCoords.Value);
_audio.PlayPvs(implant.TeleportSound, ent);
args.Handled = true;
}
_xform.SetWorldPosition(ent, targetCoords.Position);
_audio.PlayPvs(implant.TeleportSound, ent);
}
args.Handled = true;
private EntityCoordinates? SelectRandomTileInRange(TransformComponent userXform, float radius)
{
var userCoords = userXform.Coordinates.ToMap(EntityManager, _xform);
_targetGrids.Clear();
_lookupSystem.GetEntitiesInRange(userCoords, radius, _targetGrids);
Entity<MapGridComponent>? targetGrid = null;
if (_targetGrids.Count == 0)
return null;
// Give preference to the grid the entity is currently on.
// This does not guarantee that if the probability fails that the owner's grid won't be picked.
// In reality the probability is higher and depends on the number of grids.
if (userXform.GridUid != null && TryComp<MapGridComponent>(userXform.GridUid, out var gridComp))
{
var userGrid = new Entity<MapGridComponent>(userXform.GridUid.Value, gridComp);
if (_random.Prob(0.5f))
{
_targetGrids.Remove(userGrid);
targetGrid = userGrid;
}
}
if (targetGrid == null)
targetGrid = _random.GetRandom().PickAndTake(_targetGrids);
EntityCoordinates? targetCoords = null;
do
{
var valid = false;
var range = (float) Math.Sqrt(radius);
var box = Box2.CenteredAround(userCoords.Position, new Vector2(range, range));
var tilesInRange = _mapSystem.GetTilesEnumerator(targetGrid.Value.Owner, targetGrid.Value.Comp, box, false);
var tileList = new ValueList<Vector2i>();
while (tilesInRange.MoveNext(out var tile))
{
tileList.Add(tile.GridIndices);
}
while (tileList.Count != 0)
{
var tile = tileList.RemoveSwap(_random.Next(tileList.Count));
valid = true;
foreach (var entity in _mapSystem.GetAnchoredEntities(targetGrid.Value.Owner, targetGrid.Value.Comp,
tile))
{
if (!_physicsQuery.TryGetComponent(entity, out var body))
continue;
if (body.BodyType != BodyType.Static ||
!body.Hard ||
(body.CollisionLayer & (int) CollisionGroup.MobMask) == 0)
continue;
valid = false;
break;
}
if (valid)
{
targetCoords = new EntityCoordinates(targetGrid.Value.Owner,
_mapSystem.TileCenterToVector(targetGrid.Value, tile));
break;
}
}
if (valid || _targetGrids.Count == 0) // if we don't do the check here then PickAndTake will blow up on an empty set.
break;
targetGrid = _random.GetRandom().PickAndTake(_targetGrids);
} while (true);
return targetCoords;
}
private void OnDnaScramblerImplant(EntityUid uid, SubdermalImplantComponent component, UseDnaScramblerImplantEvent args)

View File

@@ -1,12 +1,12 @@
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Glue;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Lube;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Audio;

View File

@@ -1,8 +1,6 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.GameTicking;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Stack;
@@ -10,11 +8,13 @@ using Content.Server.Wires;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Materials;
using Content.Shared.Mind;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Utility;

View File

@@ -14,6 +14,7 @@ using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.Systems;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;

View File

@@ -106,7 +106,7 @@ namespace Content.Server.NodeContainer.EntitySystems
&& ent.Comp.Nodes.TryGetValue(id2, out var n2)
&& n2 is T2 t2
&& ent.Comp.Nodes.TryGetValue(id3, out var n3)
&& n2 is T3 t3)
&& n3 is T3 t3)
{
node1 = t1;
node2 = t2;

View File

@@ -24,6 +24,7 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
@@ -410,6 +411,10 @@ public sealed class DrinkSystem : EntitySystem
!_body.TryGetBodyOrganComponents<StomachComponent>(ev.User, out var stomachs, body))
return;
// Make sure the solution exists
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
return;
// no drinking from living drinks, have to kill them first.
if (_mobState.IsAlive(entity))
return;

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