Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;

namespace Content.Client.Atmos.EntitySystems;

/// <summary>
/// Used to change the appearance of gas canisters.
/// </summary>
public sealed class GasCanisterAppearanceSystem : VisualizerSystem<GasCanisterComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;

protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args)
{
if (!AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old)
return;

if (!_prototypeManager.HasIndex(protoName))
return;

// Create the given prototype and get its first layer.
var tempUid = Spawn(protoName);
SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0));
QueueDel(tempUid);
}
}
23 changes: 12 additions & 11 deletions Content.Client/Doors/DoorSystem.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Prototypes;

namespace Content.Client.Doors;

public sealed class DoorSystem : SharedDoorSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;

public override void Initialize()
Expand Down Expand Up @@ -85,8 +86,8 @@ private void OnAppearanceChange(Entity<DoorComponent> entity, ref AppearanceChan
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;

if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi);
if (AppearanceSystem.TryGetData<string>(entity, PaintableVisuals.Prototype, out var prototype, args.Component))
UpdateSpriteLayers((entity.Owner, args.Sprite), prototype);

if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
Expand Down Expand Up @@ -139,14 +140,14 @@ private void UpdateAppearanceForDoorState(Entity<DoorComponent> entity, SpriteCo
}
}

private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string baseRsi)
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string targetProto)
{
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
{
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
if (!_prototypeManager.TryIndex(targetProto, out var target))
return;

if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
return;
}

_sprite.SetBaseRsi(sprite.AsNullable(), res.RSI);
_sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
}
}
139 changes: 106 additions & 33 deletions Content.Client/SprayPainter/SprayPainterSystem.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,129 @@
using System.Linq;
using Content.Client.Items;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Content.Shared.SprayPainter.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
using Robust.Shared.Graphics;

namespace Content.Client.SprayPainter;

/// <summary>
/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state.
/// </summary>
public sealed class SprayPainterSystem : SharedSprayPainterSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;

public List<SprayPainterDecalEntry> Decals = [];
public Dictionary<string, List<string>> PaintableGroupsByCategory = new();
public Dictionary<string, Dictionary<string, EntProtoId>> PaintableStylesByGroup = new();

public override void Initialize()
{
base.Initialize();

public List<SprayPainterEntry> Entries { get; private set; } = new();
Subs.ItemStatus<SprayPainterComponent>(ent => new StatusControl(ent));
SubscribeLocalEvent<SprayPainterComponent, AfterAutoHandleStateEvent>(OnStateUpdate);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);

protected override void CacheStyles()
CachePrototypes();
}

private void OnStateUpdate(Entity<SprayPainterComponent> ent, ref AfterAutoHandleStateEvent args)
{
base.CacheStyles();
UpdateUi(ent);
}

Entries.Clear();
foreach (var style in Styles)
protected override void UpdateUi(Entity<SprayPainterComponent> ent)
{
if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui))
bui.Update();
}

private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<PaintableGroupCategoryPrototype>() || !args.WasModified<PaintableGroupPrototype>() || !args.WasModified<DecalPrototype>())
return;

CachePrototypes();
}

private void CachePrototypes()
{
PaintableGroupsByCategory.Clear();
PaintableStylesByGroup.Clear();
foreach (var category in Proto.EnumeratePrototypes<PaintableGroupCategoryPrototype>().OrderBy(x => x.ID))
{
var name = style.Name;
string? iconPath = Groups
.FindAll(x => x.StylePaths.ContainsKey(name))?
.MaxBy(x => x.IconPriority)?.StylePaths[name];
if (iconPath == null)
var groupList = new List<string>();
foreach (var groupId in category.Groups)
{
Entries.Add(new SprayPainterEntry(name, null));
continue;
if (!Proto.TryIndex(groupId, out var group))
continue;

groupList.Add(groupId);
PaintableStylesByGroup[groupId] = group.Styles;
}

RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
if (!doorRsi.RSI.TryGetState("closed", out var icon))
{
Entries.Add(new SprayPainterEntry(name, null));
if (groupList.Count > 0)
PaintableGroupsByCategory[category.ID] = groupList;
}

Decals.Clear();
foreach (var decalPrototype in Proto.EnumeratePrototypes<DecalPrototype>().OrderBy(x => x.ID))
{
if (!decalPrototype.Tags.Contains("station")
&& !decalPrototype.Tags.Contains("markings")
|| decalPrototype.Tags.Contains("dirty"))
continue;
}

Entries.Add(new SprayPainterEntry(name, icon.Frame0));
Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite));
}
}
}

