Merge branch 'master' of https://github.com/space-wizards/space-station-14 into moss-seniorcargo
Merged master into branch
This commit is contained in:
12
.github/CODEOWNERS
vendored
12
.github/CODEOWNERS
vendored
@@ -3,17 +3,17 @@
|
||||
# Sorting by path instead of by who added it one day :(
|
||||
# this isn't how codeowners rules work pls read the first comment instead of trying to force a sorting order
|
||||
|
||||
/Resources/ConfigPresets/WizardsDen/ @nikthechampiongr @crazybrain23
|
||||
/Content.*/Administration/ @DrSmugleaf @nikthechampiongr @crazybrain23
|
||||
/Resources/ServerInfo/ @nikthechampiongr @crazybrain23
|
||||
/Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr @crazybrain23
|
||||
/Resources/ConfigPresets/WizardsDen/ @crazybrain23
|
||||
/Content.*/Administration/ @DrSmugleaf @crazybrain23
|
||||
/Resources/ServerInfo/ @crazybrain23
|
||||
/Resources/ServerInfo/Guidebook/ServerRules/ @crazybrain23
|
||||
|
||||
/Resources/Prototypes/Maps/** @Emisse @ArtisticRoomba
|
||||
|
||||
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
|
||||
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
|
||||
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
|
||||
/Resources/Prototypes/Guidebook/rules.yml @nikthechampiongr @crazybrain23
|
||||
/Resources/Prototypes/Guidebook/rules.yml @crazybrain23
|
||||
/Content.*/Body/ @DrSmugleaf
|
||||
/Content.YAMLLinter @DrSmugleaf
|
||||
/Content.Shared/Damage/ @DrSmugleaf
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
# SKREEEE
|
||||
/Content.*.Database/ @PJB3005 @DrSmugleaf
|
||||
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr @crazybrain23
|
||||
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @crazybrain23
|
||||
/Pow3r/ @PJB3005
|
||||
/Content.Server/Power/Pow3r/ @PJB3005
|
||||
|
||||
|
||||
54
.github/workflows/check-trailing-whitespace.yml
vendored
54
.github/workflows/check-trailing-whitespace.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Trailing Whitespace Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Trailing Whitespace Check
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Get changed text files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
with:
|
||||
files: |
|
||||
**.cs
|
||||
**.yml
|
||||
**.swsl
|
||||
**.json
|
||||
**.py
|
||||
- name: Check for trailing whitespace and EOF newline
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
|
||||
run: |
|
||||
has_trailing_whitespace=0
|
||||
has_missing_eof_newline=0
|
||||
|
||||
for file in ${ALL_CHANGED_FILES}; do
|
||||
echo "Checking $file"
|
||||
|
||||
# Check for trailing whitespace
|
||||
if grep -qP '[ \t]+$' "$file"; then
|
||||
echo "::error file=$file::Trailing whitespace found"
|
||||
has_trailing_whitespace=1
|
||||
fi
|
||||
|
||||
# Check for missing EOF newline
|
||||
if [ -f "$file" ] && [ -s "$file" ]; then
|
||||
last_char=$(tail -c 1 "$file")
|
||||
if [ "$last_char" != "" ] && [ "$last_char" != $'\n' ]; then
|
||||
echo "::error file=$file::Missing newline at end of file"
|
||||
has_missing_eof_newline=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$has_trailing_whitespace" -eq 1 ] || [ "$has_missing_eof_newline" -eq 1 ]; then
|
||||
echo "Issues found: trailing whitespace or missing EOF newline."
|
||||
echo "We recommend using an IDE to prevent this from happening."
|
||||
exit 1
|
||||
fi
|
||||
40
CODE_OF_CONDUCT.md
Normal file
40
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Space Station 14 Code of Conduct
|
||||
|
||||
Space Station 14's staff and community is made up volunteers from all over the world, working on every aspect of the project - including development, teaching, and hosting integral tools.
|
||||
|
||||
Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to all levels of the project, from commenters to contributors to staff.
|
||||
|
||||
This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
|
||||
|
||||
This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here.
|
||||
|
||||
If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com).
|
||||
|
||||
- **Be friendly and patient.**
|
||||
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
|
||||
- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and contributors, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. We have contributors of all skill levels, some even making their first foray into a new field with this project, so keep that in mind when discussing someone's work.
|
||||
- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Space Station 14 community should be respectful when dealing with other members as well as with people outside the Space Station 14 community. Assume contributions to the project, even those that do not end up being included, are made in good faith.
|
||||
- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
|
||||
- Violent threats or language directed against another person.
|
||||
- Discriminatory jokes and language.
|
||||
- Posting sexually explicit or violent material.
|
||||
- Posting (or threatening to post) other people's personally identifying information ("doxing").
|
||||
- Personal insults, especially those using racist or sexist terms.
|
||||
- Unwelcome sexual attention.
|
||||
- Advocating for, or encouraging, any of the above behavior.
|
||||
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
|
||||
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Space Station 14 is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Space Station 14 comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to make mistakes and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
|
||||
|
||||
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
|
||||
|
||||
## On Comunity Moderation
|
||||
|
||||
Deviating from the Code of Conduct on the Github repository may result in moderative actions taken by project Maintainers. This can involve your content being edited or deleted, and may result in a temporary or permanent block from the repository.
|
||||
|
||||
This is to ensure Space Station 14 is a healthy community in which contributors feel encouraged and empowered to contribute, and to give you as a member of this community a chance to reflect on how you are interacting with it. While outright offensive and bigoted content will *always* be unacceptable on the repository, Maintainers are at liberty to take moderative actions against more ambiguous content that fail to provide constructive criticism, or that provides constructive criticism in a non-constructive manner. Examples of this include using hyperbole, bringing up PRs/changes unrelated to the discussion at hand, hostile tone, off-topic comments, creating PRs/Issues for the sole purpose of causing discussions, skirting the line of acceptable behavior, etc. Disagreeing with content or each other is fine and appreciated, but only as long as it's done with respect and in a constructive manner.
|
||||
|
||||
Maintainers are expected to adhere to the guidelines as listed in the [Github Moderation Guidelines](https://docs.spacestation14.com/en/general-development/github-moderation-guidelines.html), though may deviate should they feel it's in the best interest of the community. If you believe you had an action incorrectly applied against you, you are encouraged to contact staff via [Discord](https://discord.ss14.io/) or [the forums](https://forum.spacestation14.com/), [appeal your Github ban](https://forum.spacestation14.com/c/ban-appeals/appeals-github/38), or make a [staff complaint](https://forum.spacestation14.com/t/staff-complaint-instructions-and-info/31).
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is an edited version of the [Django Code of Conduct](https://www.djangoproject.com/conduct/), licensed under CC BY 3.0, for the Space Station 14 Github repository.
|
||||
@@ -47,7 +47,7 @@ public class MapLoadBenchmark
|
||||
PoolManager.Shutdown();
|
||||
}
|
||||
|
||||
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
|
||||
public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
|
||||
|
||||
[ParamsSource(nameof(MapsSource))]
|
||||
public string Map;
|
||||
|
||||
@@ -4,39 +4,20 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client.Access.Commands;
|
||||
|
||||
public sealed class ShowAccessReadersCommand : IConsoleCommand
|
||||
public sealed class ShowAccessReadersCommand : LocalizedEntityCommands
|
||||
{
|
||||
public string Command => "showaccessreaders";
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
[Dependency] private readonly IResourceCache _cache = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
|
||||
public string Description => "Toggles showing access reader permissions on the map";
|
||||
public string Help => """
|
||||
Overlay Info:
|
||||
-Disabled | The access reader is disabled
|
||||
+Unrestricted | The access reader has no restrictions
|
||||
+Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set)
|
||||
+Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed
|
||||
-Tag [Tag Name] | A tag that is not allowed (takes priority over other allows)
|
||||
""";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
public override string Command => "showaccessreaders";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var collection = IoCManager.Instance;
|
||||
var existing = _overlay.RemoveOverlay<AccessOverlay>();
|
||||
if (!existing)
|
||||
_overlay.AddOverlay(new AccessOverlay(EntityManager, _cache, _xform));
|
||||
|
||||
if (collection == null)
|
||||
return;
|
||||
|
||||
var overlay = collection.Resolve<IOverlayManager>();
|
||||
|
||||
if (overlay.RemoveOverlay<AccessOverlay>())
|
||||
{
|
||||
shell.WriteLine($"Set access reader debug overlay to false");
|
||||
return;
|
||||
}
|
||||
|
||||
var entManager = collection.Resolve<IEntityManager>();
|
||||
var cache = collection.Resolve<IResourceCache>();
|
||||
var xform = entManager.System<SharedTransformSystem>();
|
||||
|
||||
overlay.AddOverlay(new AccessOverlay(entManager, cache, xform));
|
||||
shell.WriteLine($"Set access reader debug overlay to true");
|
||||
shell.WriteLine(Loc.GetString($"cmd-showaccessreaders-status", ("status", !existing)));
|
||||
}
|
||||
}
|
||||
|
||||
26
Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml
Normal file
26
Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Orientation="Horizontal"
|
||||
Margin="10 10 10 10"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinHeight="70">
|
||||
|
||||
<!-- Access groups -->
|
||||
<BoxContainer Name="AccessGroupList" Access="Public" Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.5" Margin="0 0 10 0">
|
||||
<!-- Populated with C# code -->
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#FFFFFF" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Access levels -->
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="10 0 0 0">
|
||||
<BoxContainer Name="AccessLevelChecklist" Access="Public" Orientation="Vertical" HorizontalAlignment="Left">
|
||||
<!-- Populated with C# code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
449
Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
Normal file
449
Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
Normal file
@@ -0,0 +1,449 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Access;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Access.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
private bool _isMonotone;
|
||||
private string? _labelStyleClass;
|
||||
|
||||
// Access data
|
||||
private HashSet<ProtoId<AccessGroupPrototype>> _accessGroups = new();
|
||||
private HashSet<ProtoId<AccessLevelPrototype>> _accessLevels = new();
|
||||
private HashSet<ProtoId<AccessLevelPrototype>> _activeAccessLevels = new();
|
||||
|
||||
// Button groups
|
||||
private readonly ButtonGroup _accessGroupsButtons = new();
|
||||
|
||||
// Temp values
|
||||
private int _accessGroupTabIndex = 0;
|
||||
private bool _canInteract = false;
|
||||
private List<AccessLevelPrototype> _accessLevelsForTab = new();
|
||||
private readonly List<AccessLevelEntry> _accessLevelEntries = new();
|
||||
private readonly Dictionary<AccessGroupPrototype, List<AccessLevelPrototype>> _groupedAccessLevels = new();
|
||||
|
||||
// Events
|
||||
public event Action<HashSet<ProtoId<AccessLevelPrototype>>, bool>? OnAccessLevelsChangedEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a UI control for changing access levels.
|
||||
/// Access levels are organized under a list of tabs by their associated access group.
|
||||
/// </summary>
|
||||
public GroupedAccessLevelChecklist()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
private void ArrangeAccessControls()
|
||||
{
|
||||
// Create a list of known access groups with which to populate the UI
|
||||
_groupedAccessLevels.Clear();
|
||||
|
||||
foreach (var accessGroup in _accessGroups)
|
||||
{
|
||||
if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
|
||||
continue;
|
||||
|
||||
_groupedAccessLevels.Add(accessGroupProto, new());
|
||||
}
|
||||
|
||||
// Ensure that the 'general' access group is added to handle
|
||||
// misc. access levels that aren't associated with any group
|
||||
if (_protoManager.TryIndex<AccessGroupPrototype>("General", out var generalAccessProto))
|
||||
_groupedAccessLevels.TryAdd(generalAccessProto, new());
|
||||
|
||||
// Assign known access levels with their associated groups
|
||||
foreach (var accessLevel in _accessLevels)
|
||||
{
|
||||
if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
|
||||
continue;
|
||||
|
||||
var assigned = false;
|
||||
|
||||
foreach (var (accessGroup, accessLevels) in _groupedAccessLevels)
|
||||
{
|
||||
if (!accessGroup.Tags.Contains(accessLevelProto.ID))
|
||||
continue;
|
||||
|
||||
assigned = true;
|
||||
_groupedAccessLevels[accessGroup].Add(accessLevelProto);
|
||||
}
|
||||
|
||||
if (!assigned && generalAccessProto != null)
|
||||
_groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
|
||||
}
|
||||
|
||||
// Remove access groups that have no assigned access levels
|
||||
foreach (var (group, accessLevels) in _groupedAccessLevels)
|
||||
{
|
||||
if (accessLevels.Count == 0)
|
||||
_groupedAccessLevels.Remove(group);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRebuildAccessGroupControls()
|
||||
{
|
||||
AccessGroupList.DisposeAllChildren();
|
||||
AccessLevelChecklist.DisposeAllChildren();
|
||||
|
||||
// No access level prototypes were assigned to any of the access level groups.
|
||||
// Either the turret controller has no assigned access levels or their names were invalid.
|
||||
if (_groupedAccessLevels.Count == 0)
|
||||
return false;
|
||||
|
||||
// Reorder the access groups alphabetically
|
||||
var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
|
||||
|
||||
// Add group access buttons to the UI
|
||||
foreach (var accessGroup in orderedAccessGroups)
|
||||
{
|
||||
var accessGroupButton = CreateAccessGroupButton();
|
||||
|
||||
// Button styling
|
||||
if (_groupedAccessLevels.Count > 1)
|
||||
{
|
||||
if (AccessGroupList.ChildCount == 0)
|
||||
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft);
|
||||
else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
|
||||
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight);
|
||||
else
|
||||
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth);
|
||||
}
|
||||
|
||||
accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup);
|
||||
|
||||
// Label text and styling
|
||||
if (_labelStyleClass != null)
|
||||
accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass);
|
||||
|
||||
var accessLevelPrototypes = _groupedAccessLevels[accessGroup];
|
||||
var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x))
|
||||
? "»"
|
||||
: accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x))
|
||||
? "›"
|
||||
: " ";
|
||||
|
||||
var text = Loc.GetString(
|
||||
"turret-controls-window-access-group-label",
|
||||
("prefix", prefix),
|
||||
("label", accessGroup.GetAccessGroupName())
|
||||
);
|
||||
|
||||
accessGroupButton.Text = text;
|
||||
|
||||
// Button events
|
||||
accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent());
|
||||
|
||||
AccessGroupList.AddChild(accessGroupButton);
|
||||
}
|
||||
|
||||
// Adjust the current tab index so it remains in range
|
||||
if (_accessGroupTabIndex >= _groupedAccessLevels.Count)
|
||||
_accessGroupTabIndex = _groupedAccessLevels.Count - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the checkbox list for the access level controls.
|
||||
/// </summary>
|
||||
public void RebuildAccessLevelsControls()
|
||||
{
|
||||
AccessLevelChecklist.DisposeAllChildren();
|
||||
_accessLevelEntries.Clear();
|
||||
|
||||
// No access level prototypes were assigned to any of the access level groups
|
||||
// Either turret controller has no assigned access levels, or their names were invalid
|
||||
if (_groupedAccessLevels.Count == 0)
|
||||
return;
|
||||
|
||||
// Reorder the access groups alphabetically
|
||||
var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
|
||||
|
||||
// Get the access levels associated with the current tab
|
||||
var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex];
|
||||
_accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto];
|
||||
_accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList();
|
||||
|
||||
// Add an 'all' checkbox as the first child of the list if it has more than one access level
|
||||
// Toggling this checkbox on will mark all other boxes below it on/off
|
||||
var allCheckBox = CreateAccessLevelCheckbox();
|
||||
allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox");
|
||||
|
||||
if (_labelStyleClass != null)
|
||||
allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
|
||||
|
||||
// Add the 'all' checkbox events
|
||||
allCheckBox.OnPressed += args =>
|
||||
{
|
||||
SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed);
|
||||
|
||||
var accessLevels = new HashSet<ProtoId<AccessLevelPrototype>>();
|
||||
|
||||
foreach (var accessLevel in _accessLevelsForTab)
|
||||
{
|
||||
accessLevels.Add(accessLevel);
|
||||
}
|
||||
|
||||
OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed);
|
||||
};
|
||||
|
||||
AccessLevelChecklist.AddChild(allCheckBox);
|
||||
|
||||
// Hide the 'all' checkbox if the tab has only one access level
|
||||
var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
|
||||
|
||||
allCheckBox.Visible = allCheckBoxVisible;
|
||||
allCheckBox.Disabled = !_canInteract;
|
||||
|
||||
// Add any remaining missing access level buttons to the UI
|
||||
foreach (var accessLevel in _accessLevelsForTab)
|
||||
{
|
||||
// Create the entry
|
||||
var accessLevelEntry = new AccessLevelEntry(_isMonotone);
|
||||
|
||||
accessLevelEntry.AccessLevel = accessLevel;
|
||||
accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
|
||||
accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel);
|
||||
accessLevelEntry.CheckBox.Disabled = !_canInteract;
|
||||
|
||||
if (_labelStyleClass != null)
|
||||
accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
|
||||
|
||||
// Set the checkbox linkage lines
|
||||
var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1);
|
||||
|
||||
var lines = new List<(Vector2, Vector2)>
|
||||
{
|
||||
(new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)),
|
||||
(new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)),
|
||||
};
|
||||
|
||||
accessLevelEntry.UpdateCheckBoxLink(lines);
|
||||
accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible;
|
||||
accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White;
|
||||
|
||||
// Add checkbox events
|
||||
accessLevelEntry.CheckBox.OnPressed += args =>
|
||||
{
|
||||
// If the checkbox and its siblings are checked, check the 'all' checkbox too
|
||||
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
|
||||
|
||||
OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed);
|
||||
};
|
||||
|
||||
AccessLevelChecklist.AddChild(accessLevelEntry);
|
||||
_accessLevelEntries.Add(accessLevelEntry);
|
||||
}
|
||||
|
||||
// Press the 'all' checkbox if all others are pressed
|
||||
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
|
||||
}
|
||||
|
||||
private bool AreAllCheckBoxesPressed(IEnumerable<CheckBox> checkBoxes)
|
||||
{
|
||||
foreach (var checkBox in checkBoxes)
|
||||
{
|
||||
if (!checkBox.Pressed)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetCheckBoxPressedState(List<AccessLevelEntry> accessLevelEntries, bool pressed)
|
||||
{
|
||||
foreach (var accessLevelEntry in accessLevelEntries)
|
||||
{
|
||||
accessLevelEntry.CheckBox.Pressed = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Provides the UI with a list of access groups using which list of tabs should be populated.
|
||||
/// </summary>
|
||||
public void SetAccessGroups(HashSet<ProtoId<AccessGroupPrototype>> accessGroups)
|
||||
{
|
||||
_accessGroups = accessGroups;
|
||||
|
||||
ArrangeAccessControls();
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides the UI with a list of access levels with which it can populate the currently selected tab.
|
||||
/// </summary>
|
||||
public void SetAccessLevels(HashSet<ProtoId<AccessLevelPrototype>> accessLevels)
|
||||
{
|
||||
_accessLevels = accessLevels;
|
||||
|
||||
ArrangeAccessControls();
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets which access level checkboxes should be marked on the UI.
|
||||
/// </summary>
|
||||
public void SetActiveAccessLevels(HashSet<ProtoId<AccessLevelPrototype>> activeAccessLevels)
|
||||
{
|
||||
_activeAccessLevels = activeAccessLevels;
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the local player can interact with the checkboxes.
|
||||
/// </summary>
|
||||
public void SetLocalPlayerAccessibility(bool canInteract)
|
||||
{
|
||||
_canInteract = canInteract;
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the UI should use monotone buttons and checkboxes.
|
||||
/// </summary>
|
||||
public void SetMonotone(bool monotone)
|
||||
{
|
||||
_isMonotone = monotone;
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the specified style to the labels on the UI buttons and checkboxes.
|
||||
/// </summary>
|
||||
public void SetLabelStyleClass(string? styleClass)
|
||||
{
|
||||
_labelStyleClass = styleClass;
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
private void OnAccessGroupChanged(int newTabIndex)
|
||||
{
|
||||
if (newTabIndex == _accessGroupTabIndex)
|
||||
return;
|
||||
|
||||
_accessGroupTabIndex = newTabIndex;
|
||||
|
||||
if (TryRebuildAccessGroupControls())
|
||||
RebuildAccessLevelsControls();
|
||||
}
|
||||
|
||||
private Button CreateAccessGroupButton()
|
||||
{
|
||||
var button = _isMonotone ? new MonotoneButton() : new Button();
|
||||
|
||||
button.ToggleMode = true;
|
||||
button.Group = _accessGroupsButtons;
|
||||
button.Label.HorizontalAlignment = HAlignment.Left;
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private CheckBox CreateAccessLevelCheckbox()
|
||||
{
|
||||
var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox();
|
||||
|
||||
checkbox.Margin = new Thickness(0, 0, 0, 3);
|
||||
checkbox.ToggleMode = true;
|
||||
checkbox.ReservesSpace = false;
|
||||
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private sealed class AccessLevelEntry : BoxContainer
|
||||
{
|
||||
public ProtoId<AccessLevelPrototype> AccessLevel;
|
||||
public readonly CheckBox CheckBox;
|
||||
public readonly LineRenderer CheckBoxLink;
|
||||
|
||||
public AccessLevelEntry(bool monotone)
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
|
||||
CheckBoxLink = new LineRenderer
|
||||
{
|
||||
SetWidth = 22,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(0, -1),
|
||||
ReservesSpace = false,
|
||||
};
|
||||
|
||||
AddChild(CheckBoxLink);
|
||||
|
||||
CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox();
|
||||
CheckBox.ToggleMode = true;
|
||||
CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f);
|
||||
|
||||
AddChild(CheckBox);
|
||||
}
|
||||
|
||||
public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
|
||||
{
|
||||
CheckBoxLink.Lines = lines;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LineRenderer : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// List of lines to render (their start and end x-y coordinates).
|
||||
/// Position (0,0) is the top left corner of the control and
|
||||
/// position (1,1) is the bottom right corner.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The color of the lines is inherited from the control.
|
||||
/// </remarks>
|
||||
public List<(Vector2, Vector2)> Lines;
|
||||
|
||||
public LineRenderer()
|
||||
{
|
||||
Lines = new List<(Vector2, Vector2)>();
|
||||
}
|
||||
|
||||
public LineRenderer(List<(Vector2, Vector2)> lines)
|
||||
{
|
||||
Lines = lines;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
foreach (var line in Lines)
|
||||
{
|
||||
var start = PixelPosition +
|
||||
new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y);
|
||||
|
||||
var end = PixelPosition +
|
||||
new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y);
|
||||
|
||||
handle.DrawLine(start, end, ActualModulateSelf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using Content.Shared.Actions.Components;
|
||||
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
|
||||
|
||||
namespace Content.Client.Actions;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,3 +10,17 @@ public sealed class FillActionSlotEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid? Action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side event used to attempt to trigger a targeted action.
|
||||
/// This only gets raised if the has <see cref="TargetActionComponent">.
|
||||
/// Handlers must set <c>Handled</c> to true, then if the action has been performed,
|
||||
/// i.e. a target is found, then FoundTarget must be set to true.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct ActionTargetAttemptEvent(
|
||||
PointerInputCmdArgs Input,
|
||||
Entity<ActionsComponent> User,
|
||||
ActionComponent Action,
|
||||
bool Handled = false,
|
||||
bool FoundTarget = false);
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Charges.Systems;
|
||||
using Content.Shared.Mapping;
|
||||
using Content.Shared.Maps;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
@@ -25,8 +30,8 @@ namespace Content.Client.Actions
|
||||
|
||||
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IResourceManager _resources = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
|
||||
public event Action<EntityUid>? OnActionAdded;
|
||||
@@ -38,131 +43,67 @@ namespace Content.Client.Actions
|
||||
public event Action<List<SlotAssignment>>? AssignSlot;
|
||||
|
||||
private readonly List<EntityUid> _removed = new();
|
||||
private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
|
||||
private readonly List<Entity<ActionComponent>> _added = new();
|
||||
|
||||
public static readonly EntProtoId MappingEntityAction = "BaseMappingEntityAction";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(OnHandleState);
|
||||
|
||||
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
|
||||
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
|
||||
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
|
||||
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
|
||||
SubscribeLocalEvent<ActionComponent, AfterAutoHandleStateEvent>(OnActionAutoHandleState);
|
||||
|
||||
SubscribeLocalEvent<EntityTargetActionComponent, ActionTargetAttemptEvent>(OnEntityTargetAttempt);
|
||||
SubscribeLocalEvent<WorldTargetActionComponent, ActionTargetAttemptEvent>(OnWorldTargetAttempt);
|
||||
}
|
||||
|
||||
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not InstantActionComponentState state)
|
||||
return;
|
||||
|
||||
BaseHandleState<InstantActionComponent>(uid, component, state);
|
||||
private void OnActionAutoHandleState(Entity<ActionComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateAction(ent);
|
||||
}
|
||||
|
||||
private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
|
||||
public override void UpdateAction(Entity<ActionComponent> ent)
|
||||
{
|
||||
if (args.Current is not EntityTargetActionComponentState state)
|
||||
return;
|
||||
|
||||
component.Whitelist = state.Whitelist;
|
||||
component.Blacklist = state.Blacklist;
|
||||
component.CanTargetSelf = state.CanTargetSelf;
|
||||
BaseHandleState<EntityTargetActionComponent>(uid, component, state);
|
||||
}
|
||||
|
||||
private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not WorldTargetActionComponentState state)
|
||||
return;
|
||||
|
||||
BaseHandleState<WorldTargetActionComponent>(uid, component, state);
|
||||
}
|
||||
|
||||
private void OnEntityWorldTargetHandleState(EntityUid uid,
|
||||
EntityWorldTargetActionComponent component,
|
||||
ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not EntityWorldTargetActionComponentState state)
|
||||
return;
|
||||
|
||||
component.Whitelist = state.Whitelist;
|
||||
component.CanTargetSelf = state.CanTargetSelf;
|
||||
BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state);
|
||||
}
|
||||
|
||||
private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
|
||||
{
|
||||
// TODO ACTIONS use auto comp states
|
||||
component.Icon = state.Icon;
|
||||
component.IconOn = state.IconOn;
|
||||
component.IconColor = state.IconColor;
|
||||
component.OriginalIconColor = state.OriginalIconColor;
|
||||
component.DisabledIconColor = state.DisabledIconColor;
|
||||
component.Keywords.Clear();
|
||||
component.Keywords.UnionWith(state.Keywords);
|
||||
component.Enabled = state.Enabled;
|
||||
component.Toggled = state.Toggled;
|
||||
component.Cooldown = state.Cooldown;
|
||||
component.UseDelay = state.UseDelay;
|
||||
component.Container = EnsureEntity<T>(state.Container, uid);
|
||||
component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
|
||||
component.CheckCanInteract = state.CheckCanInteract;
|
||||
component.CheckConsciousness = state.CheckConsciousness;
|
||||
component.ClientExclusive = state.ClientExclusive;
|
||||
component.Priority = state.Priority;
|
||||
component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
|
||||
component.RaiseOnUser = state.RaiseOnUser;
|
||||
component.RaiseOnAction = state.RaiseOnAction;
|
||||
component.AutoPopulate = state.AutoPopulate;
|
||||
component.Temporary = state.Temporary;
|
||||
component.ItemIconStyle = state.ItemIconStyle;
|
||||
component.Sound = state.Sound;
|
||||
|
||||
UpdateAction(uid, component);
|
||||
}
|
||||
|
||||
public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
|
||||
{
|
||||
if (!ResolveActionData(actionId, ref action))
|
||||
return;
|
||||
|
||||
// TODO: Decouple this.
|
||||
action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
|
||||
|
||||
base.UpdateAction(actionId, action);
|
||||
if (_playerManager.LocalEntity != action.AttachedEntity)
|
||||
ent.Comp.IconColor = _sharedCharges.GetCurrentCharges(ent.Owner) == 0 ? ent.Comp.DisabledIconColor : ent.Comp.OriginalIconColor;
|
||||
base.UpdateAction(ent);
|
||||
if (_playerManager.LocalEntity != ent.Comp.AttachedEntity)
|
||||
return;
|
||||
|
||||
ActionsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
|
||||
private void OnHandleState(Entity<ActionsComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not ActionsComponentState state)
|
||||
return;
|
||||
|
||||
var (uid, comp) = ent;
|
||||
_added.Clear();
|
||||
_removed.Clear();
|
||||
var stateEnts = EnsureEntitySet<ActionsComponent>(state.Actions, uid);
|
||||
foreach (var act in component.Actions)
|
||||
foreach (var act in comp.Actions)
|
||||
{
|
||||
if (!stateEnts.Contains(act) && !IsClientSide(act))
|
||||
_removed.Add(act);
|
||||
}
|
||||
component.Actions.ExceptWith(_removed);
|
||||
comp.Actions.ExceptWith(_removed);
|
||||
|
||||
foreach (var actionId in stateEnts)
|
||||
{
|
||||
if (!actionId.IsValid())
|
||||
continue;
|
||||
|
||||
if (!component.Actions.Add(actionId))
|
||||
if (!comp.Actions.Add(actionId))
|
||||
continue;
|
||||
|
||||
TryGetActionData(actionId, out var action);
|
||||
_added.Add((actionId, action));
|
||||
if (GetAction(actionId) is {} action)
|
||||
_added.Add(action);
|
||||
}
|
||||
|
||||
if (_playerManager.LocalEntity != uid)
|
||||
@@ -177,47 +118,46 @@ namespace Content.Client.Actions
|
||||
|
||||
foreach (var action in _added)
|
||||
{
|
||||
OnActionAdded?.Invoke(action.Item1);
|
||||
OnActionAdded?.Invoke(action);
|
||||
}
|
||||
|
||||
ActionsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
|
||||
public static int ActionComparer(Entity<ActionComponent> a, Entity<ActionComponent> b)
|
||||
{
|
||||
var priorityA = a.Item2?.Priority ?? 0;
|
||||
var priorityB = b.Item2?.Priority ?? 0;
|
||||
var priorityA = a.Comp?.Priority ?? 0;
|
||||
var priorityB = b.Comp?.Priority ?? 0;
|
||||
if (priorityA != priorityB)
|
||||
return priorityA - priorityB;
|
||||
|
||||
priorityA = a.Item2?.Container?.Id ?? 0;
|
||||
priorityB = b.Item2?.Container?.Id ?? 0;
|
||||
priorityA = a.Comp?.Container?.Id ?? 0;
|
||||
priorityB = b.Comp?.Container?.Id ?? 0;
|
||||
return priorityA - priorityB;
|
||||
}
|
||||
|
||||
protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
|
||||
BaseActionComponent action)
|
||||
protected override void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
|
||||
{
|
||||
if (_playerManager.LocalEntity != performer)
|
||||
if (_playerManager.LocalEntity != performer.Owner)
|
||||
return;
|
||||
|
||||
OnActionAdded?.Invoke(actionId);
|
||||
OnActionAdded?.Invoke(action);
|
||||
ActionsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
|
||||
protected override void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
|
||||
{
|
||||
if (_playerManager.LocalEntity != performer)
|
||||
if (_playerManager.LocalEntity != performer.Owner)
|
||||
return;
|
||||
|
||||
OnActionRemoved?.Invoke(actionId);
|
||||
OnActionRemoved?.Invoke(action);
|
||||
ActionsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
|
||||
public IEnumerable<Entity<ActionComponent>> GetClientActions()
|
||||
{
|
||||
if (_playerManager.LocalEntity is not { } user)
|
||||
return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
|
||||
return Enumerable.Empty<Entity<ActionComponent>>();
|
||||
|
||||
return GetActions(user);
|
||||
}
|
||||
@@ -254,24 +194,23 @@ namespace Content.Client.Actions
|
||||
CommandBinds.Unregister<ActionsSystem>();
|
||||
}
|
||||
|
||||
public void TriggerAction(EntityUid actionId, BaseActionComponent action)
|
||||
public void TriggerAction(Entity<ActionComponent> action)
|
||||
{
|
||||
if (_playerManager.LocalEntity is not { } user ||
|
||||
!TryComp(user, out ActionsComponent? actions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (action is not InstantActionComponent instantAction)
|
||||
if (_playerManager.LocalEntity is not { } user)
|
||||
return;
|
||||
|
||||
if (action.ClientExclusive)
|
||||
// TODO: unhardcode this somehow
|
||||
|
||||
if (!HasComp<InstantActionComponent>(action))
|
||||
return;
|
||||
|
||||
if (action.Comp.ClientExclusive)
|
||||
{
|
||||
PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
PerformAction(user, action);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new RequestPerformActionEvent(GetNetEntity(actionId));
|
||||
var request = new RequestPerformActionEvent(GetNetEntity(action));
|
||||
EntityManager.RaisePredictiveEvent(request);
|
||||
}
|
||||
}
|
||||
@@ -295,39 +234,137 @@ namespace Content.Client.Actions
|
||||
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
|
||||
return;
|
||||
|
||||
var actions = EnsureComp<ActionsComponent>(user);
|
||||
|
||||
ClearAssignments?.Invoke();
|
||||
|
||||
var assignments = new List<SlotAssignment>();
|
||||
|
||||
foreach (var entry in sequence.Sequence)
|
||||
{
|
||||
if (entry is not MappingDataNode map)
|
||||
continue;
|
||||
|
||||
if (!map.TryGet("action", out var actionNode))
|
||||
continue;
|
||||
|
||||
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
|
||||
var actionId = Spawn();
|
||||
AddComp(actionId, action);
|
||||
AddActionDirect(user, actionId);
|
||||
|
||||
if (map.TryGet<ValueDataNode>("name", out var nameNode))
|
||||
_metaData.SetEntityName(actionId, nameNode.Value);
|
||||
|
||||
if (!map.TryGet("assignments", out var assignmentNode))
|
||||
continue;
|
||||
|
||||
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
|
||||
|
||||
foreach (var index in nodeAssignments)
|
||||
var actionId = EntityUid.Invalid;
|
||||
if (map.TryGet<ValueDataNode>("action", out var actionNode))
|
||||
{
|
||||
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
|
||||
assignments.Add(assignment);
|
||||
var id = new EntProtoId(actionNode.Value);
|
||||
actionId = Spawn(id);
|
||||
}
|
||||
else if (map.TryGet<ValueDataNode>("entity", out var entityNode))
|
||||
{
|
||||
var id = new EntProtoId(entityNode.Value);
|
||||
var proto = _proto.Index(id);
|
||||
actionId = Spawn(MappingEntityAction);
|
||||
SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id));
|
||||
SetEvent(actionId, new StartPlacementActionEvent()
|
||||
{
|
||||
PlacementOption = "SnapgridCenter",
|
||||
EntityType = id
|
||||
});
|
||||
_metaData.SetEntityName(actionId, proto.Name);
|
||||
}
|
||||
else if (map.TryGet<ValueDataNode>("tileId", out var tileNode))
|
||||
{
|
||||
var id = new ProtoId<ContentTileDefinition>(tileNode.Value);
|
||||
var proto = _proto.Index(id);
|
||||
actionId = Spawn(MappingEntityAction);
|
||||
if (proto.Sprite is {} sprite)
|
||||
SetIcon(actionId, new SpriteSpecifier.Texture(sprite));
|
||||
SetEvent(actionId, new StartPlacementActionEvent()
|
||||
{
|
||||
PlacementOption = "AlignTileAny",
|
||||
TileId = id
|
||||
});
|
||||
_metaData.SetEntityName(actionId, Loc.GetString(proto.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Mapping actions from {path} had unknown action data!");
|
||||
continue;
|
||||
}
|
||||
|
||||
AddActionDirect((user, actions), actionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var (uid, comp) = ent;
|
||||
var action = args.Action;
|
||||
var coords = args.Input.Coordinates;
|
||||
var user = args.User;
|
||||
|
||||
if (!ValidateWorldTarget(user, coords, ent))
|
||||
return;
|
||||
|
||||
// optionally send the clicked entity too, if it matches its whitelist etc
|
||||
// this is the actual entity-world targeting magic
|
||||
EntityUid? targetEnt = null;
|
||||
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
|
||||
args.Input.EntityUid != null &&
|
||||
ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
|
||||
{
|
||||
targetEnt = args.Input.EntityUid;
|
||||
}
|
||||
|
||||
AssignSlot?.Invoke(assignments);
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
// TODO: abstract away from single event or maybe just RaiseLocalEvent?
|
||||
if (comp.Event is {} ev)
|
||||
{
|
||||
ev.Target = coords;
|
||||
ev.Entity = targetEnt;
|
||||
}
|
||||
|
||||
PerformAction((user, user.Comp), (uid, action));
|
||||
}
|
||||
else
|
||||
RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(targetEnt), GetNetCoordinates(coords)));
|
||||
|
||||
args.FoundTarget = true;
|
||||
}
|
||||
|
||||
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
|
||||
{
|
||||
if (args.Handled || args.Input.EntityUid is not { Valid: true } entity)
|
||||
return;
|
||||
|
||||
// let world target component handle it
|
||||
var (uid, comp) = ent;
|
||||
if (comp.Event is not {} ev)
|
||||
{
|
||||
DebugTools.Assert(HasComp<WorldTargetActionComponent>(ent), $"Action {ToPrettyString(ent)} requires WorldTargetActionComponent for entity-world targeting");
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var action = args.Action;
|
||||
var user = args.User;
|
||||
|
||||
if (!ValidateEntityTarget(user, entity, ent))
|
||||
return;
|
||||
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
ev.Target = entity;
|
||||
|
||||
PerformAction((user, user.Comp), (uid, action));
|
||||
}
|
||||
else
|
||||
{
|
||||
RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(entity)));
|
||||
}
|
||||
|
||||
args.FoundTarget = true;
|
||||
}
|
||||
|
||||
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Control
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Margin="4"
|
||||
MinSize="50 50">
|
||||
<GridContainer
|
||||
Columns="3">
|
||||
<cc:CommandButton Command="startround" Text="{Loc administration-ui-round-tab-start-round}" />
|
||||
<cc:CommandButton Command="endround" Text="{Loc administration-ui-round-tab-end-round}" />
|
||||
<cc:CommandButton Command="restartround" Text="{Loc administration-ui-round-tab-restart-round}" />
|
||||
<cc:CommandButton Command="restartroundnow" Text="{Loc administration-ui-round-tab-restart-round-now}" />
|
||||
<controls:ConfirmButton Name="StartRound" Text="{Loc administration-ui-round-tab-start-round}" />
|
||||
<controls:ConfirmButton Name="EndRound" Text="{Loc administration-ui-round-tab-end-round}" />
|
||||
<controls:ConfirmButton Name="RestartRound" Text="{Loc administration-ui-round-tab-restart-round}" />
|
||||
<controls:ConfirmButton Name="RestartRoundNow" Text="{Loc administration-ui-round-tab-restart-round-now}" />
|
||||
</GridContainer>
|
||||
</Control>
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RoundTab : Control
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
|
||||
public RoundTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
StartRound.OnPressed += _ => _console.ExecuteCommand("startround");
|
||||
EndRound.OnPressed += _ => _console.ExecuteCommand("endround");
|
||||
RestartRound.OnPressed += _ => _console.ExecuteCommand("restartround");
|
||||
RestartRoundNow.OnPressed += _ => _console.ExecuteCommand("restartroundnow");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<Control
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Margin="4"
|
||||
MinSize="50 50">
|
||||
<GridContainer
|
||||
Columns="4" >
|
||||
<cc:CommandButton Command="shutdown" Text="{Loc server-shutdown}" />
|
||||
<controls:ConfirmButton Name="ServerShutdownButton" Text="{Loc server-shutdown}" />
|
||||
<cc:CommandButton Name="SetOocButton" Command="setooc" Text="{Loc server-ooc-toggle}" ToggleMode="True" />
|
||||
<cc:CommandButton Name="SetLoocButton" Command="setlooc" Text="{Loc server-looc-toggle}" ToggleMode="True" />
|
||||
</GridContainer>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -10,6 +11,7 @@ namespace Content.Client.Administration.UI.Tabs
|
||||
public sealed partial class ServerTab : Control
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
|
||||
public ServerTab()
|
||||
{
|
||||
@@ -18,6 +20,8 @@ namespace Content.Client.Administration.UI.Tabs
|
||||
|
||||
_config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true);
|
||||
_config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true);
|
||||
|
||||
ServerShutdownButton.OnPressed += _ => _console.ExecuteCommand("shutdown");
|
||||
}
|
||||
|
||||
private void OocEnabledChanged(bool value)
|
||||
|
||||
@@ -20,6 +20,7 @@ public sealed class AnomalyScannerBoundUserInterface : BoundUserInterface
|
||||
|
||||
_menu = new AnomalyScannerMenu();
|
||||
_menu.OpenCentered();
|
||||
_menu.OnClose += Close;
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
|
||||
203
Content.Client/Atmos/AlignAtmosPipeLayers.cs
Normal file
203
Content.Client/Atmos/AlignAtmosPipeLayers.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using Content.Client.Construction;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Placement.Modes;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Numerics;
|
||||
using static Robust.Client.Placement.PlacementManager;
|
||||
|
||||
namespace Content.Client.Atmos;
|
||||
|
||||
/// <summary>
|
||||
/// Allows users to place atmos pipes on different layers depending on how the mouse cursor is positioned within a grid tile.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This placement mode is not on the engine because it is content specific.
|
||||
/// </remarks>
|
||||
public sealed class AlignAtmosPipeLayers : SnapgridCenter
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
private readonly SharedMapSystem _mapSystem;
|
||||
private readonly SharedTransformSystem _transformSystem;
|
||||
private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
private const float SearchBoxSize = 2f;
|
||||
private EntityCoordinates _unalignedMouseCoords = default;
|
||||
private const float MouseDeadzoneRadius = 0.25f;
|
||||
|
||||
private Color _guideColor = new Color(0, 0, 0.5785f);
|
||||
private const float GuideRadius = 0.1f;
|
||||
private const float GuideOffset = 0.21875f;
|
||||
|
||||
public AlignAtmosPipeLayers(PlacementManager pMan) : base(pMan)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
_transformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
_pipeLayersSystem = _entityManager.System<SharedAtmosPipeLayersSystem>();
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Render(in OverlayDrawArgs args)
|
||||
{
|
||||
var gridUid = _entityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
|
||||
|
||||
if (gridUid == null || Grid == null)
|
||||
return;
|
||||
|
||||
// Draw guide circles for each pipe layer if we are not in line/grid placing mode
|
||||
if (pManager.PlacementType == PlacementTypes.None)
|
||||
{
|
||||
var gridRotation = _transformSystem.GetWorldRotation(gridUid.Value);
|
||||
var worldPosition = _mapSystem.LocalToWorld(gridUid.Value, Grid, MouseCoords.Position);
|
||||
var direction = (_eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
|
||||
var multi = (direction == Direction.North || direction == Direction.South) ? -1f : 1f;
|
||||
|
||||
args.WorldHandle.DrawCircle(worldPosition, GuideRadius, _guideColor);
|
||||
args.WorldHandle.DrawCircle(worldPosition + gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
|
||||
args.WorldHandle.DrawCircle(worldPosition - gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
|
||||
}
|
||||
|
||||
base.Render(args);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
|
||||
{
|
||||
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
|
||||
base.AlignPlacementMode(mouseScreen);
|
||||
|
||||
// Exit early if we are in line/grid placing mode
|
||||
if (pManager.PlacementType != PlacementTypes.None)
|
||||
return;
|
||||
|
||||
MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
|
||||
|
||||
var gridId = _transformSystem.GetGrid(MouseCoords);
|
||||
|
||||
if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
|
||||
return;
|
||||
|
||||
var gridRotation = _transformSystem.GetWorldRotation(gridId.Value);
|
||||
CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
|
||||
|
||||
float tileSize = mapGrid.TileSize;
|
||||
GridDistancing = tileSize;
|
||||
|
||||
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
|
||||
CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
|
||||
|
||||
// Calculate the position of the mouse cursor with respect to the center of the tile to determine which layer to use
|
||||
var mouseCoordsDiff = _unalignedMouseCoords.Position - MouseCoords.Position;
|
||||
var layer = AtmosPipeLayer.Primary;
|
||||
|
||||
if (mouseCoordsDiff.Length() > MouseDeadzoneRadius)
|
||||
{
|
||||
// Determine the direction of the mouse is relative to the center of the tile, adjusting for the player eye and grid rotation
|
||||
var direction = (new Angle(mouseCoordsDiff) + _eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
|
||||
layer = (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary;
|
||||
}
|
||||
|
||||
// Update the construction menu placer
|
||||
if (pManager.Hijack != null)
|
||||
UpdateHijackedPlacer(layer, mouseScreen);
|
||||
|
||||
// Otherwise update the debug placer
|
||||
else
|
||||
UpdatePlacer(layer);
|
||||
}
|
||||
|
||||
private void UpdateHijackedPlacer(AtmosPipeLayer layer, ScreenCoordinates mouseScreen)
|
||||
{
|
||||
// Try to get alternative prototypes from the construction prototype
|
||||
var constructionSystem = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentConstructionSystem;
|
||||
var altPrototypes = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.AlternativePrototypes;
|
||||
|
||||
if (constructionSystem == null || altPrototypes == null || (int)layer >= altPrototypes.Length)
|
||||
return;
|
||||
|
||||
var newProtoId = altPrototypes[(int)layer];
|
||||
|
||||
if (!_protoManager.TryIndex(newProtoId, out var newProto))
|
||||
return;
|
||||
|
||||
if (newProto.Type != ConstructionType.Structure)
|
||||
{
|
||||
pManager.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newProto.ID == (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.ID)
|
||||
return;
|
||||
|
||||
// Start placing
|
||||
pManager.BeginPlacing(new PlacementInformation()
|
||||
{
|
||||
IsTile = false,
|
||||
PlacementOption = newProto.PlacementMode,
|
||||
}, new ConstructionPlacementHijack(constructionSystem, newProto));
|
||||
|
||||
if (pManager.CurrentMode is AlignAtmosPipeLayers { } newMode)
|
||||
newMode.RefreshGrid(mouseScreen);
|
||||
|
||||
// Update construction guide
|
||||
constructionSystem.GetGuide(newProto);
|
||||
}
|
||||
|
||||
private void UpdatePlacer(AtmosPipeLayer layer)
|
||||
{
|
||||
// Try to get alternative prototypes from the entity atmos pipe layer component
|
||||
if (pManager.CurrentPermission?.EntityType == null)
|
||||
return;
|
||||
|
||||
if (!_protoManager.TryIndex<EntityPrototype>(pManager.CurrentPermission.EntityType, out var currentProto))
|
||||
return;
|
||||
|
||||
if (!currentProto.TryGetComponent<AtmosPipeLayersComponent>(out var atmosPipeLayers, _entityManager.ComponentFactory))
|
||||
return;
|
||||
|
||||
if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
|
||||
return;
|
||||
|
||||
if (_protoManager.TryIndex<EntityPrototype>(newProtoId, out var newProto))
|
||||
{
|
||||
// Update the placed prototype
|
||||
pManager.CurrentPermission.EntityType = newProtoId;
|
||||
|
||||
// Update the appearance of the ghost sprite
|
||||
if (newProto.TryGetComponent<SpriteComponent>(out var sprite, _entityManager.ComponentFactory))
|
||||
{
|
||||
var textures = new List<IDirectionalTextureProvider>();
|
||||
|
||||
foreach (var spriteLayer in sprite.AllLayers)
|
||||
{
|
||||
if (spriteLayer.ActualRsi?.Path != null && spriteLayer.RsiState.Name != null)
|
||||
textures.Add(_spriteSystem.RsiStateLike(new SpriteSpecifier.Rsi(spriteLayer.ActualRsi.Path, spriteLayer.RsiState.Name)));
|
||||
}
|
||||
|
||||
pManager.CurrentTextures = textures;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshGrid(ScreenCoordinates mouseScreen)
|
||||
{
|
||||
base.AlignPlacementMode(mouseScreen);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
public int? FocusNetId = null;
|
||||
|
||||
private const int ChunkSize = 4;
|
||||
private const float ScaleModifier = 4f;
|
||||
|
||||
private readonly float[] _layerFraction = { 0.5f, 0.75f, 0.25f };
|
||||
private const float LineThickness = 0.05f;
|
||||
|
||||
private readonly Color _basePipeNetColor = Color.LightGray;
|
||||
private readonly Color _unfocusedPipeNetColor = Color.DimGray;
|
||||
@@ -95,23 +99,23 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
foreach (var chunkedLine in atmosPipeNetwork)
|
||||
{
|
||||
var leftTop = ScalePosition(new Vector2
|
||||
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
|
||||
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
|
||||
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
|
||||
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
|
||||
- offset);
|
||||
|
||||
var rightTop = ScalePosition(new Vector2
|
||||
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
|
||||
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
|
||||
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
|
||||
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
|
||||
- offset);
|
||||
|
||||
var leftBottom = ScalePosition(new Vector2
|
||||
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
|
||||
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
|
||||
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
|
||||
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
|
||||
- offset);
|
||||
|
||||
var rightBottom = ScalePosition(new Vector2
|
||||
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
|
||||
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
|
||||
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
|
||||
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
|
||||
- offset);
|
||||
|
||||
if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
|
||||
@@ -142,7 +146,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
if (chunks == null || grid == null)
|
||||
return decodedOutput;
|
||||
|
||||
// Clear stale look up table values
|
||||
// Clear stale look up table values
|
||||
_horizLines.Clear();
|
||||
_horizLinesReversed.Clear();
|
||||
_vertLines.Clear();
|
||||
@@ -158,7 +162,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
{
|
||||
var list = new List<AtmosMonitoringConsoleLine>();
|
||||
|
||||
foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
{
|
||||
// Determine the correct coloration for the pipe
|
||||
var color = Color.FromHex(hexColor) * _basePipeNetColor;
|
||||
@@ -191,6 +195,9 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
_vertLinesReversed[color] = vertLinesReversed;
|
||||
}
|
||||
|
||||
var layerFraction = _layerFraction[(int)layer];
|
||||
var origin = new Vector2(grid.TileSize * layerFraction, -grid.TileSize * layerFraction);
|
||||
|
||||
// Loop over the chunk
|
||||
for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
|
||||
{
|
||||
@@ -208,21 +215,22 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
|
||||
// Calculate the draw point offsets
|
||||
var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 1f) : origin;
|
||||
|
||||
var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 0f) : origin;
|
||||
|
||||
var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
new Vector2(grid.TileSize * 1f, -grid.TileSize * layerFraction) : origin;
|
||||
|
||||
var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
new Vector2(grid.TileSize * 0f, -grid.TileSize * layerFraction) : origin;
|
||||
|
||||
// Since we can have pipe lines that have a length of a half tile,
|
||||
// double the vectors and convert to vector2i so we can merge them
|
||||
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
|
||||
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
|
||||
// Scale up the vectors and convert to vector2i so we can merge them
|
||||
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, ScaleModifier),
|
||||
ConvertVector2ToVector2i(tile + horizLineTerminus, ScaleModifier), horizLines, horizLinesReversed);
|
||||
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, ScaleModifier),
|
||||
ConvertVector2ToVector2i(tile + vertLineTerminus, ScaleModifier), vertLines, vertLinesReversed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,7 +243,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
|
||||
foreach (var (origin, terminal) in horizLines)
|
||||
decodedOutput.Add(new AtmosMonitoringConsoleLine
|
||||
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
|
||||
(ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
|
||||
}
|
||||
|
||||
foreach (var (color, vertLines) in _vertLines)
|
||||
@@ -245,7 +253,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
|
||||
foreach (var (origin, terminal) in vertLines)
|
||||
decodedOutput.Add(new AtmosMonitoringConsoleLine
|
||||
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
|
||||
(ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
|
||||
}
|
||||
|
||||
return decodedOutput;
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
|
||||
private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
|
||||
Dictionary<Vector2i, Dictionary<AtmosMonitoringConsoleSubnet, ulong>> modifiedChunks;
|
||||
Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices;
|
||||
|
||||
switch (args.Current)
|
||||
@@ -54,7 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
foreach (var (origin, chunk) in modifiedChunks)
|
||||
{
|
||||
var newChunk = new AtmosPipeChunk(origin);
|
||||
newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
|
||||
newChunk.AtmosPipeData = new Dictionary<AtmosMonitoringConsoleSubnet, ulong>(chunk);
|
||||
|
||||
component.AtmosPipeChunks[origin] = newChunk;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Atmos.Consoles;
|
||||
|
||||
@@ -33,6 +34,8 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
|
||||
private ProtoId<NavMapBlipPrototype> _navMapConsoleProtoId = "NavMapConsole";
|
||||
private ProtoId<NavMapBlipPrototype> _gasPipeSensorProtoId = "GasPipeSensor";
|
||||
|
||||
private readonly Vector2[] _pipeLayerOffsets = { new Vector2(0f, 0f), new Vector2(0.25f, 0.25f), new Vector2(-0.25f, -0.25f) };
|
||||
|
||||
public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -53,7 +56,7 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
|
||||
consoleCoords = xform.Coordinates;
|
||||
NavMap.MapUid = xform.GridUid;
|
||||
|
||||
// Assign station name
|
||||
// Assign station name
|
||||
if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
|
||||
stationName = stationMetaData.EntityName;
|
||||
|
||||
@@ -238,6 +241,10 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
|
||||
|
||||
var blinks = proto.Blinks || _focusEntity == metaData.NetEntity;
|
||||
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
|
||||
|
||||
if (proto.Placement == NavMapBlipPlacement.Offset && metaData.PipeLayer > 0)
|
||||
coords = coords.Offset(_pipeLayerOffsets[(int)metaData.PipeLayer]);
|
||||
|
||||
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale);
|
||||
NavMap.TrackedEntities[metaData.NetEntity] = blip;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Client.SubFloor;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -8,7 +9,7 @@ using Robust.Client.GameObjects;
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AtmosPipeAppearanceSystem : EntitySystem
|
||||
public sealed partial class AtmosPipeAppearanceSystem : SharedAtmosPipeAppearanceSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
@@ -26,26 +27,37 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
|
||||
if (!TryComp(uid, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out _);
|
||||
|
||||
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
|
||||
{
|
||||
var layer = _sprite.LayerMapReserve((uid, sprite), layerKey);
|
||||
_sprite.LayerSetRsi((uid, sprite), layer, component.Sprite.RsiPath);
|
||||
_sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite.RsiState);
|
||||
_sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey));
|
||||
for (byte i = 0; i < numberOfPipeLayers; i++)
|
||||
{
|
||||
var layerName = layerKey.ToString() + i.ToString();
|
||||
var layer = _sprite.LayerMapReserve((uid, sprite), layerName);
|
||||
_sprite.LayerSetRsi((uid, sprite), layer, component.Sprite[i].RsiPath);
|
||||
_sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite[i].RsiState);
|
||||
_sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HideAllPipeConnection(Entity<SpriteComponent> entity)
|
||||
private void HideAllPipeConnection(Entity<SpriteComponent> entity, AtmosPipeLayersComponent? atmosPipeLayers, int numberOfPipeLayers)
|
||||
{
|
||||
var sprite = entity.Comp;
|
||||
|
||||
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerKey, out var key, false))
|
||||
continue;
|
||||
for (byte i = 0; i < numberOfPipeLayers; i++)
|
||||
{
|
||||
var layerName = layerKey.ToString() + i.ToString();
|
||||
|
||||
var layer = sprite[key];
|
||||
layer.Visible = false;
|
||||
if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerName, out var key, false))
|
||||
continue;
|
||||
|
||||
var layer = sprite[key];
|
||||
layer.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,33 +73,45 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<PipeDirection>(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component))
|
||||
var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out var atmosPipeLayers);
|
||||
|
||||
if (!_appearance.TryGetData<int>(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component))
|
||||
{
|
||||
HideAllPipeConnection((uid, args.Sprite));
|
||||
HideAllPipeConnection((uid, args.Sprite), atmosPipeLayers, numberOfPipeLayers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<Color>(uid, PipeColorVisuals.Color, out var color, args.Component))
|
||||
color = Color.White;
|
||||
|
||||
// transform connected directions to local-coordinates
|
||||
var connectedDirections = worldConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation);
|
||||
|
||||
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
|
||||
for (byte i = 0; i < numberOfPipeLayers; i++)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerKey, out var key, false))
|
||||
continue;
|
||||
// Extract the cardinal pipe orientations for the current pipe layer
|
||||
// '15' is the four bit mask that is used to extract the pipe orientations of interest from 'worldConnectedDirections'
|
||||
// Fun fact: a collection of four bits is called a 'nibble'! They aren't natively supported :(
|
||||
var pipeLayerConnectedDirections = (PipeDirection)(15 & (worldConnectedDirections >> (PipeDirectionHelpers.PipeDirections * i)));
|
||||
|
||||
var layer = args.Sprite[key];
|
||||
var dir = (PipeDirection)layerKey;
|
||||
var visible = connectedDirections.HasDirection(dir);
|
||||
// Transform the connected directions to local-coordinates
|
||||
var connectedDirections = pipeLayerConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation);
|
||||
|
||||
layer.Visible &= visible;
|
||||
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
|
||||
{
|
||||
var layerName = layerKey.ToString() + i.ToString();
|
||||
|
||||
if (!visible)
|
||||
continue;
|
||||
if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerName, out var key, false))
|
||||
continue;
|
||||
|
||||
layer.Color = color;
|
||||
var layer = args.Sprite[key];
|
||||
var dir = (PipeDirection)layerKey;
|
||||
var visible = connectedDirections.HasDirection(dir);
|
||||
|
||||
layer.Visible &= visible;
|
||||
|
||||
if (!visible)
|
||||
continue;
|
||||
|
||||
layer.Color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
56
Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
Normal file
56
Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// The system responsible for updating the appearance of layered gas pipe
|
||||
/// </summary>
|
||||
public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflection = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AtmosPipeLayersComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(Entity<AtmosPipeLayersComponent> ent, ref AppearanceChangeEvent ev)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
|
||||
if (_appearance.TryGetData<string>(ent, AtmosPipeLayerVisuals.Sprite, out var spriteRsi) &&
|
||||
_resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / spriteRsi, out RSIResource? resource))
|
||||
{
|
||||
_sprite.SetBaseRsi((ent, sprite), resource.RSI);
|
||||
}
|
||||
|
||||
if (_appearance.TryGetData<Dictionary<string, string>>(ent, AtmosPipeLayerVisuals.SpriteLayers, out var pipeState))
|
||||
{
|
||||
foreach (var (layerKey, rsiPath) in pipeState)
|
||||
{
|
||||
if (TryParseKey(layerKey, out var @enum))
|
||||
_sprite.LayerSetRsi((ent, sprite), @enum, new ResPath(rsiPath));
|
||||
else
|
||||
_sprite.LayerSetRsi((ent, sprite), layerKey, new ResPath(rsiPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseKey(string keyString, [NotNullWhen(true)] out Enum? @enum)
|
||||
{
|
||||
return _reflection.TryParseEnumReference(keyString, out @enum);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ namespace Content.Client.Atmos.EntitySystems;
|
||||
/// </summary>
|
||||
public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly PointLightSystem _lights = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -33,9 +32,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
|
||||
// Need LayerMapTryGet because Init fails if there's no existing sprite / appearancecomp
|
||||
// which means in some setups (most frequently no AppearanceComp) the layer never exists.
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite) &&
|
||||
_sprite.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false))
|
||||
SpriteSystem.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false))
|
||||
{
|
||||
_sprite.RemoveLayer((uid, sprite), layer);
|
||||
SpriteSystem.RemoveLayer((uid, sprite), layer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +43,11 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite) || !TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_sprite.LayerMapReserve((uid, sprite), FireVisualLayers.Fire);
|
||||
_sprite.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false);
|
||||
SpriteSystem.LayerMapReserve((uid, sprite), FireVisualLayers.Fire);
|
||||
SpriteSystem.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false);
|
||||
sprite.LayerSetShader(FireVisualLayers.Fire, "unshaded");
|
||||
if (component.Sprite != null)
|
||||
_sprite.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite));
|
||||
SpriteSystem.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite));
|
||||
|
||||
UpdateAppearance(uid, component, sprite, appearance);
|
||||
}
|
||||
@@ -61,12 +60,12 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
|
||||
|
||||
private void UpdateAppearance(EntityUid uid, FireVisualsComponent component, SpriteComponent sprite, AppearanceComponent appearance)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var index, false))
|
||||
if (!SpriteSystem.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var index, false))
|
||||
return;
|
||||
|
||||
AppearanceSystem.TryGetData<bool>(uid, FireVisuals.OnFire, out var onFire, appearance);
|
||||
AppearanceSystem.TryGetData<float>(uid, FireVisuals.FireStacks, out var fireStacks, appearance);
|
||||
_sprite.LayerSetVisible((uid, sprite), index, onFire);
|
||||
SpriteSystem.LayerSetVisible((uid, sprite), index, onFire);
|
||||
|
||||
if (!onFire)
|
||||
{
|
||||
@@ -80,9 +79,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
|
||||
}
|
||||
|
||||
if (fireStacks > component.FireStackAlternateState && !string.IsNullOrEmpty(component.AlternateState))
|
||||
_sprite.LayerSetRsiState((uid, sprite), index, component.AlternateState);
|
||||
SpriteSystem.LayerSetRsiState((uid, sprite), index, component.AlternateState);
|
||||
else
|
||||
_sprite.LayerSetRsiState((uid, sprite), index, component.NormalState);
|
||||
SpriteSystem.LayerSetRsiState((uid, sprite), index, component.NormalState);
|
||||
|
||||
component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default));
|
||||
var light = EnsureComp<PointLightComponent>(component.LightEntity.Value);
|
||||
|
||||
@@ -7,11 +7,9 @@ namespace Content.Client.Atmos.Monitor;
|
||||
|
||||
public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmableVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null || !_sprite.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false))
|
||||
if (args.Sprite == null || !SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false))
|
||||
return;
|
||||
|
||||
if (!args.AppearanceData.TryGetValue(PowerDeviceVisuals.Powered, out var poweredObject) ||
|
||||
@@ -24,8 +22,8 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
|
||||
{
|
||||
foreach (var visLayer in component.HideOnDepowered)
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((uid, args.Sprite), visLayer, out var powerVisibilityLayer, false))
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), powerVisibilityLayer, powered);
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), visLayer, out var powerVisibilityLayer, false))
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), powerVisibilityLayer, powered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +31,8 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
|
||||
{
|
||||
foreach (var (setLayer, powerState) in component.SetOnDepowered)
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((uid, args.Sprite), setLayer, out var setStateLayer, false))
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), setStateLayer, new RSI.StateId(powerState));
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), setLayer, out var setStateLayer, false))
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), setStateLayer, new RSI.StateId(powerState));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +41,7 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
|
||||
&& powered
|
||||
&& component.AlarmStates.TryGetValue(alarmType, out var state))
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, new RSI.StateId(state));
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, new RSI.StateId(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Content.Client.Atmos.UI
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
|
||||
_window.OnClose += Close;
|
||||
}
|
||||
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
@@ -29,15 +30,6 @@ namespace Content.Client.Atmos.UI
|
||||
_window.Populate(cast);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes UI and tells the server to disable the analyzer
|
||||
/// </summary>
|
||||
private void OnClose()
|
||||
{
|
||||
SendMessage(new GasAnalyzerDisableMessage());
|
||||
Close();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -136,6 +136,7 @@ namespace Content.Client.Atmos.UI
|
||||
else
|
||||
{
|
||||
// oh shit of fuck its more than 4 this ui isn't gonna look pretty anymore
|
||||
CDeviceMixes.RemoveAllChildren();
|
||||
for (var i = 1; i < msg.NodeGasMixes.Length; i++)
|
||||
{
|
||||
GenerateGasDisplay(msg.NodeGasMixes[i], CDeviceMixes);
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace Content.Client.Atmos.Visualizers;
|
||||
/// </summary>
|
||||
public sealed class PortableScrubberSystem : VisualizerSystem<PortableScrubberVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, PortableScrubberVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
@@ -20,15 +18,15 @@ public sealed class PortableScrubberSystem : VisualizerSystem<PortableScrubberVi
|
||||
&& AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsRunning, out var isRunning, args.Component))
|
||||
{
|
||||
var runningState = isRunning ? component.RunningState : component.IdleState;
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState);
|
||||
|
||||
var fullState = isFull ? component.FullState : component.ReadyState;
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState);
|
||||
}
|
||||
|
||||
if (AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsDraining, out var isDraining, args.Component))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace Content.Client.Botany;
|
||||
|
||||
public sealed class PlantHolderVisualizerSystem : VisualizerSystem<PlantHolderVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -20,8 +18,8 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem<PlantHolderVi
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
_sprite.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant);
|
||||
_sprite.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false);
|
||||
SpriteSystem.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant);
|
||||
SpriteSystem.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, PlantHolderVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
@@ -34,12 +32,12 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem<PlantHolderVi
|
||||
{
|
||||
var valid = !string.IsNullOrWhiteSpace(state);
|
||||
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), PlantHolderLayers.Plant, valid);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), PlantHolderLayers.Plant, valid);
|
||||
|
||||
if (valid)
|
||||
{
|
||||
_sprite.LayerSetRsi((uid, args.Sprite), PlantHolderLayers.Plant, new ResPath(rsi));
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), PlantHolderLayers.Plant, state);
|
||||
SpriteSystem.LayerSetRsi((uid, args.Sprite), PlantHolderLayers.Plant, new ResPath(rsi));
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PlantHolderLayers.Plant, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,9 @@ namespace Content.Client.Cargo.UI
|
||||
/// </summary>
|
||||
public void PopulateOrders(IEnumerable<CargoOrderData> orders)
|
||||
{
|
||||
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
|
||||
return;
|
||||
|
||||
Requests.DisposeAllChildren();
|
||||
|
||||
foreach (var order in orders)
|
||||
@@ -237,6 +240,7 @@ namespace Content.Client.Cargo.UI
|
||||
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
|
||||
|
||||
// TODO: Disable based on access.
|
||||
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
|
||||
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
|
||||
Requests.AddChild(row);
|
||||
}
|
||||
@@ -290,8 +294,8 @@ namespace Content.Client.Cargo.UI
|
||||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
|
||||
_timing.CurTime < orderConsole.NextAccountActionTime;
|
||||
|
||||
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
|
||||
Orders.Visible = !orderConsole.SlipPrinter;
|
||||
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,15 @@ namespace Content.Client.Cargo.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetApproveVisible(bool visible)
|
||||
{
|
||||
Approve.Visible = visible;
|
||||
|
||||
if (visible)
|
||||
Cancel.AddStyleClass("OpenLeft");
|
||||
else
|
||||
Cancel.RemoveStyleClass("OpenLeft");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,16 +25,14 @@ public sealed class ChargesSystem : SharedChargesSystem
|
||||
|
||||
while (query.MoveNext(out var uid, out var recharge, out var charges))
|
||||
{
|
||||
BaseActionComponent? actionComp = null;
|
||||
|
||||
if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
|
||||
if (_actions.GetAction(uid, false) is not {} action)
|
||||
continue;
|
||||
|
||||
var current = GetCurrentCharges((uid, charges, recharge));
|
||||
|
||||
if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
|
||||
{
|
||||
_actions.UpdateAction(uid, actionComp);
|
||||
_actions.UpdateAction(action);
|
||||
}
|
||||
|
||||
_tempLastCharges[uid] = current;
|
||||
|
||||
@@ -9,7 +9,6 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, TypingIndicatorComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
@@ -34,24 +33,24 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
|
||||
return;
|
||||
}
|
||||
|
||||
var layerExists = _sprite.LayerMapTryGet((uid, args.Sprite), TypingIndicatorLayers.Base, out var layer, false);
|
||||
var layerExists = SpriteSystem.LayerMapTryGet((uid, args.Sprite), TypingIndicatorLayers.Base, out var layer, false);
|
||||
if (!layerExists)
|
||||
layer = _sprite.LayerMapReserve((uid, args.Sprite), TypingIndicatorLayers.Base);
|
||||
layer = SpriteSystem.LayerMapReserve((uid, args.Sprite), TypingIndicatorLayers.Base);
|
||||
|
||||
_sprite.LayerSetRsi((uid, args.Sprite), layer, proto.SpritePath);
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState);
|
||||
SpriteSystem.LayerSetRsi((uid, args.Sprite), layer, proto.SpritePath);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState);
|
||||
args.Sprite.LayerSetShader(layer, proto.Shader);
|
||||
_sprite.LayerSetOffset((uid, args.Sprite), layer, proto.Offset);
|
||||
SpriteSystem.LayerSetOffset((uid, args.Sprite), layer, proto.Offset);
|
||||
|
||||
AppearanceSystem.TryGetData<TypingIndicatorState>(uid, TypingIndicatorVisuals.State, out var state);
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), layer, state != TypingIndicatorState.None);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, state != TypingIndicatorState.None);
|
||||
switch (state)
|
||||
{
|
||||
case TypingIndicatorState.Idle:
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.IdleState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.IdleState);
|
||||
break;
|
||||
case TypingIndicatorState.Typing:
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Content.Client.Chat.UI
|
||||
{
|
||||
public abstract class SpeechBubble : Control
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] protected readonly IConfigurationManager ConfigManager = default!;
|
||||
@@ -30,12 +31,12 @@ namespace Content.Client.Chat.UI
|
||||
/// <summary>
|
||||
/// The total time a speech bubble stays on screen.
|
||||
/// </summary>
|
||||
private const float TotalTime = 4;
|
||||
private static readonly TimeSpan TotalTime = TimeSpan.FromSeconds(4);
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time at the end of the bubble's life at which it starts fading.
|
||||
/// </summary>
|
||||
private const float FadeTime = 0.25f;
|
||||
private static readonly TimeSpan FadeTime = TimeSpan.FromSeconds(0.25f);
|
||||
|
||||
/// <summary>
|
||||
/// The distance in world space to offset the speech bubble from the center of the entity.
|
||||
@@ -50,7 +51,10 @@ namespace Content.Client.Chat.UI
|
||||
|
||||
private readonly EntityUid _senderEntity;
|
||||
|
||||
private float _timeLeft = TotalTime;
|
||||
/// <summary>
|
||||
/// The time at which this bubble will die.
|
||||
/// </summary>
|
||||
private TimeSpan _deathTime;
|
||||
|
||||
public float VerticalOffset { get; set; }
|
||||
private float _verticalOffsetAchieved;
|
||||
@@ -99,6 +103,7 @@ namespace Content.Client.Chat.UI
|
||||
bubble.Measure(Vector2Helpers.Infinity);
|
||||
ContentSize = bubble.DesiredSize;
|
||||
_verticalOffsetAchieved = -ContentSize.Y;
|
||||
_deathTime = _timing.RealTime + TotalTime;
|
||||
}
|
||||
|
||||
protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
|
||||
@@ -107,8 +112,8 @@ namespace Content.Client.Chat.UI
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
_timeLeft -= args.DeltaSeconds;
|
||||
if (_entityManager.Deleted(_senderEntity) || _timeLeft <= 0)
|
||||
var timeLeft = (float)(_deathTime - _timing.RealTime).TotalSeconds;
|
||||
if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0)
|
||||
{
|
||||
// Timer spawn to prevent concurrent modification exception.
|
||||
Timer.Spawn(0, Die);
|
||||
@@ -131,10 +136,10 @@ namespace Content.Client.Chat.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (_timeLeft <= FadeTime)
|
||||
if (timeLeft <= FadeTime.TotalSeconds)
|
||||
{
|
||||
// Update alpha if we're fading.
|
||||
Modulate = Color.White.WithAlpha(_timeLeft / FadeTime);
|
||||
Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -144,7 +149,7 @@ namespace Content.Client.Chat.UI
|
||||
|
||||
var baseOffset = 0f;
|
||||
|
||||
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
|
||||
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
|
||||
baseOffset = speech.SpeechBubbleOffset;
|
||||
|
||||
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
|
||||
@@ -175,9 +180,9 @@ namespace Content.Client.Chat.UI
|
||||
/// </summary>
|
||||
public void FadeNow()
|
||||
{
|
||||
if (_timeLeft > FadeTime)
|
||||
if (_deathTime > _timing.RealTime)
|
||||
{
|
||||
_timeLeft = FadeTime;
|
||||
_deathTime = _timing.RealTime + FadeTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Content.Client.Chemistry.Visualizers;
|
||||
public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -72,7 +71,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
|
||||
return;
|
||||
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||
_sprite.SetVisible((uid, sprite), false);
|
||||
SpriteSystem.SetVisible((uid, sprite), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly ItemSystem _itemSystem = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -50,7 +49,7 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (!_sprite.LayerMapTryGet((uid, args.Sprite), component.Layer, out var fillLayer, false))
|
||||
if (!SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.Layer, out var fillLayer, false))
|
||||
return;
|
||||
|
||||
var maxFillLevels = component.MaxFillLevels;
|
||||
@@ -68,9 +67,9 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
||||
}
|
||||
if (component.Metamorphic)
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((uid, args.Sprite), component.BaseLayer, out var baseLayer, false))
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.BaseLayer, out var baseLayer, false))
|
||||
{
|
||||
var hasOverlay = _sprite.LayerMapTryGet((uid, args.Sprite), component.OverlayLayer, out var overlayLayer, false);
|
||||
var hasOverlay = SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.OverlayLayer, out var overlayLayer, false);
|
||||
|
||||
if (AppearanceSystem.TryGetData<string>(uid, SolutionContainerVisuals.BaseOverride,
|
||||
out var baseOverride,
|
||||
@@ -80,35 +79,35 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
||||
|
||||
if (reagentProto?.MetamorphicSprite is { } sprite)
|
||||
{
|
||||
_sprite.LayerSetSprite((uid, args.Sprite), baseLayer, sprite);
|
||||
SpriteSystem.LayerSetSprite((uid, args.Sprite), baseLayer, sprite);
|
||||
if (reagentProto.MetamorphicMaxFillLevels > 0)
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
|
||||
maxFillLevels = reagentProto.MetamorphicMaxFillLevels;
|
||||
fillBaseName = reagentProto.MetamorphicFillBaseName;
|
||||
changeColor = reagentProto.MetamorphicChangeColor;
|
||||
fillSprite = sprite;
|
||||
}
|
||||
else
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), fillLayer, false);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false);
|
||||
|
||||
if (hasOverlay)
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), overlayLayer, false);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
|
||||
if (hasOverlay)
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), overlayLayer, true);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, true);
|
||||
if (component.MetamorphicDefaultSprite != null)
|
||||
_sprite.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite);
|
||||
SpriteSystem.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
|
||||
}
|
||||
|
||||
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, maxFillLevels + 1);
|
||||
@@ -120,25 +119,25 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
||||
|
||||
var stateName = fillBaseName + closestFillSprite;
|
||||
if (fillSprite != null)
|
||||
_sprite.LayerSetSprite((uid, args.Sprite), fillLayer, fillSprite);
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), fillLayer, stateName);
|
||||
SpriteSystem.LayerSetSprite((uid, args.Sprite), fillLayer, fillSprite);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), fillLayer, stateName);
|
||||
|
||||
if (changeColor && AppearanceSystem.TryGetData<Color>(uid, SolutionContainerVisuals.Color, out var color, args.Component))
|
||||
_sprite.LayerSetColor((uid, args.Sprite), fillLayer, color);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, color);
|
||||
else
|
||||
_sprite.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (component.EmptySpriteName == null)
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), fillLayer, false);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false);
|
||||
else
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName);
|
||||
if (changeColor)
|
||||
_sprite.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor);
|
||||
else
|
||||
_sprite.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,18 +11,6 @@ namespace Content.Client.Clothing.Systems;
|
||||
// All valid items for chameleon are calculated on client startup and stored in dictionary.
|
||||
public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
|
||||
private static readonly SlotFlags[] IgnoredSlots =
|
||||
{
|
||||
SlotFlags.All,
|
||||
SlotFlags.PREVENTEQUIP,
|
||||
SlotFlags.NONE
|
||||
};
|
||||
private static readonly SlotFlags[] Slots = Enum.GetValues<SlotFlags>().Except(IgnoredSlots).ToArray();
|
||||
|
||||
private readonly Dictionary<SlotFlags, List<string>> _data = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -61,49 +49,4 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
|
||||
borderColor.AccentVColor = otherBorderColor.AccentVColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of valid chameleon targets for these slots.
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetValidTargets(SlotFlags slot)
|
||||
{
|
||||
var set = new HashSet<string>();
|
||||
foreach (var availableSlot in _data.Keys)
|
||||
{
|
||||
if (slot.HasFlag(availableSlot))
|
||||
{
|
||||
set.UnionWith(_data[availableSlot]);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private void PrepareAllVariants()
|
||||
{
|
||||
_data.Clear();
|
||||
var prototypes = _proto.EnumeratePrototypes<EntityPrototype>();
|
||||
|
||||
foreach (var proto in prototypes)
|
||||
{
|
||||
// check if this is valid clothing
|
||||
if (!IsValidTarget(proto))
|
||||
continue;
|
||||
if (!proto.TryGetComponent(out ClothingComponent? item, Factory))
|
||||
continue;
|
||||
|
||||
// sort item by their slot flags
|
||||
// one item can be placed in several buckets
|
||||
foreach (var slot in Slots)
|
||||
{
|
||||
if (!item.Slots.HasFlag(slot))
|
||||
continue;
|
||||
|
||||
if (!_data.ContainsKey(slot))
|
||||
{
|
||||
_data.Add(slot, new List<string>());
|
||||
}
|
||||
_data[slot].Add(proto.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
|
||||
var targets = _chameleon.GetValidTargets(st.Slot);
|
||||
if (st.RequiredTag != null)
|
||||
{
|
||||
var newTargets = new List<string>();
|
||||
var newTargets = new List<EntProtoId>();
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))
|
||||
|
||||
@@ -19,8 +19,8 @@ public sealed partial class ChameleonMenu : DefaultWindow
|
||||
private readonly SpriteSystem _sprite;
|
||||
public event Action<string>? OnIdSelected;
|
||||
|
||||
private IEnumerable<string> _possibleIds = Enumerable.Empty<string>();
|
||||
private string? _selectedId;
|
||||
private IEnumerable<EntProtoId> _possibleIds = [];
|
||||
private EntProtoId? _selectedId;
|
||||
private string _searchFilter = "";
|
||||
|
||||
public ChameleonMenu()
|
||||
@@ -32,7 +32,7 @@ public sealed partial class ChameleonMenu : DefaultWindow
|
||||
Search.OnTextChanged += OnSearchEntered;
|
||||
}
|
||||
|
||||
public void UpdateState(IEnumerable<string> possibleIds, string? selectedId)
|
||||
public void UpdateState(IEnumerable<EntProtoId> possibleIds, string? selectedId)
|
||||
{
|
||||
_possibleIds = possibleIds;
|
||||
_selectedId = selectedId;
|
||||
@@ -57,7 +57,7 @@ public sealed partial class ChameleonMenu : DefaultWindow
|
||||
if (!_prototypeManager.TryIndex(id, out EntityPrototype? proto))
|
||||
continue;
|
||||
|
||||
var lowId = id.ToLowerInvariant();
|
||||
var lowId = id.Id.ToLowerInvariant();
|
||||
var lowName = proto.Name.ToLowerInvariant();
|
||||
if (!lowId.Contains(searchFilterLow) && !lowName.Contains(_searchFilter))
|
||||
continue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Placement;
|
||||
@@ -13,6 +13,9 @@ namespace Content.Client.Construction
|
||||
private readonly ConstructionSystem _constructionSystem;
|
||||
private readonly ConstructionPrototype? _prototype;
|
||||
|
||||
public ConstructionSystem? CurrentConstructionSystem { get { return _constructionSystem; } }
|
||||
public ConstructionPrototype? CurrentPrototype { get { return _prototype; } }
|
||||
|
||||
public override bool CanRotate { get; }
|
||||
|
||||
public ConstructionPlacementHijack(ConstructionSystem constructionSystem, ConstructionPrototype? prototype)
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Content.Client.Construction.UI
|
||||
private ConstructionSystem? _constructionSystem;
|
||||
private ConstructionPrototype? _selected;
|
||||
private List<ConstructionPrototype> _favoritedRecipes = [];
|
||||
private Dictionary<string, ContainerButton> _recipeButtons = new();
|
||||
private readonly Dictionary<string, ContainerButton> _recipeButtons = new();
|
||||
private string _selectedCategory = string.Empty;
|
||||
|
||||
private const string FavoriteCatName = "construction-category-favorites";
|
||||
@@ -217,8 +217,8 @@ namespace Content.Client.Construction.UI
|
||||
var itemButton = new ContainerButton()
|
||||
{
|
||||
VerticalAlignment = Control.VAlignment.Center,
|
||||
Name = recipe.TargetPrototype.Name,
|
||||
ToolTip = recipe.TargetPrototype.Name,
|
||||
Name = recipe.Prototype.Name,
|
||||
ToolTip = recipe.Prototype.Name,
|
||||
ToggleMode = true,
|
||||
Children = { protoView },
|
||||
};
|
||||
@@ -235,7 +235,7 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
if (buttonToggledEventArgs.Pressed &&
|
||||
_selected != null &&
|
||||
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
|
||||
_recipeButtons.TryGetValue(_selected.ID, out var oldButton))
|
||||
{
|
||||
oldButton.Pressed = false;
|
||||
SelectGridButton(oldButton, false);
|
||||
@@ -245,7 +245,7 @@ namespace Content.Client.Construction.UI
|
||||
};
|
||||
|
||||
recipesGrid.AddChild(itemButtonPanelContainer);
|
||||
_recipeButtons[recipe.Prototype.Name!] = itemButton;
|
||||
_recipeButtons[recipe.Prototype.ID] = itemButton;
|
||||
var isCurrentButtonSelected = _selected == recipe.Prototype;
|
||||
itemButton.Pressed = isCurrentButtonSelected;
|
||||
SelectGridButton(itemButton, isCurrentButtonSelected);
|
||||
@@ -307,7 +307,7 @@ namespace Content.Client.Construction.UI
|
||||
if (button.Parent is not PanelContainer buttonPanel)
|
||||
return;
|
||||
|
||||
button.Modulate = select ? Color.Green : Color.Transparent;
|
||||
button.Children.Single().Modulate = select ? Color.Green : Color.White;
|
||||
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
|
||||
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
if (!Menus.TryPeek(out var topMenu))
|
||||
{
|
||||
Logger.Error("Context Menu: Mouse entered menu without any open menus?");
|
||||
Log.Error("Context Menu: Mouse entered menu without any open menus?");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
if (!Menus.TryPeek(out var topMenu))
|
||||
{
|
||||
Logger.Error("Context Menu: Attempting to open sub menu without any open menus?");
|
||||
Log.Error("Context Menu: Attempting to open sub menu without any open menus?");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ namespace Content.Client.ContextMenu.UI
|
||||
// find the element associated with this entity
|
||||
if (!Elements.TryGetValue(entity, out var element))
|
||||
{
|
||||
Logger.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent<MetaDataComponent>(entity).EntityName} ({entity})");
|
||||
Log.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent<MetaDataComponent>(entity).EntityName} ({entity})");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace Content.Client.Damage;
|
||||
public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -229,7 +228,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
// the layer key just doesn't exist, we skip it.
|
||||
foreach (var key in damageVisComp.TargetLayers)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet((entity, spriteComponent), key, out var index, false))
|
||||
if (!SpriteSystem.LayerMapTryGet((entity, spriteComponent), key, out var index, false))
|
||||
{
|
||||
Log.Warning($"Layer at key {key} was invalid for entity {entity}.");
|
||||
continue;
|
||||
@@ -254,7 +253,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
foreach (var layer in damageVisComp.TargetLayerMapKeys)
|
||||
{
|
||||
var layerCount = spriteComponent.AllLayers.Count();
|
||||
var index = _sprite.LayerMapGet((entity, spriteComponent), layer);
|
||||
var index = SpriteSystem.LayerMapGet((entity, spriteComponent), layer);
|
||||
// var layerState = spriteComponent.LayerGetState(index).ToString()!;
|
||||
|
||||
if (index + 1 != layerCount)
|
||||
@@ -325,17 +324,17 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
/// </summary>
|
||||
private void AddDamageLayerToSprite(Entity<SpriteComponent?> spriteEnt, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null)
|
||||
{
|
||||
var newLayer = _sprite.AddLayer(
|
||||
var newLayer = SpriteSystem.AddLayer(
|
||||
spriteEnt,
|
||||
new SpriteSpecifier.Rsi(
|
||||
new(sprite.Sprite), state
|
||||
),
|
||||
index
|
||||
);
|
||||
_sprite.LayerMapSet(spriteEnt, mapKey, newLayer);
|
||||
SpriteSystem.LayerMapSet(spriteEnt, mapKey, newLayer);
|
||||
if (sprite.Color != null)
|
||||
_sprite.LayerSetColor(spriteEnt, newLayer, Color.FromHex(sprite.Color));
|
||||
_sprite.LayerSetVisible(spriteEnt, newLayer, false);
|
||||
SpriteSystem.LayerSetColor(spriteEnt, newLayer, Color.FromHex(sprite.Color));
|
||||
SpriteSystem.LayerSetVisible(spriteEnt, newLayer, false);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent damageVisComp, ref AppearanceChangeEvent args)
|
||||
@@ -410,7 +409,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
damageVisComp.DisabledLayers[layer] = disabled;
|
||||
if (damageVisComp.TrackAllDamage)
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, spriteComponent), $"{layer}trackDamage", !disabled);
|
||||
SpriteSystem.LayerSetVisible((uid, spriteComponent), $"{layer}trackDamage", !disabled);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -419,7 +418,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
|
||||
foreach (var damageGroup in damageVisComp.DamageOverlayGroups.Keys)
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, spriteComponent), $"{layer}{damageGroup}", !disabled);
|
||||
SpriteSystem.LayerSetVisible((uid, spriteComponent), $"{layer}{damageGroup}", !disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,20 +461,20 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
|
||||
private void ReorderOverlaySprite(Entity<SpriteComponent> spriteEnt, DamageVisualsComponent damageVisComp, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold)
|
||||
{
|
||||
_sprite.LayerMapTryGet(spriteEnt.AsNullable(), key, out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), key, out var spriteLayer, false);
|
||||
var visibility = spriteEnt.Comp[spriteLayer].Visible;
|
||||
_sprite.RemoveLayer(spriteEnt.AsNullable(), spriteLayer);
|
||||
SpriteSystem.RemoveLayer(spriteEnt.AsNullable(), spriteLayer);
|
||||
if (threshold == FixedPoint2.Zero) // these should automatically be invisible
|
||||
threshold = damageVisComp.Thresholds[1];
|
||||
spriteLayer = _sprite.AddLayer(
|
||||
spriteLayer = SpriteSystem.AddLayer(
|
||||
spriteEnt.AsNullable(),
|
||||
new SpriteSpecifier.Rsi(
|
||||
new(sprite.Sprite),
|
||||
$"{statePrefix}_{threshold}"
|
||||
),
|
||||
spriteLayer);
|
||||
_sprite.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer);
|
||||
_sprite.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility);
|
||||
SpriteSystem.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer);
|
||||
SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility);
|
||||
// this is somewhat iffy since it constantly reallocates
|
||||
damageVisComp.TopMostLayerKey = key;
|
||||
}
|
||||
@@ -610,7 +609,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
if (!damageVisComp.DisabledLayers[layerMapKey])
|
||||
{
|
||||
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
|
||||
_sprite.LayerMapTryGet(spriteEnt.AsNullable(), $"{layerMapKey}trackDamage", out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"{layerMapKey}trackDamage", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(spriteEnt,
|
||||
spriteLayer,
|
||||
@@ -621,7 +620,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
else if (!damageVisComp.Overlay)
|
||||
{
|
||||
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
|
||||
_sprite.LayerMapTryGet(spriteEnt.AsNullable(), $"{layerMapKey}", out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"{layerMapKey}", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(spriteEnt,
|
||||
spriteLayer,
|
||||
@@ -643,7 +642,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup) && !damageVisComp.DisabledLayers[layerMapKey])
|
||||
{
|
||||
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
|
||||
_sprite.LayerMapTryGet((entity, spriteComponent), $"{layerMapKey}{damageGroup}", out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet((entity, spriteComponent), $"{layerMapKey}{damageGroup}", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(
|
||||
(entity, spriteComponent),
|
||||
@@ -655,7 +654,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
else if (!damageVisComp.Overlay)
|
||||
{
|
||||
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
|
||||
_sprite.LayerMapTryGet((entity, spriteComponent), $"{layerMapKey}", out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet((entity, spriteComponent), $"{layerMapKey}", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(
|
||||
(entity, spriteComponent),
|
||||
@@ -670,7 +669,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
/// </summary>
|
||||
private void UpdateOverlay(Entity<SpriteComponent> spriteEnt, FixedPoint2 threshold)
|
||||
{
|
||||
_sprite.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(spriteEnt,
|
||||
spriteLayer,
|
||||
@@ -690,7 +689,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
{
|
||||
if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup))
|
||||
{
|
||||
_sprite.LayerMapTryGet((entity, spriteComponent), $"DamageOverlay{damageGroup}", out var spriteLayer, false);
|
||||
SpriteSystem.LayerMapTryGet((entity, spriteComponent), $"DamageOverlay{damageGroup}", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(
|
||||
(entity, spriteComponent),
|
||||
@@ -711,15 +710,15 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
{
|
||||
if (threshold == 0)
|
||||
{
|
||||
_sprite.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, false);
|
||||
SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!spriteEnt.Comp[spriteLayer].Visible)
|
||||
{
|
||||
_sprite.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, true);
|
||||
SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, true);
|
||||
}
|
||||
_sprite.LayerSetRsiState(spriteEnt.AsNullable(), spriteLayer, $"{statePrefix}_{threshold}");
|
||||
SpriteSystem.LayerSetRsiState(spriteEnt.AsNullable(), spriteLayer, $"{statePrefix}_{threshold}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ namespace Content.Client.DamageState;
|
||||
|
||||
public sealed class DamageStateVisualizerSystem : VisualizerSystem<DamageStateVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, DamageStateVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
var sprite = args.Sprite;
|
||||
@@ -25,18 +23,18 @@ public sealed class DamageStateVisualizerSystem : VisualizerSystem<DamageStateVi
|
||||
// Brain no worky rn so this was just easier.
|
||||
foreach (var key in new[] { DamageStateVisualLayers.Base, DamageStateVisualLayers.BaseUnshaded })
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet((uid, sprite), key, out _, false)) continue;
|
||||
if (!SpriteSystem.LayerMapTryGet((uid, sprite), key, out _, false)) continue;
|
||||
|
||||
_sprite.LayerSetVisible((uid, sprite), key, false);
|
||||
SpriteSystem.LayerSetVisible((uid, sprite), key, false);
|
||||
}
|
||||
|
||||
foreach (var (key, state) in layers)
|
||||
{
|
||||
// Inheritance moment.
|
||||
if (!_sprite.LayerMapTryGet((uid, sprite), key, out _, false)) continue;
|
||||
if (!SpriteSystem.LayerMapTryGet((uid, sprite), key, out _, false)) continue;
|
||||
|
||||
_sprite.LayerSetVisible((uid, sprite), key, true);
|
||||
_sprite.LayerSetRsiState((uid, sprite), key, state);
|
||||
SpriteSystem.LayerSetVisible((uid, sprite), key, true);
|
||||
SpriteSystem.LayerSetRsiState((uid, sprite), key, state);
|
||||
}
|
||||
|
||||
// So they don't draw over mobs anymore
|
||||
@@ -45,12 +43,12 @@ public sealed class DamageStateVisualizerSystem : VisualizerSystem<DamageStateVi
|
||||
if (sprite.DrawDepth > (int)DrawDepth.DeadMobs)
|
||||
{
|
||||
component.OriginalDrawDepth = sprite.DrawDepth;
|
||||
_sprite.SetDrawDepth((uid, sprite), (int)DrawDepth.DeadMobs);
|
||||
SpriteSystem.SetDrawDepth((uid, sprite), (int)DrawDepth.DeadMobs);
|
||||
}
|
||||
}
|
||||
else if (component.OriginalDrawDepth != null)
|
||||
{
|
||||
_sprite.SetDrawDepth((uid, sprite), component.OriginalDrawDepth.Value);
|
||||
SpriteSystem.SetDrawDepth((uid, sprite), component.OriginalDrawDepth.Value);
|
||||
component.OriginalDrawDepth = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.Decals.Overlays;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -21,9 +22,12 @@ public sealed class DecalPlacementSystem : EntitySystem
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public static readonly EntProtoId DecalAction = "BaseMappingDecalAction";
|
||||
|
||||
private string? _decalId;
|
||||
private Color _decalColor = Color.White;
|
||||
private Angle _decalAngle = Angle.Zero;
|
||||
@@ -152,19 +156,12 @@ public sealed class DecalPlacementSystem : EntitySystem
|
||||
Cleanable = _cleanable,
|
||||
};
|
||||
|
||||
var actionId = Spawn(null);
|
||||
AddComp(actionId, new WorldTargetActionComponent
|
||||
{
|
||||
// non-unique actions may be considered duplicates when saving/loading.
|
||||
Icon = decalProto.Sprite,
|
||||
Repeat = true,
|
||||
ClientExclusive = true,
|
||||
CheckCanAccess = false,
|
||||
CheckCanInteract = false,
|
||||
Range = -1,
|
||||
Event = actionEvent,
|
||||
IconColor = _decalColor,
|
||||
});
|
||||
var actionId = Spawn(DecalAction);
|
||||
var action = Comp<ActionComponent>(actionId);
|
||||
var ent = (actionId, action);
|
||||
_actions.SetEvent(actionId, actionEvent);
|
||||
_actions.SetIcon(ent, decalProto.Sprite);
|
||||
_actions.SetIconColor(ent, _decalColor);
|
||||
|
||||
_metaData.SetEntityName(actionId, $"{_decalId} ({_decalColor.ToHex()}, {(int) _decalAngle.Degrees})");
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Drugs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.Drugs;
|
||||
|
||||
@@ -12,6 +13,7 @@ public sealed class DrugOverlaySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private RainbowOverlay _overlay = default!;
|
||||
|
||||
@@ -45,7 +47,10 @@ public sealed class DrugOverlaySystem : EntitySystem
|
||||
private void OnInit(EntityUid uid, SeeingRainbowsComponent component, ComponentInit args)
|
||||
{
|
||||
if (_player.LocalEntity == uid)
|
||||
{
|
||||
_overlay.Phase = _random.NextFloat(MathF.Tau); // random starting phase for movement effect
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, SeeingRainbowsComponent component, ComponentShutdown args)
|
||||
|
||||
@@ -24,16 +24,27 @@ public sealed class RainbowOverlay : Overlay
|
||||
|
||||
public float Intoxication = 0.0f;
|
||||
public float TimeTicker = 0.0f;
|
||||
public float Phase = 0.0f;
|
||||
|
||||
private const float VisualThreshold = 10.0f;
|
||||
private const float PowerDivisor = 250.0f;
|
||||
private float _timeScale = 0.0f;
|
||||
private float _warpScale = 0.0f;
|
||||
|
||||
private float EffectScale => Math.Clamp((Intoxication - VisualThreshold) / PowerDivisor, 0.0f, 1.0f);
|
||||
|
||||
public RainbowOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_rainbowShader = _prototypeManager.Index<ShaderPrototype>("Rainbow").InstanceUnique();
|
||||
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private void OnReducedMotionChanged(bool reducedMotion)
|
||||
{
|
||||
_timeScale = reducedMotion ? 0.0f : 1.0f;
|
||||
_warpScale = reducedMotion ? 0.0f : 1.0f;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
@@ -51,7 +62,7 @@ public sealed class RainbowOverlay : Overlay
|
||||
if (!statusSys.TryGetTime(playerEntity.Value, DrugOverlaySystem.RainbowKey, out var time, status))
|
||||
return;
|
||||
|
||||
var timeLeft = (float) (time.Value.Item2 - time.Value.Item1).TotalSeconds;
|
||||
var timeLeft = (float)(time.Value.Item2 - time.Value.Item1).TotalSeconds;
|
||||
|
||||
TimeTicker += args.DeltaSeconds;
|
||||
|
||||
@@ -61,7 +72,7 @@ public sealed class RainbowOverlay : Overlay
|
||||
}
|
||||
else
|
||||
{
|
||||
Intoxication -= Intoxication/(timeLeft - TimeTicker) * args.DeltaSeconds;
|
||||
Intoxication -= Intoxication / (timeLeft - TimeTicker) * args.DeltaSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,16 +89,15 @@ public sealed class RainbowOverlay : Overlay
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
// TODO disable only the motion part or ike's idea (single static frame of the overlay)
|
||||
if (_config.GetCVar(CCVars.ReducedMotion))
|
||||
return;
|
||||
|
||||
if (ScreenTexture == null)
|
||||
return;
|
||||
|
||||
var handle = args.WorldHandle;
|
||||
_rainbowShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
_rainbowShader.SetParameter("effectScale", EffectScale);
|
||||
_rainbowShader.SetParameter("colorScale", EffectScale);
|
||||
_rainbowShader.SetParameter("timeScale", _timeScale);
|
||||
_rainbowShader.SetParameter("warpScale", _warpScale * EffectScale);
|
||||
_rainbowShader.SetParameter("phase", Phase);
|
||||
handle.UseShader(_rainbowShader);
|
||||
handle.DrawRect(args.WorldBounds, Color.White);
|
||||
handle.UseShader(null);
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Content.Client.Electrocution;
|
||||
public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem<ElectrocutionHUDVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -59,7 +58,7 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem<Electroc
|
||||
if (!AppearanceSystem.TryGetData<bool>(uid, ElectrifiedVisuals.IsElectrified, out var electrified, appearanceComp))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, electrified);
|
||||
SpriteSystem.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, electrified);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem<Electroc
|
||||
var electrifiedQuery = AllEntityQuery<ElectrocutionHUDVisualsComponent, AppearanceComponent, SpriteComponent>();
|
||||
while (electrifiedQuery.MoveNext(out var uid, out _, out _, out var spriteComp))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, false);
|
||||
SpriteSystem.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +83,6 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem<Electroc
|
||||
return;
|
||||
|
||||
var player = _playerMan.LocalEntity;
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), ElectrifiedLayers.HUD, electrified && HasComp<ShowElectrocutionHUDComponent>(player));
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), ElectrifiedLayers.HUD, electrified && HasComp<ShowElectrocutionHUDComponent>(player));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
using Content.Shared.Ensnaring.Components;
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ namespace Content.Client.Entry
|
||||
_prototypeManager.RegisterIgnore("alertLevels");
|
||||
_prototypeManager.RegisterIgnore("nukeopsRole");
|
||||
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
|
||||
_prototypeManager.RegisterIgnore("codewordGenerator");
|
||||
_prototypeManager.RegisterIgnore("codewordFaction");
|
||||
|
||||
_componentFactory.GenerateNetIds();
|
||||
_adminManager.Initialize();
|
||||
|
||||
@@ -28,12 +28,15 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
private readonly TagSystem _tagSystem;
|
||||
private readonly ExamineSystem _examineSystem;
|
||||
private readonly GuidebookSystem _guidebookSystem;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public bool Interactive;
|
||||
|
||||
public Entity<SpriteComponent>? Sprite => View.Entity == null || View.Sprite == null
|
||||
@@ -53,6 +56,7 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
|
||||
_tagSystem = _systemManager.GetEntitySystem<TagSystem>();
|
||||
_examineSystem = _systemManager.GetEntitySystem<ExamineSystem>();
|
||||
_guidebookSystem = _systemManager.GetEntitySystem<GuidebookSystem>();
|
||||
_sawmill = _logManager.GetSawmill("guidebook.entity");
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
@@ -135,7 +139,7 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
if (!args.TryGetValue("Entity", out var proto))
|
||||
{
|
||||
Logger.Error("Entity embed tag is missing entity prototype argument");
|
||||
_sawmill.Error("Entity embed tag is missing entity prototype argument");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly ISawmill _sawmill = default!;
|
||||
|
||||
public IPrototype? RepresentedPrototype { get; private set; }
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
|
||||
IoCManager.InjectDependencies(this);
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
|
||||
_sawmill = _logManager.GetSawmill("guidemicrowaveembed");
|
||||
_sawmill = _logManager.GetSawmill("guidebook.microwave");
|
||||
}
|
||||
|
||||
public GuideMicrowaveEmbed(string recipe) : this()
|
||||
|
||||
@@ -15,12 +15,16 @@ namespace Content.Client.Guidebook.Controls;
|
||||
[UsedImplicitly]
|
||||
public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public GuideMicrowaveGroupEmbed()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("guidebook.microwave_group");
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
@@ -34,7 +38,7 @@ public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTa
|
||||
control = null;
|
||||
if (!args.TryGetValue("Group", out var group))
|
||||
{
|
||||
Logger.Error("Microwave group embed tag is missing group argument");
|
||||
_sawmill.Error("Microwave group embed tag is missing group argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@ namespace Content.Client.Guidebook.Controls;
|
||||
public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly ChemistryGuideDataSystem _chemistryGuideData;
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public IPrototype? RepresentedPrototype { get; private set; }
|
||||
|
||||
@@ -35,6 +37,7 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("guidebook.reagent");
|
||||
_chemistryGuideData = _systemManager.GetEntitySystem<ChemistryGuideDataSystem>();
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
@@ -64,13 +67,13 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
|
||||
control = null;
|
||||
if (!args.TryGetValue("Reagent", out var id))
|
||||
{
|
||||
Logger.Error("Reagent embed tag is missing reagent prototype argument");
|
||||
_sawmill.Error("Reagent embed tag is missing reagent prototype argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_prototype.TryIndex<ReagentPrototype>(id, out var reagent))
|
||||
{
|
||||
Logger.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype");
|
||||
_sawmill.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,16 @@ namespace Content.Client.Guidebook.Controls;
|
||||
[UsedImplicitly, GenerateTypedNameReferences]
|
||||
public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public GuideReagentGroupEmbed()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("guidebook.reagent_group");
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
@@ -42,7 +46,7 @@ public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag
|
||||
control = null;
|
||||
if (!args.TryGetValue("Group", out var group))
|
||||
{
|
||||
Logger.Error("Reagent group embed tag is missing group argument");
|
||||
_sawmill.Error("Reagent group embed tag is missing group argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,16 @@ namespace Content.Client.Guidebook.Controls;
|
||||
[UsedImplicitly, GenerateTypedNameReferences]
|
||||
public sealed partial class GuideTechDisciplineEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public GuideTechDisciplineEmbed()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("guidebook.tech_discipline");
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
@@ -42,7 +46,7 @@ public sealed partial class GuideTechDisciplineEmbed : BoxContainer, IDocumentTa
|
||||
control = null;
|
||||
if (!args.TryGetValue("Discipline", out var group))
|
||||
{
|
||||
Logger.Error("Technology discipline embed tag is missing discipline argument");
|
||||
_sawmill.Error("Technology discipline embed tag is missing discipline argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,12 @@ namespace Content.Client.Guidebook.Controls;
|
||||
public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, ISearchableControl
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly ResearchSystem _research;
|
||||
private readonly SpriteSystem _sprite;
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public GuideTechnologyEmbed()
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, I
|
||||
IoCManager.InjectDependencies(this);
|
||||
_research = _systemManager.GetEntitySystem<ResearchSystem>();
|
||||
_sprite = _systemManager.GetEntitySystem<SpriteSystem>();
|
||||
_sawmill = _logManager.GetSawmill("guidebook.technology");
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
@@ -61,13 +64,13 @@ public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, I
|
||||
control = null;
|
||||
if (!args.TryGetValue("Technology", out var id))
|
||||
{
|
||||
Logger.Error("Technology embed tag is missing technology prototype argument");
|
||||
_sawmill.Error("Technology embed tag is missing technology prototype argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_prototype.TryIndex<TechnologyPrototype>(id, out var technology))
|
||||
{
|
||||
Logger.Error($"Specified technology prototype \"{id}\" is not a valid technology prototype");
|
||||
_sawmill.Error($"Specified technology prototype \"{id}\" is not a valid technology prototype");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = Logger.GetSawmill("Guidebook");
|
||||
_sawmill = Logger.GetSawmill("guidebook");
|
||||
|
||||
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
||||
|
||||
@@ -225,7 +225,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
||||
{
|
||||
// TODO GUIDEBOOK Maybe allow duplicate entries?
|
||||
// E.g., for adding medicine under both chemicals & the chemist job
|
||||
Logger.Error($"Adding duplicate guide entry: {id}");
|
||||
_sawmill.Error($"Adding duplicate guide entry: {id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
5
Content.Client/Implants/ChameleonControllerSystem.cs
Normal file
5
Content.Client/Implants/ChameleonControllerSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Implants;
|
||||
|
||||
namespace Content.Client.Implants;
|
||||
|
||||
public sealed partial class ChameleonControllerSystem : SharedChameleonControllerSystem;
|
||||
@@ -0,0 +1,49 @@
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Implants;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Timing;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Implants.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ChameleonControllerBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private readonly UseDelaySystem _delay;
|
||||
|
||||
[ViewVariables]
|
||||
private ChameleonControllerMenu? _menu;
|
||||
|
||||
public ChameleonControllerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
_delay = EntMan.System<UseDelaySystem>();
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<ChameleonControllerMenu>();
|
||||
_menu.OnJobSelected += OnJobSelected;
|
||||
}
|
||||
|
||||
private void OnJobSelected(ProtoId<ChameleonOutfitPrototype> outfit)
|
||||
{
|
||||
if (!EntMan.TryGetComponent<UseDelayComponent>(Owner, out var useDelayComp))
|
||||
return;
|
||||
|
||||
if (!_delay.TryResetDelay((Owner, useDelayComp), true))
|
||||
return;
|
||||
|
||||
SendMessage(new ChameleonControllerSelectedOutfitMessage(outfit));
|
||||
|
||||
if (!_delay.TryGetDelayInfo((Owner, useDelayComp), out var delay) || _menu == null)
|
||||
return;
|
||||
|
||||
_menu._lockedUntil = DateTime.Now.Add(delay.Length);
|
||||
_menu.UpdateGrid(true);
|
||||
}
|
||||
}
|
||||
12
Content.Client/Implants/UI/ChameleonControllerMenu.xaml
Normal file
12
Content.Client/Implants/UI/ChameleonControllerMenu.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'chameleon-controller-ui-window-name'}"
|
||||
MinSize="250 300"
|
||||
SetSize="850 700">
|
||||
<BoxContainer Orientation="Vertical" Margin="7 0 0 0">
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<GridContainer Name="Grid" Columns="3" Margin="0 5" >
|
||||
</GridContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
157
Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
Normal file
157
Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Roles;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Implants;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Implants.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChameleonControllerMenu : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
private readonly SpriteSystem _sprite;
|
||||
private readonly JobSystem _job;
|
||||
|
||||
// List of all the job protos that you can select!
|
||||
private IEnumerable<ChameleonOutfitPrototype> _outfits;
|
||||
|
||||
// Lock the UI until this time
|
||||
public DateTime? _lockedUntil;
|
||||
|
||||
private static readonly ProtoId<JobIconPrototype> UnknownIcon = "JobIconUnknown";
|
||||
private static readonly LocId UnknownDepartment = "department-Unknown";
|
||||
|
||||
public event Action<ProtoId<ChameleonOutfitPrototype>>? OnJobSelected;
|
||||
|
||||
public ChameleonControllerMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
_job = _entityManager.System<JobSystem>();
|
||||
|
||||
_outfits = _prototypeManager.EnumeratePrototypes<ChameleonOutfitPrototype>();
|
||||
|
||||
UpdateGrid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill the grid with the correct job icons and buttons.
|
||||
/// </summary>
|
||||
/// <param name="disabled">Set to true to disable all the buttons.</param>
|
||||
public void UpdateGrid(bool disabled = false)
|
||||
{
|
||||
Grid.RemoveAllChildren();
|
||||
|
||||
// Dictionary to easily put outfits in departments.
|
||||
// Department name -> UI element holding that department.
|
||||
var departments = new Dictionary<string, BoxContainer>();
|
||||
|
||||
departments.Add(UnknownDepartment, CreateDepartment(UnknownDepartment));
|
||||
|
||||
// Go through every outfit and add them to the correct department.
|
||||
foreach (var outfit in _outfits)
|
||||
{
|
||||
_prototypeManager.TryIndex(outfit.Job, out var jobProto);
|
||||
|
||||
var name = outfit.LoadoutName ?? outfit.Name ?? jobProto?.Name ?? "Prototype has no name or job.";
|
||||
|
||||
var jobIconId = outfit.Icon ?? jobProto?.Icon ?? UnknownIcon;
|
||||
var jobIconProto = _prototypeManager.Index(jobIconId);
|
||||
|
||||
var outfitButton = CreateOutfitButton(disabled, name, jobIconProto, outfit.ID);
|
||||
|
||||
if (outfit.Job != null && _job.TryGetLowestWeightDepartment(outfit.Job, out var departmentPrototype))
|
||||
{
|
||||
if (!departments.ContainsKey(departmentPrototype.Name))
|
||||
departments.Add(departmentPrototype.Name, CreateDepartment(departmentPrototype.Name));
|
||||
|
||||
departments[departmentPrototype.Name].AddChild(outfitButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
departments[UnknownDepartment].AddChild(outfitButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the departments by their weight.
|
||||
var departmentList = departments.ToList();
|
||||
departmentList.Sort((a, b) => a.Value.ChildCount.CompareTo(b.Value.ChildCount));
|
||||
|
||||
// Actually add the departments to the window.
|
||||
foreach (var department in departmentList)
|
||||
{
|
||||
Grid.AddChild(department.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private BoxContainer CreateDepartment(string name)
|
||||
{
|
||||
var departmentContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
};
|
||||
departmentContainer.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString(name),
|
||||
});
|
||||
|
||||
return departmentContainer;
|
||||
}
|
||||
|
||||
private BoxContainer CreateOutfitButton(bool disabled, string name, JobIconPrototype jobIconProto, ProtoId<ChameleonOutfitPrototype> outfitProto)
|
||||
{
|
||||
var outfitButton = new BoxContainer();
|
||||
|
||||
var button = new Button
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
StyleClasses = {StyleBase.ButtonSquare},
|
||||
ToolTip = Loc.GetString(name),
|
||||
Text = Loc.GetString(name),
|
||||
Margin = new Thickness(0, 0, 15, 0),
|
||||
Disabled = disabled,
|
||||
};
|
||||
|
||||
var jobIconTexture = new TextureRect
|
||||
{
|
||||
Texture = _sprite.Frame0(jobIconProto.Icon),
|
||||
TextureScale = new Vector2(2.5f, 2.5f),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered,
|
||||
Margin = new Thickness(0, 0, 5, 0),
|
||||
};
|
||||
|
||||
outfitButton.AddChild(jobIconTexture);
|
||||
outfitButton.AddChild(button);
|
||||
|
||||
button.OnPressed += _ => JobButtonPressed(outfitProto);
|
||||
|
||||
return outfitButton;
|
||||
}
|
||||
|
||||
private void JobButtonPressed(ProtoId<ChameleonOutfitPrototype> outfit)
|
||||
{
|
||||
OnJobSelected?.Invoke(outfit);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (_lockedUntil == null || DateTime.Now < _lockedUntil)
|
||||
return;
|
||||
|
||||
_lockedUntil = null;
|
||||
UpdateGrid();
|
||||
}
|
||||
}
|
||||
54
Content.Client/Instruments/InstrumentSystem.MidiParsing.cs
Normal file
54
Content.Client/Instruments/InstrumentSystem.MidiParsing.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Instruments;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
|
||||
namespace Content.Client.Instruments;
|
||||
|
||||
public sealed partial class InstrumentSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to parse the input data as a midi and set the channel names respectively.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Thank you to http://www.somascape.org/midi/tech/mfile.html for providing an awesome resource for midi files.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// This method has exception tolerance and does not throw, even if the midi file is invalid.
|
||||
/// </remarks>
|
||||
private bool TrySetChannels(EntityUid uid, byte[] data)
|
||||
{
|
||||
if (!MidiParser.MidiParser.TryGetMidiTracks(data, out var tracks, out var error))
|
||||
{
|
||||
Log.Error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
var resolvedTracks = new List<MidiTrack?>();
|
||||
for (var index = 0; index < tracks.Length; index++)
|
||||
{
|
||||
var midiTrack = tracks[index];
|
||||
if (midiTrack is { TrackName: null, ProgramName: null, InstrumentName: null})
|
||||
continue;
|
||||
|
||||
switch (midiTrack)
|
||||
{
|
||||
case { TrackName: not null, ProgramName: not null }:
|
||||
case { TrackName: not null, InstrumentName: not null }:
|
||||
case { TrackName: not null }:
|
||||
case { ProgramName: not null }:
|
||||
resolvedTracks.Add(midiTrack);
|
||||
break;
|
||||
default:
|
||||
resolvedTracks.Add(null); // Used so the channel still displays as MIDI Channel X and doesn't just take the next valid one in the UI
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Debug($"Channel name: {resolvedTracks.Last()}");
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new InstrumentSetChannelsEvent(GetNetEntity(uid), resolvedTracks.Take(RobustMidiEvent.MaxChannels).ToArray()));
|
||||
Log.Debug($"Resolved {resolvedTracks.Count} channels.");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Instruments;
|
||||
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Instruments;
|
||||
|
||||
public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
@@ -23,6 +24,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
public int MaxMidiEventsPerBatch { get; private set; }
|
||||
public int MaxMidiEventsPerSecond { get; private set; }
|
||||
|
||||
public event Action? OnChannelsUpdated;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -38,6 +41,26 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<ActiveInstrumentComponent, AfterAutoHandleStateEvent>(OnActiveInstrumentAfterHandleState);
|
||||
}
|
||||
|
||||
private bool _isUpdateQueued = false;
|
||||
|
||||
private void OnActiveInstrumentAfterHandleState(Entity<ActiveInstrumentComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
// Called in the update loop so that the components update client side for resolving them in TryComps.
|
||||
_isUpdateQueued = true;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (!_isUpdateQueued)
|
||||
return;
|
||||
|
||||
_isUpdateQueued = false;
|
||||
OnChannelsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, SharedInstrumentComponent component, ref ComponentHandleState args)
|
||||
@@ -252,7 +275,13 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Use overload that takes in byte[] instead.")]
|
||||
public bool OpenMidi(EntityUid uid, ReadOnlySpan<byte> data, InstrumentComponent? instrument = null)
|
||||
{
|
||||
return OpenMidi(uid, data.ToArray(), instrument);
|
||||
}
|
||||
|
||||
public bool OpenMidi(EntityUid uid, byte[] data, InstrumentComponent? instrument = null)
|
||||
{
|
||||
if (!Resolve(uid, ref instrument))
|
||||
return false;
|
||||
@@ -263,6 +292,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
return false;
|
||||
|
||||
SetMaster(uid, null);
|
||||
TrySetChannels(uid, data);
|
||||
|
||||
instrument.MidiEventBuffer.Clear();
|
||||
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
|
||||
return true;
|
||||
|
||||
147
Content.Client/Instruments/MidiParser/MidiInstrument.cs
Normal file
147
Content.Client/Instruments/MidiParser/MidiInstrument.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.MidiParser;
|
||||
|
||||
// This file was autogenerated. Based on https://www.ccarh.org/courses/253/handout/gminstruments/
|
||||
public enum MidiInstrument : byte
|
||||
{
|
||||
AcousticGrandPiano = 0,
|
||||
BrightAcousticPiano = 1,
|
||||
ElectricGrandPiano = 2,
|
||||
HonkyTonkPiano = 3,
|
||||
RhodesPiano = 4,
|
||||
ChorusedPiano = 5,
|
||||
Harpsichord = 6,
|
||||
Clavinet = 7,
|
||||
Celesta = 8,
|
||||
Glockenspiel = 9,
|
||||
MusicBox = 10,
|
||||
Vibraphone = 11,
|
||||
Marimba = 12,
|
||||
Xylophone = 13,
|
||||
TubularBells = 14,
|
||||
Dulcimer = 15,
|
||||
HammondOrgan = 16,
|
||||
PercussiveOrgan = 17,
|
||||
RockOrgan = 18,
|
||||
ChurchOrgan = 19,
|
||||
ReedOrgan = 20,
|
||||
Accordion = 21,
|
||||
Harmonica = 22,
|
||||
TangoAccordion = 23,
|
||||
AcousticNylonGuitar = 24,
|
||||
AcousticSteelGuitar = 25,
|
||||
ElectricJazzGuitar = 26,
|
||||
ElectricCleanGuitar = 27,
|
||||
ElectricMutedGuitar = 28,
|
||||
OverdrivenGuitar = 29,
|
||||
DistortionGuitar = 30,
|
||||
GuitarHarmonics = 31,
|
||||
AcousticBass = 32,
|
||||
FingeredElectricBass = 33,
|
||||
PluckedElectricBass = 34,
|
||||
FretlessBass = 35,
|
||||
SlapBass1 = 36,
|
||||
SlapBass2 = 37,
|
||||
SynthBass1 = 38,
|
||||
SynthBass2 = 39,
|
||||
Violin = 40,
|
||||
Viola = 41,
|
||||
Cello = 42,
|
||||
Contrabass = 43,
|
||||
TremoloStrings = 44,
|
||||
PizzicatoStrings = 45,
|
||||
OrchestralHarp = 46,
|
||||
Timpani = 47,
|
||||
StringEnsemble1 = 48,
|
||||
StringEnsemble2 = 49,
|
||||
SynthStrings1 = 50,
|
||||
SynthStrings2 = 51,
|
||||
ChoirAah = 52,
|
||||
VoiceOoh = 53,
|
||||
SynthChoir = 54,
|
||||
OrchestraHit = 55,
|
||||
Trumpet = 56,
|
||||
Trombone = 57,
|
||||
Tuba = 58,
|
||||
MutedTrumpet = 59,
|
||||
FrenchHorn = 60,
|
||||
BrassSection = 61,
|
||||
SynthBrass1 = 62,
|
||||
SynthBrass2 = 63,
|
||||
SopranoSax = 64,
|
||||
AltoSax = 65,
|
||||
TenorSax = 66,
|
||||
BaritoneSax = 67,
|
||||
Oboe = 68,
|
||||
EnglishHorn = 69,
|
||||
Bassoon = 70,
|
||||
Clarinet = 71,
|
||||
Piccolo = 72,
|
||||
Flute = 73,
|
||||
Recorder = 74,
|
||||
PanFlute = 75,
|
||||
BottleBlow = 76,
|
||||
Shakuhachi = 77,
|
||||
Whistle = 78,
|
||||
Ocarina = 79,
|
||||
SquareWaveLead = 80,
|
||||
SawtoothWaveLead = 81,
|
||||
CalliopeLead = 82,
|
||||
ChiffLead = 83,
|
||||
CharangLead = 84,
|
||||
VoiceLead = 85,
|
||||
FithsLead = 86,
|
||||
BassLead = 87,
|
||||
NewAgePad = 88,
|
||||
WarmPad = 89,
|
||||
PolysynthPad = 90,
|
||||
ChoirPad = 91,
|
||||
BowedPad = 92,
|
||||
MetallicPad = 93,
|
||||
HaloPad = 94,
|
||||
SweepPad = 95,
|
||||
RainEffect = 96,
|
||||
SoundtrackEffect = 97,
|
||||
CrystalEffect = 98,
|
||||
AtmosphereEffect = 99,
|
||||
BrightnessEffect = 100,
|
||||
GoblinsEffect = 101,
|
||||
EchoesEffect = 102,
|
||||
SciFiEffect = 103,
|
||||
Sitar = 104,
|
||||
Banjo = 105,
|
||||
Shamisen = 106,
|
||||
Koto = 107,
|
||||
Kalimba = 108,
|
||||
Bagpipe = 109,
|
||||
Fiddle = 110,
|
||||
Shanai = 111,
|
||||
TinkleBell = 112,
|
||||
Agogo = 113,
|
||||
SteelDrums = 114,
|
||||
Woodblock = 115,
|
||||
TaikoDrum = 116,
|
||||
MelodicTom = 117,
|
||||
SynthDrum = 118,
|
||||
ReverseCymbal = 119,
|
||||
GuitarFretNoise = 120,
|
||||
BreathNoise = 121,
|
||||
Seashore = 122,
|
||||
BirdTweet = 123,
|
||||
TelephoneRing = 124,
|
||||
Helicopter = 125,
|
||||
Applause = 126,
|
||||
Gunshot = 127,
|
||||
}
|
||||
|
||||
public static class MidiInstrumentExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns the given enum value into it's string representation to be used in localization.
|
||||
/// </summary>
|
||||
public static string GetStringRep(this MidiInstrument instrument)
|
||||
{
|
||||
return CaseConversion.PascalToKebab(instrument.ToString());
|
||||
}
|
||||
}
|
||||
184
Content.Client/Instruments/MidiParser/MidiParser.cs
Normal file
184
Content.Client/Instruments/MidiParser/MidiParser.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Content.Shared.Instruments;
|
||||
|
||||
namespace Content.Client.Instruments.MidiParser;
|
||||
|
||||
public static class MidiParser
|
||||
{
|
||||
// Thanks again to http://www.somascape.org/midi/tech/mfile.html
|
||||
public static bool TryGetMidiTracks(
|
||||
byte[] data,
|
||||
[NotNullWhen(true)] out MidiTrack[]? tracks,
|
||||
[NotNullWhen(false)] out string? error)
|
||||
{
|
||||
tracks = null;
|
||||
error = null;
|
||||
|
||||
var stream = new MidiStreamWrapper(data);
|
||||
|
||||
if (stream.ReadString(4) != "MThd")
|
||||
{
|
||||
error = "Invalid file header";
|
||||
return false;
|
||||
}
|
||||
|
||||
var headerLength = stream.ReadUInt32();
|
||||
// MIDI specs define that the header is 6 bytes, we only look at the 6 bytes, if its more, we skip ahead.
|
||||
|
||||
stream.Skip(2); // format
|
||||
var trackCount = stream.ReadUInt16();
|
||||
stream.Skip(2); // time div
|
||||
|
||||
// We now skip ahead if we still have any header length left
|
||||
stream.Skip((int)(headerLength - 6));
|
||||
|
||||
var parsedTracks = new List<MidiTrack>();
|
||||
|
||||
for (var i = 0; i < trackCount; i++)
|
||||
{
|
||||
if (stream.ReadString(4) != "MTrk")
|
||||
{
|
||||
tracks = null;
|
||||
error = "Track contains invalid header";
|
||||
return false;
|
||||
}
|
||||
|
||||
var track = new MidiTrack();
|
||||
|
||||
var trackLength = stream.ReadUInt32();
|
||||
var trackEnd = stream.StreamPosition + trackLength;
|
||||
var hasMidiEvent = false;
|
||||
byte? lastStatusByte = null;
|
||||
|
||||
while (stream.StreamPosition < trackEnd)
|
||||
{
|
||||
stream.ReadVariableLengthQuantity();
|
||||
|
||||
/*
|
||||
* If the first (status) byte is less than 128 (hex 80), this implies that running status is in effect,
|
||||
* and that this byte is actually the first data byte (the status carrying over from the previous MIDI event).
|
||||
* This can only be the case if the immediately previous event was also a MIDI event,
|
||||
* i.e. SysEx and Meta events interrupt (clear) running status.
|
||||
* See http://www.somascape.org/midi/tech/mfile.html#events
|
||||
*/
|
||||
|
||||
var firstByte = stream.ReadByte();
|
||||
if (firstByte >= 0x80)
|
||||
{
|
||||
lastStatusByte = firstByte;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Running status: push byte back for reading as data
|
||||
stream.Skip(-1);
|
||||
}
|
||||
|
||||
// The first event in each MTrk chunk must specify status.
|
||||
if (lastStatusByte == null)
|
||||
{
|
||||
tracks = null;
|
||||
error = "Track data not valid, expected status byte, got nothing.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var eventType = (byte)(lastStatusByte & 0xF0);
|
||||
|
||||
switch (lastStatusByte)
|
||||
{
|
||||
// Meta events
|
||||
case 0xFF:
|
||||
{
|
||||
var metaType = stream.ReadByte();
|
||||
var metaLength = stream.ReadVariableLengthQuantity();
|
||||
var metaData = stream.ReadBytes((int)metaLength);
|
||||
if (metaType == 0x00) // SequenceNumber event
|
||||
continue;
|
||||
|
||||
// Meta event types 01 through 0F are reserved for text and all follow the basic FF 01 len text format
|
||||
if (metaType is < 0x01 or > 0x0F)
|
||||
break;
|
||||
|
||||
// 0x03 is TrackName,
|
||||
// 0x04 is InstrumentName
|
||||
|
||||
var text = Encoding.ASCII.GetString(metaData, 0, (int)metaLength);
|
||||
switch (metaType)
|
||||
{
|
||||
case 0x03 when track.TrackName == null:
|
||||
track.TrackName = text;
|
||||
break;
|
||||
case 0x04 when track.InstrumentName == null:
|
||||
track.InstrumentName = text;
|
||||
break;
|
||||
}
|
||||
|
||||
// still here? then we dont care about the event
|
||||
break;
|
||||
}
|
||||
|
||||
// SysEx events
|
||||
case 0xF0:
|
||||
case 0xF7:
|
||||
{
|
||||
var sysexLength = stream.ReadVariableLengthQuantity();
|
||||
stream.Skip((int)sysexLength);
|
||||
// Sysex events and meta-events cancel any running status which was in effect.
|
||||
// Running status does not apply to and may not be used for these messages.
|
||||
lastStatusByte = null;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
switch (eventType)
|
||||
{
|
||||
// Program Change
|
||||
case 0xC0:
|
||||
{
|
||||
var programNumber = stream.ReadByte();
|
||||
if (track.ProgramName == null)
|
||||
{
|
||||
if (programNumber < Enum.GetValues<MidiInstrument>().Length)
|
||||
track.ProgramName = Loc.GetString($"instruments-component-menu-midi-channel-{((MidiInstrument)programNumber).GetStringRep()}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x80: // Note Off
|
||||
case 0x90: // Note On
|
||||
case 0xA0: // Polyphonic Key Pressure
|
||||
case 0xB0: // Control Change
|
||||
case 0xE0: // Pitch Bend
|
||||
{
|
||||
hasMidiEvent = true;
|
||||
stream.Skip(2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xD0: // Channel Pressure
|
||||
{
|
||||
hasMidiEvent = true;
|
||||
stream.Skip(1);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
error = $"Unknown MIDI event type {lastStatusByte:X2}";
|
||||
tracks = null;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasMidiEvent)
|
||||
parsedTracks.Add(track);
|
||||
}
|
||||
|
||||
tracks = parsedTracks.ToArray();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
103
Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs
Normal file
103
Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Content.Client.Instruments.MidiParser;
|
||||
|
||||
public sealed class MidiStreamWrapper
|
||||
{
|
||||
private readonly MemoryStream _stream;
|
||||
private byte[] _buffer;
|
||||
|
||||
public long StreamPosition => _stream.Position;
|
||||
|
||||
public MidiStreamWrapper(byte[] data)
|
||||
{
|
||||
_stream = new MemoryStream(data, writable: false);
|
||||
_buffer = new byte[4];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips X number of bytes in the stream.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of bytes to skip. If 0, no operations on the stream are performed.</param>
|
||||
public void Skip(int count)
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
_stream.Seek(count, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
var b = _stream.ReadByte();
|
||||
if (b == -1)
|
||||
throw new Exception("Unexpected end of stream");
|
||||
|
||||
return (byte)b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads N bytes using the buffer.
|
||||
/// </summary>
|
||||
public byte[] ReadBytes(int count)
|
||||
{
|
||||
if (_buffer.Length < count)
|
||||
{
|
||||
Array.Resize(ref _buffer, count);
|
||||
}
|
||||
|
||||
var read = _stream.Read(_buffer, 0, count);
|
||||
if (read != count)
|
||||
throw new Exception("Unexpected end of stream");
|
||||
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4 byte big-endian uint.
|
||||
/// </summary>
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
var bytes = ReadBytes(4);
|
||||
return (uint)((bytes[0] << 24) |
|
||||
(bytes[1] << 16) |
|
||||
(bytes[2] << 8) |
|
||||
(bytes[3]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 2 byte big-endian ushort.
|
||||
/// </summary>
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
var bytes = ReadBytes(2);
|
||||
return (ushort)((bytes[0] << 8) | bytes[1]);
|
||||
}
|
||||
|
||||
public string ReadString(int count)
|
||||
{
|
||||
var bytes = ReadBytes(count);
|
||||
return Encoding.UTF8.GetString(bytes, 0, count);
|
||||
}
|
||||
|
||||
public uint ReadVariableLengthQuantity()
|
||||
{
|
||||
uint value = 0;
|
||||
|
||||
// variable-length-quantities encode ints using 7 bits per byte
|
||||
// the highest bit (7) is used for a continuation flag. We read until the high bit is 0
|
||||
|
||||
while (true)
|
||||
{
|
||||
var b = ReadByte();
|
||||
value = (value << 7) | (uint)(b & 0x7f); // Shift current value and add 7 bits
|
||||
// value << 7, make room for the next 7 bits
|
||||
// b & 0x7F mask out the high bit to just get the 7 bit payload
|
||||
if ((b & 0x80) == 0)
|
||||
break; // This was the last bit.
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,7 @@
|
||||
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
<CheckButton Name="DisplayTrackNames"
|
||||
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,26 +1,56 @@
|
||||
using Content.Shared.Instruments;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelsMenu : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = null!;
|
||||
|
||||
private readonly InstrumentBoundUserInterface _owner;
|
||||
|
||||
public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_owner = owner;
|
||||
|
||||
ChannelList.OnItemSelected += OnItemSelected;
|
||||
ChannelList.OnItemDeselected += OnItemDeselected;
|
||||
AllButton.OnPressed += OnAllPressed;
|
||||
ClearButton.OnPressed += OnClearPressed;
|
||||
DisplayTrackNames.OnPressed += OnDisplayTrackNamesPressed;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated += UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
DisplayTrackNames.SetClickPressed(!DisplayTrackNames.Pressed);
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void UpdateChannelList()
|
||||
{
|
||||
Populate(); // This is kind of in-efficent because we don't filter for which instrument updated its channels, but idc
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated -= UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
@@ -51,15 +81,71 @@ public sealed partial class ChannelsMenu : DefaultWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void Populate(InstrumentComponent? instrument)
|
||||
/// <summary>
|
||||
/// Walks up the tree of instrument masters to find the truest master of them all.
|
||||
/// </summary>
|
||||
private ActiveInstrumentComponent ResolveActiveInstrument(InstrumentComponent? comp)
|
||||
{
|
||||
comp ??= _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
|
||||
var instrument = new Entity<InstrumentComponent>(_owner.Owner, comp);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (instrument.Comp.Master == null)
|
||||
break;
|
||||
|
||||
instrument = new Entity<InstrumentComponent>((EntityUid)instrument.Comp.Master,
|
||||
_entityManager.GetComponent<InstrumentComponent>((EntityUid)instrument.Comp.Master));
|
||||
}
|
||||
|
||||
return _entityManager.GetComponent<ActiveInstrumentComponent>(instrument.Owner);
|
||||
}
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
ChannelList.Clear();
|
||||
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
var activeInstrument = ResolveActiveInstrument(instrument);
|
||||
|
||||
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i)), null, true, i);
|
||||
var label = _owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i));
|
||||
if (activeInstrument != null
|
||||
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
|
||||
&& resolvedMidiChannel != null)
|
||||
{
|
||||
if (DisplayTrackNames.Pressed)
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ TrackName: not null, InstrumentName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-multi",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName),
|
||||
("other", resolvedMidiChannel.InstrumentName)),
|
||||
{ TrackName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ ProgramName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.ProgramName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var item = ChannelList.AddItem(label, null, true, i);
|
||||
|
||||
item.Selected = !instrument?.FilteredChannels[i] ?? false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Instruments.UI;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Client.Audio.Midi;
|
||||
@@ -101,9 +102,7 @@ namespace Content.Client.Instruments.UI
|
||||
public void OpenChannelsMenu()
|
||||
{
|
||||
_channelsMenu ??= new ChannelsMenu(this);
|
||||
EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument);
|
||||
|
||||
_channelsMenu.Populate(instrument);
|
||||
_channelsMenu.Populate();
|
||||
_channelsMenu.OpenCenteredRight();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using Range = Robust.Client.UserInterface.Controls.Range;
|
||||
|
||||
@@ -145,10 +146,6 @@ namespace Content.Client.Instruments.UI
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
await using var memStream = new MemoryStream((int) file.Length);
|
||||
|
||||
await file.CopyToAsync(memStream);
|
||||
|
||||
if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument))
|
||||
{
|
||||
return;
|
||||
@@ -156,7 +153,7 @@ namespace Content.Client.Instruments.UI
|
||||
|
||||
if (!_entManager.System<InstrumentSystem>()
|
||||
.OpenMidi(Entity,
|
||||
memStream.GetBuffer().AsSpan(0, (int) memStream.Length),
|
||||
file.CopyToArray(),
|
||||
instrument))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -50,6 +50,18 @@ namespace Content.Client.Inventory
|
||||
[ViewVariables]
|
||||
private readonly EntityUid _virtualHiddenEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The current amount of added hand buttons.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private int _handCount;
|
||||
|
||||
/// <summary>
|
||||
/// The current shape of the inventory, needed to calculate the window size.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private Vector2i _inventoryDimensions;
|
||||
|
||||
public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
_examine = EntMan.System<ExamineSystem>();
|
||||
@@ -93,6 +105,8 @@ namespace Content.Client.Inventory
|
||||
return;
|
||||
|
||||
_strippingMenu.ClearButtons();
|
||||
_handCount = 0;
|
||||
_inventoryDimensions = Vector2i.Zero;
|
||||
|
||||
if (EntMan.TryGetComponent<InventoryComponent>(Owner, out var inv))
|
||||
{
|
||||
@@ -152,9 +166,15 @@ namespace Content.Client.Inventory
|
||||
// TODO allow windows to resize based on content's desired size
|
||||
|
||||
// for now: shit-code
|
||||
// this breaks for drones (too many hands, lots of empty vertical space), and looks shit for monkeys and the like.
|
||||
// but the window is realizable, so eh.
|
||||
_strippingMenu.SetSize = new Vector2(220, snare?.IsEnsnared == true ? 550 : 530);
|
||||
// calculate the window size manually
|
||||
// +20 horizontally and vertically from the ContentsContainer margin
|
||||
// +16 vertically from the BoxContainer margin
|
||||
// +27 vertically from the window header
|
||||
var horizontalMenuSize = Math.Max(200, Math.Max(_handCount, _inventoryDimensions.X + 1) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 20);
|
||||
var verticalMenuSize = Math.Max(200, (_inventoryDimensions.Y + (_handCount > 0 ? 2 : 1)) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 53);
|
||||
if (snare?.IsEnsnared == true)
|
||||
verticalMenuSize += 20;
|
||||
_strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
|
||||
}
|
||||
|
||||
private void AddHandButton(Hand hand)
|
||||
@@ -172,6 +192,8 @@ namespace Content.Client.Inventory
|
||||
|
||||
UpdateEntityIcon(button, hand.HeldEntity);
|
||||
_strippingMenu!.HandsContainer.AddChild(button);
|
||||
LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation));
|
||||
_handCount++;
|
||||
}
|
||||
|
||||
private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot)
|
||||
@@ -220,6 +242,10 @@ namespace Content.Client.Inventory
|
||||
UpdateEntityIcon(button, entity);
|
||||
|
||||
LayoutContainer.SetPosition(button, slotDef.StrippingWindowPos * (SlotControl.DefaultButtonSize + ButtonSeparation));
|
||||
if (slotDef.StrippingWindowPos.X > _inventoryDimensions.X)
|
||||
_inventoryDimensions = new Vector2i(slotDef.StrippingWindowPos.X, _inventoryDimensions.Y);
|
||||
if (slotDef.StrippingWindowPos.Y > _inventoryDimensions.Y)
|
||||
_inventoryDimensions = new Vector2i(_inventoryDimensions.X, slotDef.StrippingWindowPos.Y);
|
||||
}
|
||||
|
||||
private void UpdateEntityIcon(SlotControl button, EntityUid? entity)
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Content.Client.IoC
|
||||
var collection = IoCManager.Instance!;
|
||||
|
||||
collection.Register<IParallaxManager, ParallaxManager>();
|
||||
collection.Register<GeneratedParallaxCache>();
|
||||
collection.Register<IChatManager, ChatManager>();
|
||||
collection.Register<ISharedChatManager, ChatManager>();
|
||||
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
|
||||
|
||||
@@ -29,12 +29,14 @@ namespace Content.Client.LateJoin
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
|
||||
[Dependency] private readonly JobRequirementsManager _jobRequirements = default!;
|
||||
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
public event Action<(NetEntity, string)> SelectedId;
|
||||
|
||||
private readonly ClientGameTicker _gameTicker;
|
||||
private readonly SpriteSystem _sprites;
|
||||
private readonly CrewManifestSystem _crewManifest;
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
private readonly Dictionary<NetEntity, Dictionary<string, List<JobButton>>> _jobButtons = new();
|
||||
private readonly Dictionary<NetEntity, Dictionary<string, BoxContainer>> _jobCategories = new();
|
||||
@@ -49,6 +51,7 @@ namespace Content.Client.LateJoin
|
||||
_sprites = _entitySystem.GetEntitySystem<SpriteSystem>();
|
||||
_crewManifest = _entitySystem.GetEntitySystem<CrewManifestSystem>();
|
||||
_gameTicker = _entitySystem.GetEntitySystem<ClientGameTicker>();
|
||||
_sawmill = _logManager.GetSawmill("latejoin.panel");
|
||||
|
||||
Title = Loc.GetString("late-join-gui-title");
|
||||
|
||||
@@ -66,7 +69,7 @@ namespace Content.Client.LateJoin
|
||||
SelectedId += x =>
|
||||
{
|
||||
var (station, jobId) = x;
|
||||
Logger.InfoS("latejoin", $"Late joining as ID: {jobId}");
|
||||
_sawmill.Info($"Late joining as ID: {jobId}");
|
||||
_consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)} {station}");
|
||||
Close();
|
||||
};
|
||||
@@ -82,7 +85,7 @@ namespace Content.Client.LateJoin
|
||||
_jobCategories.Clear();
|
||||
|
||||
if (!_gameTicker.DisallowedLateJoin && _gameTicker.StationNames.Count == 0)
|
||||
Logger.Warning("No stations exist, nothing to display in late-join GUI");
|
||||
_sawmill.Warning("No stations exist, nothing to display in late-join GUI");
|
||||
|
||||
foreach (var (id, name) in _gameTicker.StationNames)
|
||||
{
|
||||
|
||||
@@ -6,8 +6,6 @@ namespace Content.Client.Light.EntitySystems;
|
||||
|
||||
public sealed class EmergencyLightSystem : VisualizerSystem<EmergencyLightComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, EmergencyLightComponent comp, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
@@ -16,13 +14,13 @@ public sealed class EmergencyLightSystem : VisualizerSystem<EmergencyLightCompon
|
||||
if (!AppearanceSystem.TryGetData<bool>(uid, EmergencyLightVisuals.On, out var on, args.Component))
|
||||
on = false;
|
||||
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, !on);
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, on);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, !on);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, on);
|
||||
|
||||
if (AppearanceSystem.TryGetData<Color>(uid, EmergencyLightVisuals.Color, out var color, args.Component))
|
||||
{
|
||||
_sprite.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, color);
|
||||
_sprite.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, color);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, color);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ public sealed class ExpendableLightSystem : VisualizerSystem<ExpendableLightComp
|
||||
[Dependency] private readonly PointLightSystem _pointLightSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly LightBehaviorSystem _lightBehavior = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -54,37 +53,37 @@ public sealed class ExpendableLightSystem : VisualizerSystem<ExpendableLightComp
|
||||
comp.PlayingStream = _audioSystem.PlayPvs(
|
||||
comp.LoopedSound, uid)?.Entity;
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, args.Sprite), ExpendableLightVisualLayers.Overlay, out var layerIdx, true))
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), ExpendableLightVisualLayers.Overlay, out var layerIdx, true))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(comp.IconStateLit))
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layerIdx, comp.IconStateLit);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layerIdx, comp.IconStateLit);
|
||||
if (!string.IsNullOrWhiteSpace(comp.SpriteShaderLit))
|
||||
args.Sprite.LayerSetShader(layerIdx, comp.SpriteShaderLit);
|
||||
else
|
||||
args.Sprite.LayerSetShader(layerIdx, null, null);
|
||||
if (comp.GlowColorLit.HasValue)
|
||||
_sprite.LayerSetColor((uid, args.Sprite), layerIdx, comp.GlowColorLit.Value);
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), layerIdx, true);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), layerIdx, comp.GlowColorLit.Value);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), layerIdx, true);
|
||||
}
|
||||
|
||||
if (comp.GlowColorLit.HasValue)
|
||||
_sprite.LayerSetColor((uid, args.Sprite), ExpendableLightVisualLayers.Glow, comp.GlowColorLit.Value);
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), ExpendableLightVisualLayers.Glow, true);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), ExpendableLightVisualLayers.Glow, comp.GlowColorLit.Value);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), ExpendableLightVisualLayers.Glow, true);
|
||||
|
||||
break;
|
||||
case ExpendableLightState.Dead:
|
||||
comp.PlayingStream = _audioSystem.Stop(comp.PlayingStream);
|
||||
if (_sprite.LayerMapTryGet((uid, args.Sprite), ExpendableLightVisualLayers.Overlay, out layerIdx, true))
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), ExpendableLightVisualLayers.Overlay, out layerIdx, true))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(comp.IconStateSpent))
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layerIdx, comp.IconStateSpent);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layerIdx, comp.IconStateSpent);
|
||||
if (!string.IsNullOrWhiteSpace(comp.SpriteShaderSpent))
|
||||
args.Sprite.LayerSetShader(layerIdx, comp.SpriteShaderSpent);
|
||||
else
|
||||
args.Sprite.LayerSetShader(layerIdx, null, null);
|
||||
}
|
||||
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), ExpendableLightVisualLayers.Glow, false);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), ExpendableLightVisualLayers.Glow, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace Content.Client.Light.Visualizers;
|
||||
|
||||
public sealed class LightBulbSystem : VisualizerSystem<LightBulbComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
@@ -18,13 +16,13 @@ public sealed class LightBulbSystem : VisualizerSystem<LightBulbComponent>
|
||||
switch (state)
|
||||
{
|
||||
case LightBulbState.Normal:
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
|
||||
break;
|
||||
case LightBulbState.Broken:
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
|
||||
break;
|
||||
case LightBulbState.Burned:
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +30,7 @@ public sealed class LightBulbSystem : VisualizerSystem<LightBulbComponent>
|
||||
// also update sprites color
|
||||
if (AppearanceSystem.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
|
||||
{
|
||||
_sprite.SetColor((uid, args.Sprite), color);
|
||||
SpriteSystem.SetColor((uid, args.Sprite), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<bool>(uid, ToggleableLightVisuals.Enabled, out var enabled, args.Component))
|
||||
if (!_appearance.TryGetData<bool>(uid, ToggleableVisuals.Enabled, out var enabled, args.Component))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -28,16 +27,16 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
|
||||
return;
|
||||
|
||||
if (comp.SpriteStateMap.TryGetValue(state, out var spriteState))
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), PoweredLightLayers.Base, spriteState);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PoweredLightLayers.Base, spriteState);
|
||||
|
||||
if (_sprite.LayerExists((uid, args.Sprite), PoweredLightLayers.Glow))
|
||||
if (SpriteSystem.LayerExists((uid, args.Sprite), PoweredLightLayers.Glow))
|
||||
{
|
||||
if (TryComp<PointLightComponent>(uid, out var light))
|
||||
{
|
||||
_sprite.LayerSetColor((uid, args.Sprite), PoweredLightLayers.Glow, light.Color);
|
||||
SpriteSystem.LayerSetColor((uid, args.Sprite), PoweredLightLayers.Glow, light.Color);
|
||||
}
|
||||
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), PoweredLightLayers.Glow, state == PoweredLightState.On);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), PoweredLightLayers.Glow, state == PoweredLightState.On);
|
||||
}
|
||||
|
||||
SetBlinkingAnimation(
|
||||
|
||||
@@ -32,7 +32,6 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IFileDialogManager _dialogManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
@@ -269,7 +268,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
_configurationManager,
|
||||
EntityManager,
|
||||
_dialogManager,
|
||||
_logManager,
|
||||
LogManager,
|
||||
_playerManager,
|
||||
_prototypeManager,
|
||||
_resourceCache,
|
||||
|
||||
@@ -20,6 +20,12 @@ public sealed partial class LoadoutContainer : BoxContainer
|
||||
|
||||
public Button Select => SelectButton;
|
||||
|
||||
public string? Text
|
||||
{
|
||||
get => SelectButton.Text;
|
||||
set => SelectButton.Text = value;
|
||||
}
|
||||
|
||||
public LoadoutContainer(ProtoId<LoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -54,22 +60,9 @@ public sealed partial class LoadoutContainer : BoxContainer
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_entManager.DeleteEntity(_entity);
|
||||
}
|
||||
|
||||
public bool Pressed
|
||||
{
|
||||
get => SelectButton.Pressed;
|
||||
set => SelectButton.Pressed = value;
|
||||
}
|
||||
|
||||
public string? Text
|
||||
{
|
||||
get => SelectButton.Text;
|
||||
set => SelectButton.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
|
||||
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
|
||||
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="5">
|
||||
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"/>
|
||||
</PanelContainer>
|
||||
<!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
|
||||
<Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
|
||||
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
|
||||
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="5">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'loadout-restrictions'}"/>
|
||||
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
@@ -7,12 +6,21 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Lobby.UI.Loadouts;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class LoadoutGroupContainer : BoxContainer
|
||||
{
|
||||
private const string ClosedGroupMark = "▶";
|
||||
private const string OpenedGroupMark = "▼";
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary that stores open groups
|
||||
/// </summary>
|
||||
private Dictionary<string, bool> _openedGroups = new();
|
||||
|
||||
private readonly LoadoutGroupPrototype _groupProto;
|
||||
|
||||
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
|
||||
@@ -21,6 +29,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
|
||||
public LoadoutGroupContainer(HumanoidCharacterProfile profile, RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_groupProto = groupProto;
|
||||
|
||||
RefreshLoadouts(profile, loadout, session, collection);
|
||||
@@ -63,32 +72,165 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
|
||||
}
|
||||
|
||||
LoadoutsContainer.DisposeAllChildren();
|
||||
// Didn't use options because this is more robust in future.
|
||||
|
||||
var selected = loadout.SelectedLoadouts[_groupProto.ID];
|
||||
// Get all loadout prototypes for this group.
|
||||
var validProtos = _groupProto.Loadouts.Select(id => protoMan.Index(id));
|
||||
|
||||
foreach (var loadoutProto in _groupProto.Loadouts)
|
||||
/*
|
||||
* Group the prototypes based on their GroupBy field.
|
||||
* - If GroupBy is null or empty, fallback to grouping by the prototype ID itself.
|
||||
* - The result is a dictionary where:
|
||||
* - The key is either GroupBy or ID (if GroupBy is not set).
|
||||
* - The value is the list of prototypes that belong to that group.
|
||||
*
|
||||
* This allows grouping loadouts into sub-categories within the group.
|
||||
*/
|
||||
var groups = validProtos
|
||||
.GroupBy(p => string.IsNullOrEmpty(p.GroupBy)
|
||||
? p.ID
|
||||
: p.GroupBy)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
foreach (var kvp in groups)
|
||||
{
|
||||
if (!protoMan.TryIndex(loadoutProto, out var loadProto))
|
||||
continue;
|
||||
var protos = kvp.Value;
|
||||
|
||||
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
|
||||
var pressed = matchingLoadout != null;
|
||||
|
||||
var enabled = loadout.IsValid(profile, session, loadoutProto, collection, out var reason);
|
||||
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
|
||||
loadoutContainer.Select.Pressed = pressed;
|
||||
loadoutContainer.Text = loadoutSystem.GetName(loadProto);
|
||||
|
||||
loadoutContainer.Select.OnPressed += args =>
|
||||
if (protos.Count > 1)
|
||||
{
|
||||
if (args.Button.Pressed)
|
||||
OnLoadoutPressed?.Invoke(loadoutProto);
|
||||
else
|
||||
OnLoadoutUnpressed?.Invoke(loadoutProto);
|
||||
};
|
||||
/*
|
||||
* Build the list of UI elements for each loadout prototype:
|
||||
* - For each prototype, create its corresponding LoadoutContainer UI element.
|
||||
* - Set HorizontalExpand to true so elements properly stretch in layout.
|
||||
* - Collect all UI elements into a list for further processing.
|
||||
*/
|
||||
var uiElements = protos
|
||||
.Select(proto =>
|
||||
{
|
||||
var elem = CreateLoadoutUI(proto, profile, loadout, session, collection, loadoutSystem);
|
||||
elem.HorizontalExpand = true;
|
||||
return elem;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
LoadoutsContainer.AddChild(loadoutContainer);
|
||||
/*
|
||||
* Determine which element should be displayed first:
|
||||
* - If any element is currently selected (its button is pressed), use it.
|
||||
* - Otherwise, fallback to the first element in the list.
|
||||
*
|
||||
* This moves the selected item outside of the sublist for better usability,
|
||||
* making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
|
||||
* without having to search inside expanded subgroups.
|
||||
*/
|
||||
var firstElement = uiElements.FirstOrDefault(e => e.Select.Pressed) ?? uiElements[0];
|
||||
|
||||
/*
|
||||
* Get all remaining elements except the first one:
|
||||
* - Use ReferenceEquals to ensure we exclude the exact instance used as firstElement.
|
||||
*/
|
||||
var otherElements = uiElements.Where(e => !ReferenceEquals(e, firstElement)).ToList();
|
||||
|
||||
firstElement.HorizontalExpand = true;
|
||||
var subContainer = new SubLoadoutContainer()
|
||||
{
|
||||
Visible = _openedGroups.GetValueOrDefault(kvp.Key, false)
|
||||
};
|
||||
var toggle = CreateToggleButton(kvp, firstElement, subContainer);
|
||||
|
||||
LoadoutsContainer.AddChild(firstElement);
|
||||
LoadoutsContainer.AddChild(subContainer);
|
||||
|
||||
var subList = subContainer.Grid;
|
||||
foreach (var proto in otherElements)
|
||||
{
|
||||
subList.AddChild(proto);
|
||||
}
|
||||
|
||||
UpdateToggleColor(toggle, subList);
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadoutsContainer.AddChild(
|
||||
CreateLoadoutUI(protos[0], profile, loadout, session, collection, loadoutSystem)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ToggleLoadoutButton CreateToggleButton(KeyValuePair<string, List<LoadoutPrototype>> kvp, LoadoutContainer firstElement, SubLoadoutContainer subContainer)
|
||||
{
|
||||
var toggle = new ToggleLoadoutButton
|
||||
{
|
||||
Text = ClosedGroupMark
|
||||
};
|
||||
|
||||
toggle.Text = subContainer.Visible ? OpenedGroupMark : ClosedGroupMark;
|
||||
|
||||
toggle.OnPressed += _ =>
|
||||
{
|
||||
var willOpen = !subContainer.Visible;
|
||||
subContainer.Visible = willOpen;
|
||||
toggle.Text = willOpen ? OpenedGroupMark : ClosedGroupMark;
|
||||
_openedGroups[kvp.Key] = willOpen;
|
||||
};
|
||||
|
||||
firstElement.AddChild(toggle);
|
||||
toggle.SetPositionFirst();
|
||||
return toggle;
|
||||
}
|
||||
|
||||
private void UpdateToggleColor(Button toggle, BoxContainer subList)
|
||||
{
|
||||
var anyActive = subList.Children
|
||||
.OfType<LoadoutContainer>()
|
||||
.Any(c => c.Select.Pressed);
|
||||
|
||||
toggle.Modulate = anyActive
|
||||
? Color.Green
|
||||
: Color.White;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a UI container for a single Loadout item.
|
||||
///
|
||||
/// This method was extracted from RefreshLoadouts because the logic for creating
|
||||
/// individual loadout items is used multiple times inside that method, and duplicating
|
||||
/// the code made it harder to maintain.
|
||||
///
|
||||
/// Logic:
|
||||
/// - Checks if the item is currently selected in the loadout.
|
||||
/// - Checks if the item is valid for selection (IsValid).
|
||||
/// - Creates a LoadoutContainer with the appropriate status (disabled / active).
|
||||
/// - Subscribes to button press events to handle selection and deselection.
|
||||
/// </summary>
|
||||
/// <param name="proto">The loadout item prototype.</param>
|
||||
/// <param name="profile">The humanoid character profile.</param>
|
||||
/// <param name="loadout">The current role loadout for the user.</param>
|
||||
/// <param name="session">The user's session.</param>
|
||||
/// <param name="collection">The dependency injection container.</param>
|
||||
/// <param name="loadoutSystem">The loadout system instance.</param>
|
||||
/// <returns>A fully initialized LoadoutContainer for UI display.</returns>
|
||||
private LoadoutContainer CreateLoadoutUI(LoadoutPrototype proto, HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, LoadoutSystem loadoutSystem)
|
||||
{
|
||||
var selected = loadout.SelectedLoadouts[_groupProto.ID];
|
||||
|
||||
var pressed = selected.Any(e => e.Prototype == proto.ID);
|
||||
|
||||
var enabled = loadout.IsValid(profile, session, proto.ID, collection, out var reason);
|
||||
|
||||
var cont = new LoadoutContainer(proto, !enabled, reason);
|
||||
|
||||
cont.Text = loadoutSystem.GetName(proto);
|
||||
|
||||
cont.Select.Pressed = pressed;
|
||||
|
||||
cont.Select.OnPressed += args =>
|
||||
{
|
||||
if (args.Button.Pressed)
|
||||
OnLoadoutPressed?.Invoke(proto.ID);
|
||||
else
|
||||
OnLoadoutUnpressed?.Invoke(proto.ID);
|
||||
};
|
||||
|
||||
return cont;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<PanelContainer Name="SubContainer"
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<BoxContainer Name="SubGridContainer"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="true"/>
|
||||
|
||||
</PanelContainer>
|
||||
21
Content.Client/Lobby/UI/Loadouts/SubLoadoutContainer.xaml.cs
Normal file
21
Content.Client/Lobby/UI/Loadouts/SubLoadoutContainer.xaml.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Lobby.UI.Loadouts;
|
||||
|
||||
/// <summary>
|
||||
/// A simple container used to group additional loadout UI elements
|
||||
/// that are part of the same GroupBy subgroup.
|
||||
///
|
||||
/// - Used when a loadout group contains multiple prototypes.
|
||||
/// - The first prototype is shown directly; the remaining ones are placed inside this container.
|
||||
/// - Allows toggling visibility of the subgroup via expandable UI (collapsible behavior).
|
||||
///
|
||||
/// Internally inherits from PanelContainer to allow for border/background styling if needed.
|
||||
/// Exposes its internal BoxContainer (SubGridContainer) via the <see cref="Grid"/> property for adding children.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SubLoadoutContainer : PanelContainer
|
||||
{
|
||||
public BoxContainer Grid => SubGridContainer;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Button Name="ToggleButton"
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
VerticalExpand="False"
|
||||
HorizontalExpand="False"
|
||||
SetSize="64 64"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 5 0"/>
|
||||
10
Content.Client/Lobby/UI/Loadouts/ToggleLoadoutButton.xaml.cs
Normal file
10
Content.Client/Lobby/UI/Loadouts/ToggleLoadoutButton.xaml.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Lobby.UI.Loadouts;
|
||||
|
||||
/// <summary>
|
||||
/// A button that toggles the loadout groups. Needs for override default styles.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ToggleLoadoutButton : Button;
|
||||
@@ -6,8 +6,6 @@ namespace Content.Client.Lock.Visualizers;
|
||||
|
||||
public sealed class LockVisualizerSystem : VisualizerSystem<LockVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, LockVisualsComponent comp, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null
|
||||
@@ -22,14 +20,14 @@ public sealed class LockVisualizerSystem : VisualizerSystem<LockVisualsComponent
|
||||
|
||||
if (AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, !open);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, !open);
|
||||
}
|
||||
else if (!(bool)unlockedStateExist!)
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, locked);
|
||||
SpriteSystem.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, locked);
|
||||
|
||||
if (!open && (bool)unlockedStateExist!)
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), LockVisualLayers.Lock, locked ? comp.StateLocked : comp.StateUnlocked);
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LockVisualLayers.Lock, locked ? comp.StateLocked : comp.StateUnlocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Content.Client.Machines.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component attached to all multipart machine ghosts
|
||||
/// Intended for client side usage only, but used on prototypes.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MultipartMachineGhostComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Machine this particular ghost is linked to.
|
||||
/// </summary>
|
||||
public EntityUid? LinkedMachine = null;
|
||||
}
|
||||
109
Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
Normal file
109
Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Machines.Components;
|
||||
using Content.Shared.Machines.Components;
|
||||
using Content.Shared.Machines.EntitySystems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Spawners;
|
||||
|
||||
namespace Content.Client.Machines.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Client side handling of multipart machines.
|
||||
/// Handles client side examination events to show the expected layout of the machine
|
||||
/// based on the origin of the main entity.
|
||||
/// </summary>
|
||||
public sealed class MultipartMachineSystem : SharedMultipartMachineSystem
|
||||
{
|
||||
private readonly EntProtoId _ghostPrototype = "MultipartMachineGhost";
|
||||
private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180);
|
||||
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization= default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MultipartMachineComponent, ClientExaminedEvent>(OnMachineExamined);
|
||||
SubscribeLocalEvent<MultipartMachineComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
SubscribeLocalEvent<MultipartMachineGhostComponent, TimedDespawnEvent>(OnGhostDespawned);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles spawning several ghost sprites to show where the different parts of the machine
|
||||
/// should go and the rotations they're expected to have.
|
||||
/// Can only show one set of ghost parts at a time and their location depends on the current map/grid
|
||||
/// location of the origin machine.
|
||||
/// </summary>
|
||||
/// <param name="ent">Entity/Component that has been inspected.</param>
|
||||
/// <param name="args">Args for the event.</param>
|
||||
private void OnMachineExamined(Entity<MultipartMachineComponent> ent, ref ClientExaminedEvent args)
|
||||
{
|
||||
if (ent.Comp.Ghosts.Count != 0)
|
||||
{
|
||||
// Already showing some part ghosts
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var part in ent.Comp.Parts.Values)
|
||||
{
|
||||
if (part.Entity.HasValue)
|
||||
continue;
|
||||
|
||||
var entityCoords = new EntityCoordinates(ent.Owner, part.Offset);
|
||||
var ghostEnt = Spawn(_ghostPrototype, entityCoords);
|
||||
|
||||
if (!XformQuery.TryGetComponent(ghostEnt, out var xform))
|
||||
break;
|
||||
|
||||
xform.LocalRotation = part.Rotation;
|
||||
|
||||
Comp<MultipartMachineGhostComponent>(ghostEnt).LinkedMachine = ent;
|
||||
|
||||
ent.Comp.Ghosts.Add(ghostEnt);
|
||||
|
||||
if (part.GhostProto == null)
|
||||
continue;
|
||||
|
||||
var entProto = _prototype.Index(part.GhostProto.Value);
|
||||
if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite)
|
||||
return;
|
||||
|
||||
var ghostSprite = EnsureComp<SpriteComponent>(ghostEnt);
|
||||
_serialization.CopyTo(protoSprite, ref ghostSprite, notNullableOverride: true);
|
||||
|
||||
_sprite.SetColor((ghostEnt, ghostSprite), _partiallyTransparent);
|
||||
|
||||
_metaData.SetEntityName(ghostEnt, entProto.Name);
|
||||
_metaData.SetEntityDescription(ghostEnt, entProto.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<MultipartMachineComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
foreach (var part in ent.Comp.Parts.Values)
|
||||
{
|
||||
part.Entity = part.NetEntity.HasValue ? EnsureEntity<MultipartMachinePartComponent>(part.NetEntity.Value, ent) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles when a ghost part despawns after its short lifetime.
|
||||
/// Will attempt to remove itself from the list of known ghost entities in the main multipart
|
||||
/// machine component.
|
||||
/// </summary>
|
||||
/// <param name="ent">Ghost entity that has been despawned.</param>
|
||||
/// <param name="args">Args for the event.</param>
|
||||
private void OnGhostDespawned(Entity<MultipartMachineGhostComponent> ent, ref TimedDespawnEvent args)
|
||||
{
|
||||
if (!TryComp<MultipartMachineComponent>(ent.Comp.LinkedMachine, out var machine))
|
||||
return;
|
||||
|
||||
machine.Ghosts.Remove(ent);
|
||||
}
|
||||
}
|
||||
@@ -454,7 +454,7 @@ public sealed class MappingState : GameplayStateBase
|
||||
switch (prototype)
|
||||
{
|
||||
case EntityPrototype entity:
|
||||
textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).Select(t => t.Default));
|
||||
textures.AddRange(_sprite.GetPrototypeTextures(entity).Select(t => t.Default));
|
||||
break;
|
||||
case DecalPrototype decal:
|
||||
textures.Add(_sprite.Frame0(decal.Sprite));
|
||||
|
||||
@@ -4,8 +4,8 @@ using Content.Shared.Mapping;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Utility.SpriteSpecifier;
|
||||
|
||||
namespace Content.Client.Mapping;
|
||||
|
||||
@@ -14,16 +14,10 @@ public sealed partial class MappingSystem : EntitySystem
|
||||
[Dependency] private readonly IPlacementManager _placementMan = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileMan = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The icon to use for space tiles.
|
||||
/// </summary>
|
||||
private readonly SpriteSpecifier _spaceIcon = new Texture(new ("Tiles/cropped_parallax.png"));
|
||||
|
||||
/// <summary>
|
||||
/// The icon to use for entity-eraser.
|
||||
/// </summary>
|
||||
private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
|
||||
public static readonly EntProtoId SpawnAction = "BaseMappingSpawnAction";
|
||||
public static readonly EntProtoId EraserAction = "ActionMappingEraser";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -38,90 +32,46 @@ public sealed partial class MappingSystem : EntitySystem
|
||||
/// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd
|
||||
/// prefer if it were to function more like DecalPlacementSystem.
|
||||
/// </summary>
|
||||
private void OnFillActionSlot(FillActionSlotEvent ev)
|
||||
private void OnFillActionSlot(FillActionSlotEvent args)
|
||||
{
|
||||
if (!_placementMan.IsActive)
|
||||
return;
|
||||
|
||||
if (ev.Action != null)
|
||||
if (args.Action != null)
|
||||
return;
|
||||
|
||||
var actionEvent = new StartPlacementActionEvent();
|
||||
ITileDefinition? tileDef = null;
|
||||
|
||||
if (_placementMan.CurrentPermission != null)
|
||||
if (_placementMan.CurrentPermission is {} permission)
|
||||
{
|
||||
actionEvent.EntityType = _placementMan.CurrentPermission.EntityType;
|
||||
actionEvent.PlacementOption = _placementMan.CurrentPermission.PlacementOption;
|
||||
var ev = new StartPlacementActionEvent()
|
||||
{
|
||||
EntityType = permission.EntityType,
|
||||
PlacementOption = permission.PlacementOption,
|
||||
};
|
||||
|
||||
var action = Spawn(SpawnAction);
|
||||
if (_placementMan.CurrentPermission.IsTile)
|
||||
{
|
||||
tileDef = _tileMan[_placementMan.CurrentPermission.TileType];
|
||||
actionEvent.TileId = tileDef.ID;
|
||||
if (_tileMan[_placementMan.CurrentPermission.TileType] is not ContentTileDefinition tileDef)
|
||||
return;
|
||||
|
||||
if (!tileDef.MapAtmosphere && tileDef.Sprite is {} sprite)
|
||||
_actions.SetIcon(action, new SpriteSpecifier.Texture(sprite));
|
||||
ev.TileId = tileDef.ID;
|
||||
_metaData.SetEntityName(action, Loc.GetString(tileDef.Name));
|
||||
}
|
||||
else if (permission.EntityType is {} id)
|
||||
{
|
||||
_actions.SetIcon(action, new SpriteSpecifier.EntityPrototype(id));
|
||||
_metaData.SetEntityName(action, id);
|
||||
}
|
||||
|
||||
_actions.SetEvent(action, ev);
|
||||
args.Action = action;
|
||||
}
|
||||
else if (_placementMan.Eraser)
|
||||
{
|
||||
actionEvent.Eraser = true;
|
||||
args.Action = Spawn(EraserAction);
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
InstantActionComponent action;
|
||||
string name;
|
||||
|
||||
if (tileDef != null)
|
||||
{
|
||||
if (tileDef is not ContentTileDefinition contentTileDef)
|
||||
return;
|
||||
|
||||
var tileIcon = contentTileDef.MapAtmosphere
|
||||
? _spaceIcon
|
||||
: new Texture(contentTileDef.Sprite!.Value);
|
||||
|
||||
action = new InstantActionComponent
|
||||
{
|
||||
ClientExclusive = true,
|
||||
CheckCanInteract = false,
|
||||
Event = actionEvent,
|
||||
Icon = tileIcon
|
||||
};
|
||||
|
||||
name = Loc.GetString(tileDef.Name);
|
||||
}
|
||||
else if (actionEvent.Eraser)
|
||||
{
|
||||
action = new InstantActionComponent
|
||||
{
|
||||
ClientExclusive = true,
|
||||
CheckCanInteract = false,
|
||||
Event = actionEvent,
|
||||
Icon = _deleteIcon,
|
||||
};
|
||||
|
||||
name = Loc.GetString("action-name-mapping-erase");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actionEvent.EntityType))
|
||||
return;
|
||||
|
||||
action = new InstantActionComponent
|
||||
{
|
||||
ClientExclusive = true,
|
||||
CheckCanInteract = false,
|
||||
Event = actionEvent,
|
||||
Icon = new EntityPrototype(actionEvent.EntityType),
|
||||
};
|
||||
|
||||
name = actionEvent.EntityType;
|
||||
}
|
||||
|
||||
var actionId = Spawn(null);
|
||||
AddComp<Component>(actionId, action);
|
||||
_metaData.SetEntityName(actionId, name);
|
||||
|
||||
ev.Action = actionId;
|
||||
}
|
||||
|
||||
private void OnStartPlacementAction(StartPlacementActionEvent args)
|
||||
|
||||
@@ -16,7 +16,6 @@ public sealed class JetpackSystem : SharedJetpackSystem
|
||||
[Dependency] private readonly ClothingSystem _clothing = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -34,10 +33,6 @@ public sealed class JetpackSystem : SharedJetpackSystem
|
||||
{
|
||||
Appearance.TryGetData<bool>(uid, JetpackVisuals.Enabled, out var enabled, args.Component);
|
||||
|
||||
var state = "icon" + (enabled ? "-on" : "");
|
||||
if (args.Sprite != null)
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), 0, state);
|
||||
|
||||
if (TryComp<ClothingComponent>(uid, out var clothing))
|
||||
_clothing.SetEquippedPrefix(uid, enabled ? "on" : null, clothing);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ namespace Content.Client.NodeContainer
|
||||
private readonly IInputManager _inputManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SharedTransformSystem _transformSystem;
|
||||
private readonly SharedMapSystem _mapSystem;
|
||||
|
||||
private readonly Dictionary<(int, int), NodeRenderData> _nodeIndex = new();
|
||||
private readonly Dictionary<EntityUid, Dictionary<Vector2i, List<(GroupData, NodeDatum)>>> _gridIndex = new ();
|
||||
private readonly Dictionary<EntityUid, Dictionary<Vector2i, List<(GroupData, NodeDatum)>>> _gridIndex = new();
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
private readonly Font _font;
|
||||
@@ -48,6 +49,7 @@ namespace Content.Client.NodeContainer
|
||||
_inputManager = inputManager;
|
||||
_entityManager = entityManager;
|
||||
_transformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
_mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
|
||||
_font = cache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
|
||||
}
|
||||
@@ -84,7 +86,7 @@ namespace Content.Client.NodeContainer
|
||||
var xform = _entityManager.GetComponent<TransformComponent>(_entityManager.GetEntity(node.Entity));
|
||||
if (!_entityManager.TryGetComponent<MapGridComponent>(xform.GridUid, out var grid))
|
||||
return;
|
||||
var gridTile = grid.TileIndicesFor(xform.Coordinates);
|
||||
var gridTile = _mapSystem.TileIndicesFor((xform.GridUid.Value, grid), xform.Coordinates);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"entity: {node.Entity}\n");
|
||||
@@ -133,7 +135,7 @@ namespace Content.Client.NodeContainer
|
||||
if (float.IsNaN(coords.Position.X) || float.IsNaN(coords.Position.Y))
|
||||
continue;
|
||||
|
||||
var tile = gridDict.GetOrNew(grid.Comp.TileIndicesFor(coords));
|
||||
var tile = gridDict.GetOrNew(_mapSystem.TileIndicesFor(grid, coords));
|
||||
|
||||
foreach (var (group, nodeDatum) in nodeData)
|
||||
{
|
||||
@@ -153,7 +155,7 @@ namespace Content.Client.NodeContainer
|
||||
var lCursorBox = invMatrix.TransformBox(cursorBox);
|
||||
foreach (var (pos, list) in gridDict)
|
||||
{
|
||||
var centerPos = (Vector2) pos + grid.TileSizeHalfVector;
|
||||
var centerPos = (Vector2)pos + grid.TileSizeHalfVector;
|
||||
list.Sort(NodeDisplayComparer.Instance);
|
||||
|
||||
var offset = -(list.Count - 1) * nodeOffset / 2;
|
||||
|
||||
7
Content.Client/Options/UI/OptionColorSlider.xaml
Normal file
7
Content.Client/Options/UI/OptionColorSlider.xaml
Normal file
@@ -0,0 +1,7 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="TitleLabel" Access="Public" />
|
||||
<Label Name="ExampleLabel" Access="Public" />
|
||||
<ColorSelectorSliders Name="Slider" Access="Public" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
31
Content.Client/Options/UI/OptionColorSlider.xaml.cs
Normal file
31
Content.Client/Options/UI/OptionColorSlider.xaml.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Content.Client.Options.UI;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Options.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Standard UI control used for color sliders in the options menu. Intended for use with <see cref="OptionsTabControlRow"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="OptionsTabControlRow.AddOptionColorSlider"/>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class OptionColorSlider : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// The text describing what this slider affects.
|
||||
/// </summary>
|
||||
public string? Title
|
||||
{
|
||||
get => TitleLabel.Text;
|
||||
set => TitleLabel.Text = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The example text showing the current color of the slider.
|
||||
/// </summary>
|
||||
public string? Example
|
||||
{
|
||||
get => ExampleLabel.Text;
|
||||
set => ExampleLabel.Text = value;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,19 @@ public sealed partial class OptionsTabControlRow : Control
|
||||
return AddOption(new OptionSliderFloatCVar(this, _cfg, cVar, slider, min, max, scale, FormatPercent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a color slider option, backed by a simple string CVar.
|
||||
/// </summary>
|
||||
/// <param name="cVar">The CVar represented by the slider.</param>
|
||||
/// <param name="slider">The UI control for the option.</param>
|
||||
/// <returns>The option instance backing the added option.</returns>
|
||||
public OptionColorSliderCVar AddOptionColorSlider(
|
||||
CVarDef<string> cVar,
|
||||
OptionColorSlider slider)
|
||||
{
|
||||
return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a slider option, backed by a simple integer CVar.
|
||||
/// </summary>
|
||||
@@ -518,6 +531,58 @@ public sealed class OptionSliderFloatCVar : BaseOptionCVar<float>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a CVar option that simply corresponds with a string <see cref="OptionColorSlider"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="OptionsTabControlRow"/>
|
||||
public sealed class OptionColorSliderCVar : BaseOptionCVar<string>
|
||||
{
|
||||
private readonly OptionColorSlider _slider;
|
||||
|
||||
protected override string Value
|
||||
{
|
||||
get => _slider.Slider.Color.ToHex();
|
||||
set
|
||||
{
|
||||
_slider.Slider.Color = Color.FromHex(value);
|
||||
UpdateLabelColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of this type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
|
||||
/// such as <see cref="OptionsTabControlRow.AddOptionPercentSlider"/> instead of instantiating this type directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="controller">The control row that owns this option.</param>
|
||||
/// <param name="cfg">The configuration manager to get and set values from.</param>
|
||||
/// <param name="cVar">The CVar that is being controlled by this option.</param>
|
||||
/// <param name="slider">The UI control for the option.</param>
|
||||
public OptionColorSliderCVar(
|
||||
OptionsTabControlRow controller,
|
||||
IConfigurationManager cfg,
|
||||
CVarDef<string> cVar,
|
||||
OptionColorSlider slider) : base(controller, cfg, cVar)
|
||||
{
|
||||
_slider = slider;
|
||||
|
||||
slider.Slider.OnColorChanged += _ =>
|
||||
{
|
||||
ValueChanged();
|
||||
UpdateLabelColor();
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateLabelColor()
|
||||
{
|
||||
_slider.ExampleLabel.FontColorOverride = Color.FromHex(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a CVar option that simply corresponds with an integer <see cref="OptionSlider"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
|
||||
<CheckBox Name="AutoFillHighlightsCheckBox" Text="{Loc 'ui-options-auto-fill-highlights'}" />
|
||||
<ui:OptionColorSlider Name="HighlightsColorSlider"
|
||||
Title="{Loc 'ui-options-highlights-color'}"
|
||||
Example="{Loc 'ui-options-highlights-color-example'}"/>
|
||||
<Label Text="{Loc 'ui-options-accessability-header-content'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="CensorNudityCheckBox" Text="{Loc 'ui-options-censor-nudity'}" />
|
||||
|
||||
@@ -20,6 +20,8 @@ public sealed partial class AccessibilityTab : Control
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
|
||||
Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
|
||||
Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);
|
||||
|
||||
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -7,9 +6,7 @@ namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class BlackAndWhiteOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
@@ -22,17 +19,6 @@ public sealed partial class BlackAndWhiteOverlay : Overlay
|
||||
ZIndex = 10; // draw this over the DamageOverlay, RainbowOverlay etc.
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
|
||||
return false;
|
||||
|
||||
if (args.Viewport.Eye != eyeComp.Eye)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (ScreenTexture == null)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
|
||||
33
Content.Client/Overlays/NoirOverlay.cs
Normal file
33
Content.Client/Overlays/NoirOverlay.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class NoirOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
private readonly ShaderInstance _noirShader;
|
||||
|
||||
public NoirOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_noirShader = _prototypeManager.Index<ShaderPrototype>("Noir").InstanceUnique();
|
||||
ZIndex = 9; // draw this over the DamageOverlay, RainbowOverlay etc, but before the black and white shader
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (ScreenTexture == null)
|
||||
return;
|
||||
|
||||
var handle = args.WorldHandle;
|
||||
_noirShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
handle.UseShader(_noirShader);
|
||||
handle.DrawRect(args.WorldBounds, Color.White);
|
||||
handle.UseShader(null);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user