using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Doors.Components; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.SprayPainter.Components; using Content.Shared.SprayPainter.Prototypes; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using System.Linq; namespace Content.Shared.SprayPainter; /// /// System for painting airlocks using a spray painter. /// Pipes are handled serverside since AtmosPipeColorSystem is server only. /// public abstract class SharedSprayPainterSystem : EntitySystem { [Dependency] protected readonly IPrototypeManager Proto = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; public List Styles { get; private set; } = new(); public List Groups { get; private set; } = new(); [ValidatePrototypeId] private const string Departments = "Departments"; public override void Initialize() { base.Initialize(); CacheStyles(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnDoorDoAfter); Subs.BuiEvents(SprayPainterUiKey.Key, subs => { subs.Event(OnSpritePicked); subs.Event(OnColorPicked); }); SubscribeLocalEvent(OnAirlockInteract); SubscribeLocalEvent(OnPrototypesReloaded); } private void OnMapInit(Entity ent, ref MapInitEvent args) { if (ent.Comp.ColorPalette.Count == 0) return; SetColor(ent, ent.Comp.ColorPalette.First().Key); } private void OnDoorDoAfter(Entity ent, ref SprayPainterDoorDoAfterEvent args) { if (args.Handled || args.Cancelled) return; if (args.Args.Target is not {} target) return; if (!TryComp(target, out var airlock)) return; airlock.Department = args.Department; Dirty(target, airlock); Audio.PlayPredicted(ent.Comp.SpraySound, ent, args.Args.User); Appearance.SetData(target, DoorVisuals.BaseRSI, args.Sprite); _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Args.User):user} painted {ToPrettyString(args.Args.Target.Value):target}"); args.Handled = true; } #region UI messages private void OnColorPicked(Entity ent, ref SprayPainterColorPickedMessage args) { SetColor(ent, args.Key); } private void OnSpritePicked(Entity ent, ref SprayPainterSpritePickedMessage args) { if (args.Index >= Styles.Count) return; ent.Comp.Index = args.Index; Dirty(ent, ent.Comp); } private void SetColor(Entity ent, string? paletteKey) { if (paletteKey == null || paletteKey == ent.Comp.PickedColor) return; if (!ent.Comp.ColorPalette.ContainsKey(paletteKey)) return; ent.Comp.PickedColor = paletteKey; Dirty(ent, ent.Comp); } #endregion private void OnAirlockInteract(Entity ent, ref InteractUsingEvent args) { if (args.Handled) return; if (!TryComp(args.Used, out var painter)) return; var group = Proto.Index(ent.Comp.Group); var style = Styles[painter.Index]; if (!group.StylePaths.TryGetValue(style.Name, out var sprite)) { string msg = Loc.GetString("spray-painter-style-not-available"); _popup.PopupClient(msg, args.User, args.User); return; } var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.AirlockSprayTime, new SprayPainterDoorDoAfterEvent(sprite, style.Department), args.Used, target: ent, used: args.Used) { BreakOnMove = true, BreakOnDamage = true, NeedHand = true, }; if (!DoAfter.TryStartDoAfter(doAfterEventArgs, out var id)) return; args.Handled = true; // Log the attempt _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is painting {ToPrettyString(ent):target} to '{style.Name}' at {Transform(ent).Coordinates:targetlocation}"); } #region Style caching private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) { if (!args.WasModified() && !args.WasModified()) return; Styles.Clear(); Groups.Clear(); CacheStyles(); // style index might be invalid now so check them all var max = Styles.Count - 1; var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp)) { if (comp.Index > max) { comp.Index = max; Dirty(uid, comp); } } } protected virtual void CacheStyles() { // collect every style's name var names = new SortedSet(); foreach (var group in Proto.EnumeratePrototypes()) { Groups.Add(group); foreach (var style in group.StylePaths.Keys) { names.Add(style); } } // get their department ids too for the final style list var departments = Proto.Index(Departments); Styles.Capacity = names.Count; foreach (var name in names) { departments.Departments.TryGetValue(name, out var department); Styles.Add(new AirlockStyle(name, department)); } } #endregion } public record struct AirlockStyle(string Name, string? Department);