public sealed class SprayPainterEntry
{
public string Name;
public Texture? Icon;

public SprayPainterEntry(string name, Texture? icon)
private sealed class StatusControl : Control
{
Name = name;
Icon = icon;
private readonly RichTextLabel _label;
private readonly Entity<SprayPainterComponent> _entity;
private DecalPaintMode? _lastPaintingDecals = null;

public StatusControl(Entity<SprayPainterComponent> ent)
{
_entity = ent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
}

protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);

if (_entity.Comp.DecalMode == _lastPaintingDecals)
return;

_lastPaintingDecals = _entity.Comp.DecalMode;

string modeLocString = _entity.Comp.DecalMode switch
{
DecalPaintMode.Add => "spray-painter-item-status-add",
DecalPaintMode.Remove => "spray-painter-item-status-remove",
_ => "spray-painter-item-status-off"
};

_label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label",
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString))));
}
}
}

/// <summary>
/// A spray paintable decal, mapped by ID.
/// </summary>
public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite);
84 changes: 69 additions & 15 deletions Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,96 @@
using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;

namespace Content.Client.SprayPainter.UI;

public sealed class SprayPainterBoundUserInterface : BoundUserInterface
/// <summary>
/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category.
/// </summary>
public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private SprayPainterWindow? _window;

public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
protected override void Open()
{
base.Open();

if (_window == null)
{
_window = this.CreateWindow<SprayPainterWindow>();

_window.OnSpritePicked += OnSpritePicked;
_window.OnSetPipeColor += OnSetPipeColor;
_window.OnTabChanged += OnTabChanged;
_window.OnDecalChanged += OnDecalChanged;
_window.OnDecalColorChanged += OnDecalColorChanged;
_window.OnDecalAngleChanged += OnDecalAngleChanged;
_window.OnDecalSnapChanged += OnDecalSnapChanged;
}

var sprayPainter = EntMan.System<SprayPainterSystem>();
_window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals);
Update();

if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp))
_window.SetSelectedTab(sprayPainterComp.SelectedTab);
}

protected override void Open()
public override void Update()
{
base.Open();
if (_window == null)
return;

_window = this.CreateWindow<SprayPainterWindow>();
if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter))
return;

_window.OnSpritePicked = OnSpritePicked;
_window.OnColorPicked = OnColorPicked;
_window.PopulateColors(sprayPainter.ColorPalette);
if (sprayPainter.PickedColor != null)
_window.SelectColor(sprayPainter.PickedColor);
_window.SetSelectedStyles(sprayPainter.StylesByGroup);
_window.SetSelectedDecal(sprayPainter.SelectedDecal);
_window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
_window.SetDecalColor(sprayPainter.SelectedDecalColor);
_window.SetDecalSnap(sprayPainter.SnapDecals);
}

if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp))
{
_window.Populate(EntMan.System<SprayPainterSystem>().Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
}
private void OnDecalSnapChanged(bool snap)
{
SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap));
}

private void OnDecalAngleChanged(int angle)
{
SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
}

private void OnDecalColorChanged(Color? color)
{
SendPredictedMessage(new SprayPainterSetDecalColorMessage(color));
}

private void OnDecalChanged(ProtoId<DecalPrototype> protoId)
{
SendPredictedMessage(new SprayPainterSetDecalMessage(protoId));
}

private void OnTabChanged(int index, bool isSelectedTabWithDecals)
{
SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals));
}

private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
private void OnSpritePicked(string group, string style)
{
SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
}

private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
{
var key = _window?.IndexToColorKey(args.ItemIndex);
SendMessage(new SprayPainterColorPickedMessage(key));
SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
}
}
26 changes: 26 additions & 0 deletions Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<controls:SprayPainterDecals
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'spray-painter-selected-decals'}" />
<ScrollContainer VerticalExpand="True">
<GridContainer Columns="7" Name="DecalsGrid">
<!-- populated by code -->
</GridContainer>
</ScrollContainer>

<BoxContainer Orientation="Vertical">
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
<CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
<CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
</BoxContainer>

<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'spray-painter-angle-rotation'}" />
<SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
<Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
<Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
<Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
</BoxContainer>
</BoxContainer>
</controls:SprayPainterDecals>
Loading
Loading