using System.Linq; using Content.Shared.Access.Components; using Content.Shared.Clothing.Components; using Content.Shared.Contraband; using Content.Shared.Emp; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Lock; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Clothing.EntitySystems; public abstract class SharedChameleonClothingSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly ClothingSystem _clothingSystem = default!; [Dependency] private readonly ContrabandSystem _contraband = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedItemSystem _itemSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TagSystem _tag = default!; [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly LockSystem _lock = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; [Dependency] private readonly INetManager _net = default!; private static readonly SlotFlags[] IgnoredSlots = { SlotFlags.All, SlotFlags.PREVENTEQUIP, SlotFlags.NONE }; private static readonly SlotFlags[] Slots = Enum.GetValues().Except(IgnoredSlots).ToArray(); private readonly Dictionary> _data = new(); public readonly Dictionary> ValidVariants = new(); private static readonly ProtoId WhitelistChameleonTag = "WhitelistChameleon"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); SubscribeLocalEvent>(OnVerb); SubscribeLocalEvent(OnEmpPulse); SubscribeLocalEvent(OnPrototypeReload); PrepareAllVariants(); } private void OnPrototypeReload(EntityUid uid, ChameleonClothingComponent component, PrototypesReloadedEventArgs args) { PrepareAllVariants(); } private void OnGotEquipped(EntityUid uid, ChameleonClothingComponent component, GotEquippedEvent args) { component.User = args.Equipee; } private void OnGotUnequipped(EntityUid uid, ChameleonClothingComponent component, GotUnequippedEvent args) { component.User = null; } // Updates chameleon visuals and meta information. // This function is called on a server after user selected new outfit. // And after that on a client after state was updated. // This 100% makes sure that server and client have exactly same data. protected void UpdateVisuals(EntityUid uid, ChameleonClothingComponent component) { if (string.IsNullOrEmpty(component.Default) || !_proto.Resolve(component.Default, out EntityPrototype? proto)) return; // world sprite icon UpdateSprite(uid, proto); // copy name and description, unless its an ID card if (!HasComp(uid)) { var meta = MetaData(uid); _metaData.SetEntityName(uid, proto.Name, meta); _metaData.SetEntityDescription(uid, proto.Description, meta); } // item sprite logic if (TryComp(uid, out ItemComponent? item) && proto.TryGetComponent(out ItemComponent? otherItem, Factory)) { _itemSystem.CopyVisuals(uid, otherItem, item); } // clothing sprite logic if (TryComp(uid, out ClothingComponent? clothing) && proto.TryGetComponent(out ClothingComponent? otherClothing, Factory)) { _clothingSystem.CopyVisuals(uid, otherClothing, clothing); } // appearance data logic if (TryComp(uid, out AppearanceComponent? appearance) && proto.TryGetComponent(out AppearanceComponent? appearanceOther, Factory)) { _appearance.AppendData(appearanceOther, uid); Dirty(uid, appearance); } // properly mark contraband if (proto.TryGetComponent(out ContrabandComponent? contra, Factory)) { EnsureComp(uid, out var current); _contraband.CopyDetails(uid, contra, current); } else { RemComp(uid); } } private void OnVerb(Entity ent, ref GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || _lock.IsLocked(ent.Owner)) return; // Can't pass args from a ref event inside of lambdas var user = args.User; args.Verbs.Add(new InteractionVerb() { Text = Loc.GetString("chameleon-component-verb-text"), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), Act = () => UI.TryToggleUi(ent.Owner, ChameleonUiKey.Key, user) }); } private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args) { if (!component.AffectedByEmp) return; if (component.EmpContinuous) component.NextEmpChange = Timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity); if (_net.IsServer) // needs RandomPredicted { var pick = GetRandomValidPrototype(component.Slot, component.RequireTag); SetSelectedPrototype(uid, pick, component: component); } args.Affected = true; args.Disabled = true; } protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { } /// /// Check if this entity prototype is valid target for chameleon item. /// public bool IsValidTarget(EntityPrototype proto, SlotFlags chameleonSlot = SlotFlags.NONE, string? requiredTag = null) { // check if entity is valid if (proto.Abstract || proto.HideSpawnMenu) return false; // check if it is marked as valid chameleon target if (!proto.TryGetComponent(out TagComponent? tag, Factory) || !_tag.HasTag(tag, WhitelistChameleonTag)) return false; if (requiredTag != null && !_tag.HasTag(tag, requiredTag)) return false; // check if it's valid clothing if (!proto.TryGetComponent(out ClothingComponent? clothing, Factory)) return false; if (!clothing.Slots.HasFlag(chameleonSlot)) return false; return true; } /// /// Get a list of valid chameleon targets for these slots. /// public IEnumerable GetValidTargets(SlotFlags slot, string? tag = null) { var validTargets = new List(); if (tag != null) { foreach (var proto in _data[slot]) { if (IsValidTarget(_proto.Index(proto), slot, tag)) validTargets.Add(proto); } } else { validTargets = _data[slot]; } return validTargets; } /// /// Get a random prototype for a given slot. /// public string GetRandomValidPrototype(SlotFlags slot, string? tag = null) { return _random.Pick(GetValidTargets(slot, tag).ToList()); } protected void PrepareAllVariants() { _data.Clear(); var prototypes = _proto.EnumeratePrototypes(); 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()); } _data[slot].Add(proto.ID); } } } // TODO: Predict and use component states for the UI public virtual void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false, ChameleonClothingComponent? component = null) { } }