diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs index 3340755343890b..f3b7837f21a59c 100644 --- a/Content.Client/Chat/UI/EmotesMenu.xaml.cs +++ b/Content.Client/Chat/UI/EmotesMenu.xaml.cs @@ -19,9 +19,6 @@ public sealed partial class EmotesMenu : RadialMenu [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ISharedPlayerManager _playerManager = default!; - private readonly SpriteSystem _spriteSystem; - private readonly EntityWhitelistSystem _whitelistSystem; - public event Action>? OnPlayEmote; public EmotesMenu() @@ -29,8 +26,8 @@ public EmotesMenu() IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - _spriteSystem = _entManager.System(); - _whitelistSystem = _entManager.System(); + var spriteSystem = _entManager.System(); + var whitelistSystem = _entManager.System(); var main = FindControl("Main"); @@ -40,8 +37,8 @@ public EmotesMenu() var player = _playerManager.LocalSession?.AttachedEntity; if (emote.Category == EmoteCategory.Invalid || emote.ChatTriggers.Count == 0 || - !(player.HasValue && _whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) || - _whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value)) + !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) || + whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value)) continue; if (!emote.Available && @@ -63,7 +60,7 @@ public EmotesMenu() { VerticalAlignment = VAlignment.Center, HorizontalAlignment = HAlignment.Center, - Texture = _spriteSystem.Frame0(emote.Icon), + Texture = spriteSystem.Frame0(emote.Icon), TextureScale = new Vector2(2f, 2f), }; diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml index afa783c7aa9c03..87d11005be8a98 100644 --- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml +++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml @@ -1,10 +1,24 @@ + MinSize="800 128"> + + + VerticalExpand="True" + HorizontalExpand="True"> + diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs index d029eb1223d8df..aab2a56ff6819e 100644 --- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs +++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Client.UserInterface.Controls; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; @@ -5,6 +6,7 @@ using Robust.Client.UserInterface.XAML; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; namespace Content.Client.Lobby.UI.Loadouts; @@ -24,27 +26,36 @@ public LoadoutWindow(HumanoidCharacterProfile profile, RoleLoadout loadout, Role Profile = profile; var protoManager = collection.Resolve(); - foreach (var group in proto.Groups) + // Hide if no groups + if (proto.Groups.Count == 0) { - if (!protoManager.TryIndex(group, out var groupProto)) - continue; + LoadoutGroupsContainer.Visible = false; + SetSize = Vector2.Zero; + } + else + { + foreach (var group in proto.Groups) + { + if (!protoManager.TryIndex(group, out var groupProto)) + continue; - if (groupProto.Hidden) - continue; + if (groupProto.Hidden) + continue; - var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection); - LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name)); - _groups.Add(container); + var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection); + LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name)); + _groups.Add(container); - container.OnLoadoutPressed += args => - { - OnLoadoutPressed?.Invoke(group, args); - }; + container.OnLoadoutPressed += args => + { + OnLoadoutPressed?.Invoke(group, args); + }; - container.OnLoadoutUnpressed += args => - { - OnLoadoutUnpressed?.Invoke(group, args); - }; + container.OnLoadoutUnpressed += args => + { + OnLoadoutUnpressed?.Invoke(group, args); + }; + } } } diff --git a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs index a4d59d1f3150f3..03c74032f73bd3 100644 --- a/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs +++ b/Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs @@ -6,7 +6,7 @@ namespace Content.Client.Silicons.Laws.SiliconLawEditUi; public sealed class SiliconLawEui : BaseEui { - public readonly EntityManager _entityManager = default!; + private readonly EntityManager _entityManager; private SiliconLawUi _siliconLawUi; private EntityUid _target; diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs new file mode 100644 index 00000000000000..68318305a0c9e4 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs @@ -0,0 +1,28 @@ +using Content.Shared.Silicons.StationAi; +using Robust.Client.UserInterface; + +namespace Content.Client.Silicons.StationAi; + +public sealed class StationAiBoundUserInterface : BoundUserInterface +{ + private StationAiMenu? _menu; + + public StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + _menu = this.CreateWindow(); + _menu.Track(Owner); + + _menu.OnAiRadial += args => + { + SendPredictedMessage(new StationAiRadialMessage() + { + Event = args, + }); + }; + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml b/Content.Client/Silicons/StationAi/StationAiMenu.xaml new file mode 100644 index 00000000000000..d56fc832898490 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs new file mode 100644 index 00000000000000..24a802a60fe49c --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs @@ -0,0 +1,128 @@ +using System.Numerics; +using Content.Client.UserInterface.Controls; +using Content.Shared.Silicons.StationAi; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Timing; + +namespace Content.Client.Silicons.StationAi; + +[GenerateTypedNameReferences] +public sealed partial class StationAiMenu : RadialMenu +{ + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + + public event Action? OnAiRadial; + + private EntityUid _tracked; + + public StationAiMenu() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + } + + public void Track(EntityUid owner) + { + _tracked = owner; + + if (!_entManager.EntityExists(_tracked)) + { + Close(); + return; + } + + BuildButtons(); + UpdatePosition(); + } + + private void BuildButtons() + { + var ev = new GetStationAiRadialEvent(); + _entManager.EventBus.RaiseLocalEvent(_tracked, ref ev); + + var main = FindControl("Main"); + main.DisposeAllChildren(); + var sprites = _entManager.System(); + + foreach (var action in ev.Actions) + { + // TODO: This radial boilerplate is quite annoying + var button = new StationAiMenuButton(action.Event) + { + StyleClasses = { "RadialMenuButton" }, + SetSize = new Vector2(64f, 64f), + ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null, + }; + + if (action.Sprite != null) + { + var texture = sprites.Frame0(action.Sprite); + var scale = Vector2.One; + + if (texture.Width <= 32) + { + scale *= 2; + } + + var tex = new TextureRect + { + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Texture = texture, + TextureScale = scale, + }; + + button.AddChild(tex); + } + + button.OnPressed += args => + { + OnAiRadial?.Invoke(action.Event); + Close(); + }; + main.AddChild(button); + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + UpdatePosition(); + } + + private void UpdatePosition() + { + if (!_entManager.TryGetComponent(_tracked, out TransformComponent? xform)) + { + Close(); + return; + } + + if (!xform.Coordinates.IsValid(_entManager)) + { + Close(); + return; + } + + var coords = _entManager.System().GetSpriteScreenCoordinates((_tracked, null, xform)); + + if (!coords.IsValid) + { + Close(); + return; + } + + OpenScreenAt(coords.Position, _clyde); + } +} + +public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton +{ + public BaseStationAiAction Action = action; +} diff --git a/Content.Client/Silicons/StationAi/StationAiOverlay.cs b/Content.Client/Silicons/StationAi/StationAiOverlay.cs index efa1b8dbeff10a..15a8a3a63fe90a 100644 --- a/Content.Client/Silicons/StationAi/StationAiOverlay.cs +++ b/Content.Client/Silicons/StationAi/StationAiOverlay.cs @@ -4,7 +4,9 @@ using Robust.Client.Player; using Robust.Shared.Enums; using Robust.Shared.Map.Components; +using Robust.Shared.Physics; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; namespace Content.Client.Silicons.StationAi; @@ -12,6 +14,7 @@ public sealed class StationAiOverlay : Overlay { [Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IPrototypeManager _proto = default!; @@ -22,6 +25,9 @@ public sealed class StationAiOverlay : Overlay private IRenderTexture? _staticTexture; private IRenderTexture? _stencilTexture; + private float _updateRate = 1f / 30f; + private float _accumulator; + public StationAiOverlay() { IoCManager.InjectDependencies(this); @@ -47,19 +53,22 @@ protected override void Draw(in OverlayDrawArgs args) _entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform); var gridUid = playerXform?.GridUid ?? EntityUid.Invalid; _entManager.TryGetComponent(gridUid, out MapGridComponent? grid); + _entManager.TryGetComponent(gridUid, out BroadphaseComponent? broadphase); var invMatrix = args.Viewport.GetWorldToLocalMatrix(); + _accumulator -= (float) _timing.FrameTime.TotalSeconds; - if (grid != null) + if (grid != null && broadphase != null) { - // TODO: Pass in attached entity's grid. - // TODO: Credit OD on the moved to code - // TODO: Call the moved-to code here. - - _visibleTiles.Clear(); var lookups = _entManager.System(); var xforms = _entManager.System(); - _entManager.System().GetView((gridUid, grid), worldBounds, _visibleTiles); + + if (_accumulator <= 0f) + { + _accumulator = MathF.Max(0f, _accumulator + _updateRate); + _visibleTiles.Clear(); + _entManager.System().GetView((gridUid, broadphase, grid), worldBounds, _visibleTiles); + } var gridMatrix = xforms.GetWorldMatrix(gridUid); var matty = Matrix3x2.Multiply(gridMatrix, invMatrix); diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs b/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs new file mode 100644 index 00000000000000..bf6b65a9697b5a --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs @@ -0,0 +1,30 @@ +using Content.Shared.Doors.Components; +using Content.Shared.Silicons.StationAi; +using Robust.Shared.Utility; + +namespace Content.Client.Silicons.StationAi; + +public sealed partial class StationAiSystem +{ + private void InitializeAirlock() + { + SubscribeLocalEvent(OnDoorBoltGetRadial); + } + + private void OnDoorBoltGetRadial(Entity ent, ref GetStationAiRadialEvent args) + { + args.Actions.Add(new StationAiRadial() + { + Sprite = ent.Comp.BoltsDown ? + new SpriteSpecifier.Rsi( + new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "open") : + new SpriteSpecifier.Rsi( + new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "closed"), + Tooltip = ent.Comp.BoltsDown ? Loc.GetString("bolt-open") : Loc.GetString("bolt-close"), + Event = new StationAiBoltEvent() + { + Bolted = !ent.Comp.BoltsDown, + } + }); + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs b/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs new file mode 100644 index 00000000000000..cf2f6136207d35 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs @@ -0,0 +1,32 @@ +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Light.Components; +using Content.Shared.Silicons.StationAi; +using Robust.Shared.Utility; + +namespace Content.Client.Silicons.StationAi; + +public sealed partial class StationAiSystem +{ + // Used for surveillance camera lights + + private void InitializePowerToggle() + { + SubscribeLocalEvent(OnLightGetRadial); + } + + private void OnLightGetRadial(Entity ent, ref GetStationAiRadialEvent args) + { + if (!TryComp(ent.Owner, out ItemToggleComponent? toggle)) + return; + + args.Actions.Add(new StationAiRadial() + { + Tooltip = Loc.GetString("toggle-light"), + Sprite = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/light.svg.192dpi.png")), + Event = new StationAiLightEvent() + { + Enabled = !toggle.Activated + } + }); + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.cs b/Content.Client/Silicons/StationAi/StationAiSystem.cs index 2ed06175252985..ab9ace3c1d583d 100644 --- a/Content.Client/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Client/Silicons/StationAi/StationAiSystem.cs @@ -5,7 +5,7 @@ namespace Content.Client.Silicons.StationAi; -public sealed partial class StationAiSystem : EntitySystem +public sealed partial class StationAiSystem : SharedStationAiSystem { [Dependency] private readonly IOverlayManager _overlayMgr = default!; [Dependency] private readonly IPlayerManager _player = default!; @@ -15,8 +15,8 @@ public sealed partial class StationAiSystem : EntitySystem public override void Initialize() { base.Initialize(); - // InitializeAirlock(); - // InitializePowerToggle(); + InitializeAirlock(); + InitializePowerToggle(); SubscribeLocalEvent(OnAiAttached); SubscribeLocalEvent(OnAiDetached); diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index c3e03528a79232..e28f48d6a50d92 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -78,6 +78,7 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true // Get entities List entities; + var examineFlags = LookupFlags.All & ~LookupFlags.Sensors; // Do we have to do FoV checks? if ((visibility & MenuVisibility.NoFov) == 0) @@ -88,7 +89,7 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true TryComp(player.Value, out ExaminerComponent? examiner); entities = new(); - foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize)) + foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize, flags: examineFlags)) { if (_examine.CanExamine(player.Value, targetPos, Predicate, ent, examiner)) entities.Add(ent); @@ -96,7 +97,7 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true } else { - entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize).ToList(); + entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize, flags: examineFlags).ToList(); } if (entities.Count == 0) diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 6123bba8deeb5d..308a679846ccf9 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -36,6 +36,7 @@ using System.Linq; using System.Numerics; using Content.Server.Silicons.Laws; +using Content.Shared.Silicons.Laws; using Content.Shared.Silicons.Laws.Components; using Robust.Server.Player; using Robust.Shared.Physics.Components; diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index e616949d7a1f4a..3884c1cc439195 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -183,10 +183,6 @@ private static bool CanAnnounce(CommunicationsConsoleComponent comp) private bool CanUse(EntityUid user, EntityUid console) { - // This shouldn't technically be possible because of BUI but don't trust client. - if (!_interaction.InRangeUnobstructed(console, user)) - return false; - if (TryComp(console, out var accessReaderComponent) && !HasComp(console)) { return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent); diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 2b6ce41e191a1a..3c59c36f77e3fb 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -220,7 +220,9 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile) foreach (var role in profile.Loadouts) { - var loadout = new RoleLoadout(role.RoleName); + var loadout = new RoleLoadout(role.RoleName) + { + }; foreach (var group in role.Groups) { diff --git a/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs b/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs index f47a5df8ac401d..6e7bd255c5dc4c 100644 --- a/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs @@ -1,7 +1,6 @@ using Content.Server.DeviceNetwork.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; -using Content.Shared.Power.EntitySystems; namespace Content.Server.DeviceNetwork.Systems; diff --git a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs index 3c5f7eaecb2d10..5c66d65b573594 100644 --- a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs +++ b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Power; -using Content.Shared.Power.Components; namespace Content.Server.Light.EntitySystems { diff --git a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs index 6e1363dee2a2e4..3bd788bcf43f3f 100644 --- a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs +++ b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs @@ -26,7 +26,6 @@ using Content.Shared.Damage.Systems; using Content.Shared.Damage.Components; using Content.Shared.Power; -using Content.Shared.Power.Components; namespace Content.Server.Light.EntitySystems { diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index c3e29cc3635e14..2447d886411340 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -341,13 +341,13 @@ public override void SetUserId(EntityUid mindId, NetUserId? userId, MindComponen } } - public void ControlMob(EntityUid user, EntityUid target) + public override void ControlMob(EntityUid user, EntityUid target) { if (TryComp(user, out ActorComponent? actor)) ControlMob(actor.PlayerSession.UserId, target); } - public void ControlMob(NetUserId user, EntityUid target) + public override void ControlMob(NetUserId user, EntityUid target) { var (mindId, mind) = GetOrCreateMind(user); diff --git a/Content.Server/Power/EntitySystems/ChargerSystem.cs b/Content.Server/Power/EntitySystems/ChargerSystem.cs index df7bd2a54f7da8..40b998a95d00fb 100644 --- a/Content.Server/Power/EntitySystems/ChargerSystem.cs +++ b/Content.Server/Power/EntitySystems/ChargerSystem.cs @@ -8,7 +8,6 @@ using JetBrains.Annotations; using Robust.Shared.Containers; using System.Diagnostics.CodeAnalysis; -using Content.Shared.Power.Components; using Content.Shared.Storage.Components; using Robust.Server.Containers; using Content.Shared.Whitelist; @@ -64,7 +63,7 @@ private void OnChargerExamine(EntityUid uid, ChargerComponent component, Examine } else { - // add how much each item is charged it + // add how much each item is charged it foreach (var contained in container.ContainedEntities) { if (!TryComp(contained, out var battery)) @@ -232,7 +231,7 @@ private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component) return CellChargerStatus.Charging; } - + private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime) { if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent)) diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 9ce48141396296..a7098649ceffb1 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -5,7 +5,6 @@ using Content.Server.Power.Pow3r; using Content.Shared.CCVar; using Content.Shared.Power; -using Content.Shared.Power.Components; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Configuration; diff --git a/Content.Server/Power/Generation/Teg/TegSystem.cs b/Content.Server/Power/Generation/Teg/TegSystem.cs index edf0693954c5d8..9fb7d5ff1f649b 100644 --- a/Content.Server/Power/Generation/Teg/TegSystem.cs +++ b/Content.Server/Power/Generation/Teg/TegSystem.cs @@ -11,7 +11,6 @@ using Content.Shared.DeviceNetwork; using Content.Shared.Examine; using Content.Shared.Power; -using Content.Shared.Power.Components; using Content.Shared.Power.Generation.Teg; using Content.Shared.Rounding; using Robust.Server.GameObjects; diff --git a/Content.Server/Power/Generator/GasPowerReceiverSystem.cs b/Content.Server/Power/Generator/GasPowerReceiverSystem.cs index 5a1bd31a15c363..e3979a65192957 100644 --- a/Content.Server/Power/Generator/GasPowerReceiverSystem.cs +++ b/Content.Server/Power/Generator/GasPowerReceiverSystem.cs @@ -6,7 +6,6 @@ using Content.Server.Power.Components; using Content.Shared.Atmos; using Content.Shared.Power; -using Content.Shared.Power.Components; namespace Content.Server.Power.Generator; diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs index acffd057fad0db..bb2c1cbbbfc6ea 100644 --- a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs +++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Content.Server.NPC.Pathfinding; using Content.Shared.Maps; +using Content.Shared.NPC; using Content.Shared.Procedural; using Content.Shared.Procedural.DungeonGenerators; using Robust.Shared.Collections; @@ -29,7 +30,7 @@ private async Task> GenerateExteriorDungen(Vector2i position, Exte var pathfinder = _entManager.System(); // Gridcast - pathfinder.GridCast(startTile, position, tile => + SharedPathfindingSystem.GridCast(startTile, position, tile => { if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) || tileRef.Tile.IsSpace(_tileDefManager)) diff --git a/Content.Server/Silicons/Laws/SiliconLawSystem.cs b/Content.Server/Silicons/Laws/SiliconLawSystem.cs index 0c0f68c23f352e..6b7df52a6ebc1d 100644 --- a/Content.Server/Silicons/Laws/SiliconLawSystem.cs +++ b/Content.Server/Silicons/Laws/SiliconLawSystem.cs @@ -5,12 +5,10 @@ using Content.Server.Radio.Components; using Content.Server.Roles; using Content.Server.Station.Systems; -using Content.Shared.Actions; using Content.Shared.Administration; using Content.Shared.Chat; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; -using Content.Shared.Examine; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Roles; @@ -19,10 +17,10 @@ using Content.Shared.Stunnable; using Content.Shared.Wires; using Robust.Server.GameObjects; +using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; -using Robust.Shared.Utility; namespace Content.Server.Silicons.Laws; @@ -32,11 +30,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly StationSystem _station = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly SharedStunSystem _stunSystem = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly SharedRoleSystem _roles = default!; /// @@ -44,7 +40,6 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnToggleLawsScreen); @@ -58,15 +53,8 @@ public override void Initialize() SubscribeLocalEvent(OnEmagMindRemoved); } - private void OnComponentShutdown(EntityUid uid, SiliconLawBoundComponent component, ComponentShutdown args) - { - if (component.ViewLawsActionEntity != null) - _actions.RemoveAction(uid, component.ViewLawsActionEntity); - } - private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args) { - _actions.AddAction(uid, ref component.ViewLawsActionEntity, component.ViewLawsAction); GetLaws(uid, component); } @@ -92,7 +80,7 @@ private void OnToggleLawsScreen(EntityUid uid, SiliconLawBoundComponent componen private void OnBoundUIOpened(EntityUid uid, SiliconLawBoundComponent component, BoundUIOpenedEvent args) { - _entityManager.TryGetComponent(uid, out var intrinsicRadio); + TryComp(uid, out IntrinsicRadioTransmitterComponent? intrinsicRadio); var radioChannels = intrinsicRadio?.Channels; var state = new SiliconLawBuiState(GetLaws(uid).Laws, radioChannels); @@ -264,9 +252,9 @@ public void NotifyLawsChanged(EntityUid uid) /// /// Extract all the laws from a lawset's prototype ids. /// - public SiliconLawset GetLawset(string lawset) + public SiliconLawset GetLawset(ProtoId lawset) { - var proto = _prototype.Index(lawset); + var proto = _prototype.Index(lawset); var laws = new SiliconLawset() { Laws = new List(proto.Laws.Count) @@ -294,6 +282,21 @@ public void SetLaws(List newLaws, EntityUid target) component.Lawset.Laws = newLaws; NotifyLawsChanged(target); } + + protected override void OnUpdaterInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + { + // TODO: Prediction dump this + if (!TryComp(args.Entity, out SiliconLawProviderComponent? provider)) + return; + + var lawset = GetLawset(provider.Laws).Laws; + var query = EntityManager.CompRegistryQueryEnumerator(ent.Comp.Components); + + while (query.MoveNext(out var update)) + { + SetLaws(lawset, update); + } + } } [ToolshedCommand, AdminCommand(AdminFlags.Admin)] diff --git a/Content.Server/Silicons/StationAi/AiInteractWireAction.cs b/Content.Server/Silicons/StationAi/AiInteractWireAction.cs new file mode 100644 index 00000000000000..c92c825b32b303 --- /dev/null +++ b/Content.Server/Silicons/StationAi/AiInteractWireAction.cs @@ -0,0 +1,37 @@ +using Content.Server.Wires; +using Content.Shared.Doors; +using Content.Shared.Silicons.StationAi; +using Content.Shared.Wires; + +namespace Content.Server.Silicons.StationAi; + +/// +/// Controls whether an AI can interact with the target entity. +/// +public sealed partial class AiInteractWireAction : ComponentWireAction +{ + public override string Name { get; set; } = "wire-name-ai-act-light"; + public override Color Color { get; set; } = Color.DeepSkyBlue; + public override object StatusKey => AirlockWireStatus.AiControlIndicator; + + public override StatusLightState? GetLightState(Wire wire, StationAiWhitelistComponent component) + { + return component.Enabled ? StatusLightState.On : StatusLightState.Off; + } + + public override bool Cut(EntityUid user, Wire wire, StationAiWhitelistComponent component) + { + return EntityManager.System() + .SetWhitelistEnabled((component.Owner, component), false, announce: true); + } + + public override bool Mend(EntityUid user, Wire wire, StationAiWhitelistComponent component) + { + return EntityManager.System() + .SetWhitelistEnabled((component.Owner, component), true); + } + + public override void Pulse(EntityUid user, Wire wire, StationAiWhitelistComponent component) + { + } +} diff --git a/Content.Server/Silicons/StationAi/AiVisionWireAction.cs b/Content.Server/Silicons/StationAi/AiVisionWireAction.cs new file mode 100644 index 00000000000000..3523f4d38f033d --- /dev/null +++ b/Content.Server/Silicons/StationAi/AiVisionWireAction.cs @@ -0,0 +1,40 @@ +using Content.Server.Wires; +using Content.Shared.Doors; +using Content.Shared.Silicons.StationAi; +using Content.Shared.StationAi; +using Content.Shared.Wires; + +namespace Content.Server.Silicons.StationAi; + +/// +/// Handles StationAiVision functionality for the attached entity. +/// +public sealed partial class AiVisionWireAction : ComponentWireAction +{ + public override string Name { get; set; } = "wire-name-ai-vision-light"; + public override Color Color { get; set; } = Color.DeepSkyBlue; + public override object StatusKey => AirlockWireStatus.AiControlIndicator; + + public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component) + { + return component.Enabled ? StatusLightState.On : StatusLightState.Off; + } + + public override bool Cut(EntityUid user, Wire wire, StationAiVisionComponent component) + { + return EntityManager.System() + .SetVisionEnabled((component.Owner, component), false, announce: true); + } + + public override bool Mend(EntityUid user, Wire wire, StationAiVisionComponent component) + { + return EntityManager.System() + .SetVisionEnabled((component.Owner, component), true); + } + + public override void Pulse(EntityUid user, Wire wire, StationAiVisionComponent component) + { + // TODO: This should turn it off for a bit + // Need timer cleanup first out of scope. + } +} diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs new file mode 100644 index 00000000000000..846497387d24ae --- /dev/null +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -0,0 +1,76 @@ +using System.Linq; +using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; +using Content.Shared.Chat; +using Content.Shared.Silicons.StationAi; +using Content.Shared.StationAi; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; + +namespace Content.Server.Silicons.StationAi; + +public sealed class StationAiSystem : SharedStationAiSystem +{ + [Dependency] private readonly IChatManager _chats = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + + private readonly HashSet> _ais = new(); + + public override bool SetVisionEnabled(Entity entity, bool enabled, bool announce = false) + { + if (!base.SetVisionEnabled(entity, enabled, announce)) + return false; + + if (announce) + { + AnnounceSnip(entity.Owner); + } + + return true; + } + + public override bool SetWhitelistEnabled(Entity entity, bool enabled, bool announce = false) + { + if (!base.SetWhitelistEnabled(entity, enabled, announce)) + return false; + + if (announce) + { + AnnounceSnip(entity.Owner); + } + + return true; + } + + private void AnnounceSnip(EntityUid entity) + { + var xform = Transform(entity); + + if (!TryComp(xform.GridUid, out MapGridComponent? grid)) + return; + + _ais.Clear(); + _lookup.GetChildEntities(xform.GridUid.Value, _ais); + var filter = Filter.Empty(); + + foreach (var ai in _ais) + { + // TODO: Filter API? + if (TryComp(ai.Owner, out ActorComponent? actorComp)) + { + filter.AddPlayer(actorComp.PlayerSession); + } + } + + // TEST + // filter = Filter.Broadcast(); + + // No easy way to do chat notif embeds atm. + var tile = Maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates); + var msg = Loc.GetString("ai-wire-snipped", ("coords", tile)); + + _chats.ChatMessageToMany(ChatChannel.Notifications, msg, msg, entity, false, true, filter.Recipients.Select(o => o.Channel)); + // Apparently there's no sound for this. + } +} diff --git a/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs b/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs index d2c2a8a1ca7adb..1ad1bb6c0a1222 100644 --- a/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs +++ b/Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Power; -using Content.Shared.Power.Components; using Content.Shared.Sound; using Content.Shared.Sound.Components; diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs index e960a2bbbe2c7a..fcbc3aa52f6987 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -17,6 +17,7 @@ using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; +using Content.Shared.Preferences.Loadouts.Effects; using Content.Shared.Random; using Content.Shared.Random.Helpers; using Content.Shared.Roles; @@ -150,6 +151,22 @@ public EntityUid SpawnPlayerMob( EntityUid? entity = null) { _prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var prototype); + RoleLoadout? loadout = null; + + // Need to get the loadout up-front to handle names if we use an entity spawn override. + var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID); + + if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto)) + { + profile?.Loadouts.TryGetValue(jobLoadout, out loadout); + + // Set to default if not present + if (loadout == null) + { + loadout = new RoleLoadout(jobLoadout); + loadout.SetDefault(profile, _actors.GetSession(entity), _prototypeManager); + } + } // If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff. if (prototype?.JobEntity != null) @@ -157,6 +174,13 @@ public EntityUid SpawnPlayerMob( DebugTools.Assert(entity is null); var jobEntity = EntityManager.SpawnEntity(prototype.JobEntity, coordinates); MakeSentientCommand.MakeSentient(jobEntity, EntityManager); + + // Make sure custom names get handled, what is gameticker control flow whoopy. + if (loadout != null) + { + EquipRoleName(jobEntity, loadout, roleProto!); + } + DoJobSpecials(job, jobEntity); _identity.QueueIdentityUpdate(jobEntity); return jobEntity; @@ -188,21 +212,9 @@ public EntityUid SpawnPlayerMob( profile = HumanoidCharacterProfile.RandomWithSpecies(speciesId); } - var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID); - - if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto)) + if (loadout != null) { - RoleLoadout? loadout = null; - profile?.Loadouts.TryGetValue(jobLoadout, out loadout); - - // Set to default if not present - if (loadout == null) - { - loadout = new RoleLoadout(jobLoadout); - loadout.SetDefault(profile, _actors.GetSession(entity), _prototypeManager); - } - - EquipRoleLoadout(entity.Value, loadout, roleProto); + EquipRoleLoadout(entity.Value, loadout, roleProto!); } if (prototype?.StartingGear != null) diff --git a/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs index 5970e16319615a..c9be87c623188b 100644 --- a/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs +++ b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs @@ -38,7 +38,7 @@ private void OnInteractUsing(EntityUid uid, EnergySwordComponent comp, InteractU if (args.Handled) return; - if (!_toolSystem.HasQuality(args.Used, "Pulsing")) + if (!_toolSystem.HasQuality(args.Used, SharedToolSystem.PulseQuality)) return; args.Handled = true; diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs index 019e09bbbbc018..9d2fd58980f2ce 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs @@ -44,7 +44,7 @@ private void OnInteractUsing(EntityUid uid, ArtifactElectricityTriggerComponent if (args.Handled) return; - if (!_toolSystem.HasQuality(args.Used, "Pulsing")) + if (!_toolSystem.HasQuality(args.Used, SharedToolSystem.PulseQuality)) return; args.Handled = _artifactSystem.TryActivateArtifact(uid, args.User); diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index afa1e19eaded26..8a4b5baffd34d2 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Emoting; using Content.Shared.Hands; using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; using Content.Shared.Interaction.Events; using Content.Shared.Item; using Content.Shared.Movement.Components; @@ -22,9 +23,14 @@ public sealed class ActionBlockerSystem : EntitySystem { [Dependency] private readonly SharedContainerSystem _container = default!; + private EntityQuery _complexInteractionQuery; + public override void Initialize() { base.Initialize(); + + _complexInteractionQuery = GetEntityQuery(); + SubscribeLocalEvent(OnMoverStartup); } @@ -53,6 +59,15 @@ public bool UpdateCanMove(EntityUid uid, InputMoverComponent? component = null) return !ev.Cancelled; } + /// + /// Checks if a given entity is able to do specific complex interactions. + /// This is used to gate manipulation to general humanoids. If a mouse shouldn't be able to do something, then it's complex. + /// + public bool CanComplexInteract(EntityUid user) + { + return _complexInteractionQuery.HasComp(user); + } + /// /// Raises an event directed at both the user and the target entity to check whether a user is capable of /// interacting with this entity. diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index 9b77d039f465e4..da194706f8f0a6 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -358,34 +358,26 @@ private void OnClimbEndCollide(EntityUid uid, ClimbingComponent component, ref E return; } - if (args.OurFixture.Contacts.Count > 1) + foreach (var contact in args.OurFixture.Contacts.Values) { - foreach (var contact in args.OurFixture.Contacts.Values) + if (!contact.IsTouching) + continue; + + var otherEnt = contact.OtherEnt(uid); + var (otherFixtureId, otherFixture) = contact.OtherFixture(uid); + + // TODO: Remove this on engine. + if (args.OtherEntity == otherEnt && args.OtherFixtureId == otherFixtureId) + continue; + + if (otherFixture is { Hard: true } && + _climbableQuery.HasComp(otherEnt)) { - if (!contact.IsTouching) - continue; - - var otherEnt = contact.EntityA; - var otherFixture = contact.FixtureA; - var otherFixtureId = contact.FixtureAId; - if (uid == contact.EntityA) - { - otherEnt = contact.EntityB; - otherFixture = contact.FixtureB; - otherFixtureId = contact.FixtureBId; - } - - if (args.OtherEntity == otherEnt && args.OtherFixtureId == otherFixtureId) - continue; - - if (otherFixture is { Hard: true } && - _climbableQuery.HasComp(otherEnt)) - { - return; - } + return; } } + // TODO: Is this even needed anymore? foreach (var otherFixture in args.OurFixture.Contacts.Keys) { // If it's the other fixture then ignore em diff --git a/Content.Shared/Configurable/ConfigurationComponent.cs b/Content.Shared/Configurable/ConfigurationComponent.cs index 63a0dfe95a50fb..621871af3cee5f 100644 --- a/Content.Shared/Configurable/ConfigurationComponent.cs +++ b/Content.Shared/Configurable/ConfigurationComponent.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Content.Shared.Tools; +using Content.Shared.Tools.Systems; using Robust.Shared.GameStates; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -13,7 +14,7 @@ public sealed partial class ConfigurationComponent : Component public Dictionary Config = new(); [DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string QualityNeeded = "Pulsing"; + public string QualityNeeded = SharedToolSystem.PulseQuality; [DataField("validation")] public Regex Validation = new("^[a-zA-Z0-9 ]*$", RegexOptions.Compiled); diff --git a/Content.Shared/Doors/AirlockWireStatus.cs b/Content.Shared/Doors/AirlockWireStatus.cs index a50ee2c88e9cca..d3fa15ed1b650a 100644 --- a/Content.Shared/Doors/AirlockWireStatus.cs +++ b/Content.Shared/Doors/AirlockWireStatus.cs @@ -8,7 +8,7 @@ public enum AirlockWireStatus PowerIndicator, BoltIndicator, BoltLightIndicator, - AIControlIndicator, + AiControlIndicator, TimingIndicator, SafetyIndicator, } diff --git a/Content.Shared/Examine/ExamineSystemShared.cs b/Content.Shared/Examine/ExamineSystemShared.cs index f0406c539891da..3d83886f2d34ba 100644 --- a/Content.Shared/Examine/ExamineSystemShared.cs +++ b/Content.Shared/Examine/ExamineSystemShared.cs @@ -117,12 +117,25 @@ public virtual bool CanExamine(EntityUid examiner, MapCoordinates target, Ignore if (EntityManager.GetComponent(examiner).MapID != target.MapId) return false; - return InRangeUnOccluded( - _transform.GetMapCoordinates(examiner), - target, - GetExaminerRange(examiner), - predicate: predicate, - ignoreInsideBlocker: true); + // Do target InRangeUnoccluded which has different checks. + if (examined != null) + { + return InRangeUnOccluded( + examiner, + examined.Value, + GetExaminerRange(examiner), + predicate: predicate, + ignoreInsideBlocker: true); + } + else + { + return InRangeUnOccluded( + examiner, + target, + GetExaminerRange(examiner), + predicate: predicate, + ignoreInsideBlocker: true); + } } /// @@ -214,6 +227,14 @@ public bool InRangeUnOccluded(MapCoordinates origin, MapCoordinates othe public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true) { + var ev = new InRangeOverrideEvent(origin, other); + RaiseLocalEvent(origin, ref ev); + + if (ev.Handled) + { + return ev.InRange; + } + var originPos = _transform.GetMapCoordinates(origin); var otherPos = _transform.GetMapCoordinates(other); diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs index 1fe66cd3cb8626..84beabf9ac859f 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs @@ -161,6 +161,19 @@ public bool TryGetActiveItem(Entity entity, [NotNullWhen(true)] return item != null; } + /// + /// Gets active hand item if relevant otherwise gets the entity itself. + /// + public EntityUid GetActiveItemOrSelf(Entity entity) + { + if (!TryGetActiveItem(entity, out var item)) + { + return entity.Owner; + } + + return item.Value; + } + public Hand? GetActiveHand(Entity entity) { if (!Resolve(entity, ref entity.Comp)) diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index e8cc831e4957c4..8539b9d282b8fd 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -17,6 +17,7 @@ using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Physics; using Content.Shared.Popups; +using Content.Shared.Silicons.StationAi; using Content.Shared.Storage; using Content.Shared.Tag; using Content.Shared.Timing; @@ -74,7 +75,6 @@ public abstract partial class SharedInteractionSystem : EntitySystem private EntityQuery _wallMountQuery; private EntityQuery _delayQuery; private EntityQuery _uiQuery; - private EntityQuery _complexInteractionQuery; private const CollisionGroup InRangeUnobstructedMask = CollisionGroup.Impassable | CollisionGroup.InteractImpassable; @@ -97,7 +97,6 @@ public override void Initialize() _wallMountQuery = GetEntityQuery(); _delayQuery = GetEntityQuery(); _uiQuery = GetEntityQuery(); - _complexInteractionQuery = GetEntityQuery(); SubscribeLocalEvent(HandleUserInterfaceRangeCheck); SubscribeLocalEvent(OnBoundInterfaceInteractAttempt); @@ -165,7 +164,7 @@ private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev return; } - if (uiComp.RequireHands && !_handsQuery.HasComp(ev.Actor)) + if (uiComp.RequiresComplex && !_actionBlockerSystem.CanComplexInteract(ev.Actor)) ev.Cancel(); } @@ -440,7 +439,7 @@ public void UserInteraction( public void InteractHand(EntityUid user, EntityUid target) { - var complexInteractions = SupportsComplexInteractions(user); + var complexInteractions = _actionBlockerSystem.CanComplexInteract(user); if (!complexInteractions) { InteractionActivate(user, @@ -630,6 +629,14 @@ public bool InRangeUnobstructed( if (!Resolve(other, ref other.Comp)) return false; + var ev = new InRangeOverrideEvent(origin, other); + RaiseLocalEvent(origin, ref ev); + + if (ev.Handled) + { + return ev.InRange; + } + return InRangeUnobstructed(origin, other, other.Comp.Coordinates, @@ -1128,7 +1135,7 @@ public bool AltInteract(EntityUid user, EntityUid target) // Get list of alt-interact verbs var verbs = _verbSystem.GetLocalVerbs(target, user, typeof(AlternativeVerb)); - if (!verbs.Any()) + if (verbs.Count == 0) return false; _verbSystem.ExecuteVerb(verbs.First(), user, target); @@ -1182,6 +1189,13 @@ public bool InRangeAndAccessible( /// public bool IsAccessible(Entity user, Entity target) { + var ev = new AccessibleOverrideEvent(user, target); + + RaiseLocalEvent(user, ref ev); + + if (ev.Handled) + return ev.Accessible; + if (_containerSystem.IsInSameOrParentContainer(user, target, out _, out var container)) return true; @@ -1324,13 +1338,10 @@ public bool TryGetUsedEntity(EntityUid user, [NotNullWhen(true)] out EntityUid? return ev.Handled; } - /// - /// Checks if a given entity is able to do specific complex interactions. - /// This is used to gate manipulation to general humanoids. If a mouse shouldn't be able to do something, then it's complex. - /// + [Obsolete("Use ActionBlockerSystem")] public bool SupportsComplexInteractions(EntityUid user) { - return _complexInteractionQuery.HasComp(user); + return _actionBlockerSystem.CanComplexInteract(user); } } @@ -1369,17 +1380,38 @@ public record struct GetUsedEntityEvent() }; /// - /// Raised directed by-ref on an item and a user to determine if interactions can occur. + /// Raised directed by-ref on an item to determine if hand interactions should go through. + /// Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead. /// /// Whether the hand interaction should be cancelled. [ByRefEvent] - public record struct AttemptUseInteractEvent(EntityUid User, EntityUid Used, bool Cancelled = false); + public record struct CombatModeShouldHandInteractEvent(bool Cancelled = false); /// - /// Raised directed by-ref on an item to determine if hand interactions should go through. - /// Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead. + /// Override event raised directed on the user to say the target is accessible. /// - /// Whether the hand interaction should be cancelled. + /// + /// [ByRefEvent] - public record struct CombatModeShouldHandInteractEvent(bool Cancelled = false); + public record struct AccessibleOverrideEvent(EntityUid User, EntityUid Target) + { + public readonly EntityUid User = User; + public readonly EntityUid Target = Target; + + public bool Handled; + public bool Accessible = false; + } + + /// + /// Override event raised directed on a user to check InRangeUnoccluded AND InRangeUnobstructed to the target if you require custom logic. + /// + [ByRefEvent] + public record struct InRangeOverrideEvent(EntityUid User, EntityUid Target) + { + public readonly EntityUid User = User; + public readonly EntityUid Target = Target; + + public bool Handled; + public bool InRange = false; + } } diff --git a/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs b/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs new file mode 100644 index 00000000000000..39be05a1480a55 --- /dev/null +++ b/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Light.Components; + +/// +/// Can activate when collided with. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class LightOnCollideColliderComponent : Component +{ + [DataField] + public string FixtureId = "lightTrigger"; +} diff --git a/Content.Shared/Light/Components/LightOnCollideComponent.cs b/Content.Shared/Light/Components/LightOnCollideComponent.cs new file mode 100644 index 00000000000000..c3b4bd739651ab --- /dev/null +++ b/Content.Shared/Light/Components/LightOnCollideComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Light.Components; + +/// +/// Enables / disables pointlight whenever entities are contacting with it +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class LightOnCollideComponent : Component +{ +} diff --git a/Content.Shared/Light/EntitySystems/LightCollideSystem.cs b/Content.Shared/Light/EntitySystems/LightCollideSystem.cs new file mode 100644 index 00000000000000..f09ae6824ea8ac --- /dev/null +++ b/Content.Shared/Light/EntitySystems/LightCollideSystem.cs @@ -0,0 +1,82 @@ +using Content.Shared.Light.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; + +namespace Content.Shared.Light.EntitySystems; + +public sealed class LightCollideSystem : EntitySystem +{ + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SlimPoweredLightSystem _lights = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnStart); + SubscribeLocalEvent(OnEnd); + + SubscribeLocalEvent(OnCollideShutdown); + } + + private void OnCollideShutdown(Entity ent, ref ComponentShutdown args) + { + // TODO: Check this on the event. + if (TerminatingOrDeleted(ent.Owner)) + return; + + // Regenerate contacts for everything we were colliding with. + var contacts = _physics.GetContacts(ent.Owner); + + while (contacts.MoveNext(out var contact)) + { + if (!contact.IsTouching) + continue; + + var other = contact.OtherEnt(ent.Owner); + + if (HasComp(other)) + { + _physics.RegenerateContacts(other); + } + } + } + + // You may be wondering what de fok this is doing here. + // At the moment there's no easy way to do collision whitelists based on components. + private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (!HasComp(args.OtherEntity)) + { + args.Cancelled = true; + } + } + + private void OnEnd(Entity ent, ref EndCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!HasComp(args.OtherEntity)) + return; + + // TODO: Engine bug IsTouching box2d yay. + var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1; + + if (contacts > 0) + return; + + _lights.SetEnabled(args.OtherEntity, false); + } + + private void OnStart(Entity ent, ref StartCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!HasComp(args.OtherEntity)) + return; + + _lights.SetEnabled(args.OtherEntity, true); + } +} diff --git a/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs b/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs index 6d984ed19a364d..4cf9b25dadcb29 100644 --- a/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs +++ b/Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.Light.Components; using Content.Shared.Power; -using Content.Shared.Power.Components; using Content.Shared.Power.EntitySystems; namespace Content.Shared.Light.EntitySystems; diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index 24b47b641205d2..c8e1c1a4b3a332 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -315,6 +315,10 @@ public virtual void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostCh { } + public virtual void ControlMob(EntityUid user, EntityUid target) {} + + public virtual void ControlMob(NetUserId user, EntityUid target) {} + /// /// Tries to create and add an objective from its prototype id. /// diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs b/Content.Shared/NPC/SharedPathfindingSystem.Line.cs similarity index 92% rename from Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs rename to Content.Shared/NPC/SharedPathfindingSystem.Line.cs index 479d5ad77f6b12..500ed157436998 100644 --- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs +++ b/Content.Shared/NPC/SharedPathfindingSystem.Line.cs @@ -1,8 +1,8 @@ -namespace Content.Server.NPC.Pathfinding; +namespace Content.Shared.NPC; -public sealed partial class PathfindingSystem +public abstract partial class SharedPathfindingSystem { - public void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback) + public static void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback) { // https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566 // declare all locals at the top so it's obvious how big the footprint is diff --git a/Content.Shared/NPC/SharedPathfindingSystem.cs b/Content.Shared/NPC/SharedPathfindingSystem.cs index 8831acc1ddb733..0be5f69755622f 100644 --- a/Content.Shared/NPC/SharedPathfindingSystem.cs +++ b/Content.Shared/NPC/SharedPathfindingSystem.cs @@ -2,7 +2,7 @@ namespace Content.Shared.NPC; -public abstract class SharedPathfindingSystem : EntitySystem +public abstract partial class SharedPathfindingSystem : EntitySystem { /// /// This is equivalent to agent radii for navmeshes. In our case it's preferable that things are cleanly @@ -37,4 +37,31 @@ public static float OctileDistance(Vector2i start, Vector2i end) var ab = Vector2.Abs(diff); return ab.X + ab.Y + (1.41f - 2) * Math.Min(ab.X, ab.Y); } + + public static IEnumerable GetTileOutline(Vector2i center, float radius) + { + // https://www.redblobgames.com/grids/circle-drawing/ + var vecCircle = center + Vector2.One / 2f; + + for (var r = 0; r <= Math.Floor(radius * MathF.Sqrt(0.5f)); r++) + { + var d = MathF.Floor(MathF.Sqrt(radius * radius - r * r)); + + yield return new Vector2(vecCircle.X - d, vecCircle.Y + r).Floored(); + + yield return new Vector2(vecCircle.X + d, vecCircle.Y + r).Floored(); + + yield return new Vector2(vecCircle.X - d, vecCircle.Y - r).Floored(); + + yield return new Vector2(vecCircle.X + d, vecCircle.Y - r).Floored(); + + yield return new Vector2(vecCircle.X + r, vecCircle.Y - d).Floored(); + + yield return new Vector2(vecCircle.X + r, vecCircle.Y + d).Floored(); + + yield return new Vector2(vecCircle.X - r, vecCircle.Y - d).Floored(); + + yield return new Vector2(vecCircle.X - r, vecCircle.Y + d).Floored(); + } + } } diff --git a/Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs b/Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs index 36619ab1046c4b..7a29bad6678891 100644 --- a/Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs +++ b/Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared.Dataset; using Robust.Shared.Prototypes; namespace Content.Shared.Preferences.Loadouts; @@ -15,10 +16,17 @@ public sealed partial class RoleLoadoutPrototype : IPrototype [IdDataField] public string ID { get; } = string.Empty; + /// + /// Should we use a random name for this loadout? + /// + [DataField] + public ProtoId? NameDataset; + + // Not required so people can set their names. /// /// Groups that comprise this role loadout. /// - [DataField(required: true)] + [DataField] public List> Groups = new(); /// diff --git a/Content.Shared/Silicons/Laws/Components/SiliconLawBoundComponent.cs b/Content.Shared/Silicons/Laws/Components/SiliconLawBoundComponent.cs index 824d057b3ea011..0fb9c5920fa759 100644 --- a/Content.Shared/Silicons/Laws/Components/SiliconLawBoundComponent.cs +++ b/Content.Shared/Silicons/Laws/Components/SiliconLawBoundComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Actions; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -7,21 +8,9 @@ namespace Content.Shared.Silicons.Laws.Components; /// /// This is used for entities which are bound to silicon laws and can view them. /// -[RegisterComponent, Access(typeof(SharedSiliconLawSystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(SharedSiliconLawSystem))] public sealed partial class SiliconLawBoundComponent : Component { - /// - /// The sidebar action that toggles the laws screen. - /// - [DataField] - public EntProtoId ViewLawsAction = "ActionViewLaws"; - - /// - /// The action for toggling laws. Stored here so we can remove it later. - /// - [DataField] - public EntityUid? ViewLawsActionEntity; - /// /// The last entity that provided laws to this entity. /// diff --git a/Content.Shared/Silicons/Laws/Components/SiliconLawUpdaterComponent.cs b/Content.Shared/Silicons/Laws/Components/SiliconLawUpdaterComponent.cs new file mode 100644 index 00000000000000..e28bf883d91c81 --- /dev/null +++ b/Content.Shared/Silicons/Laws/Components/SiliconLawUpdaterComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Silicons.Laws.Components; + +/// +/// Whenever an entity is inserted with silicon laws it will update the relevant entity's laws. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class SiliconLawUpdaterComponent : Component +{ + /// + /// Entities to update + /// + [DataField(required: true)] + public ComponentRegistry Components; +} diff --git a/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.Updater.cs b/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.Updater.cs new file mode 100644 index 00000000000000..9fbef58a842070 --- /dev/null +++ b/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.Updater.cs @@ -0,0 +1,17 @@ +using Content.Shared.Silicons.Laws.Components; +using Robust.Shared.Containers; + +namespace Content.Shared.Silicons.Laws; + +public abstract partial class SharedSiliconLawSystem +{ + private void InitializeUpdater() + { + SubscribeLocalEvent(OnUpdaterInsert); + } + + protected virtual void OnUpdaterInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + { + // TODO: Prediction + } +} diff --git a/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.cs b/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.cs index c0619e6e43d47d..a30e7c8980f98c 100644 --- a/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.cs +++ b/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.cs @@ -8,13 +8,14 @@ namespace Content.Shared.Silicons.Laws; /// /// This handles getting and displaying the laws for silicons. /// -public abstract class SharedSiliconLawSystem : EntitySystem +public abstract partial class SharedSiliconLawSystem : EntitySystem { [Dependency] private readonly SharedPopupSystem _popup = default!; /// public override void Initialize() { + InitializeUpdater(); SubscribeLocalEvent(OnGotEmagged); SubscribeLocalEvent(OnAttemptEmag); } diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs new file mode 100644 index 00000000000000..ff6fc1ece07967 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs @@ -0,0 +1,25 @@ +using Content.Shared.Doors.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Silicons.StationAi; + +public abstract partial class SharedStationAiSystem +{ + // Handles airlock radial + + private void InitializeAirlock() + { + SubscribeLocalEvent(OnAirlockBolt); + } + + private void OnAirlockBolt(EntityUid ent, DoorBoltComponent component, StationAiBoltEvent args) + { + _doors.SetBoltsDown((ent, component), args.Bolted, args.User, predicted: true); + } +} + +[Serializable, NetSerializable] +public sealed class StationAiBoltEvent : BaseStationAiAction +{ + public bool Bolted; +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs new file mode 100644 index 00000000000000..a6c57f5940012e --- /dev/null +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs @@ -0,0 +1,187 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Actions.Events; +using Content.Shared.Interaction.Events; +using Content.Shared.Verbs; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Silicons.StationAi; + +public abstract partial class SharedStationAiSystem +{ + /* + * Added when an entity is inserted into a StationAiCore. + */ + + private void InitializeHeld() + { + SubscribeLocalEvent(OnRadialMessage); + SubscribeLocalEvent(OnMessageAttempt); + SubscribeLocalEvent>(OnTargetVerbs); + + SubscribeLocalEvent(OnHeldInteraction); + SubscribeLocalEvent(OnHeldRelay); + SubscribeLocalEvent(OnCoreJump); + } + + private void OnCoreJump(Entity ent, ref JumpToCoreEvent args) + { + if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null) + return; + + _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ; + } + + /// + /// Tries to get the entity held in the AI core. + /// + private bool TryGetHeld(Entity entity, out EntityUid held) + { + held = EntityUid.Invalid; + + if (!Resolve(entity.Owner, ref entity.Comp)) + return false; + + if (!_containers.TryGetContainer(entity.Owner, StationAiCoreComponent.Container, out var container) || + container.ContainedEntities.Count == 0) + return false; + + held = container.ContainedEntities[0]; + return true; + } + + private bool TryGetCore(EntityUid ent, out Entity core) + { + if (!_containers.TryGetContainingContainer(ent, out var container) || + container.ID != StationAiCoreComponent.Container || + !TryComp(container.Owner, out StationAiCoreComponent? coreComp) || + coreComp.RemoteEntity == null) + { + core = (EntityUid.Invalid, null); + return false; + } + + core = (container.Owner, coreComp); + return true; + } + + private void OnHeldRelay(Entity ent, ref AttemptRelayActionComponentChangeEvent args) + { + if (!TryGetCore(ent.Owner, out var core)) + return; + + args.Target = core.Comp?.RemoteEntity; + } + + private void OnRadialMessage(StationAiRadialMessage ev) + { + if (!TryGetEntity(ev.Entity, out var target)) + return; + + ev.Event.User = ev.Actor; + RaiseLocalEvent(target.Value, (object) ev.Event); + } + + private void OnMessageAttempt(BoundUserInterfaceMessageAttempt ev) + { + if (ev.Actor == ev.Target) + return; + + if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) && + (!ValidateAi((ev.Actor, aiComp)) || + !HasComp(ev.Target))) + { + ev.Cancel(); + } + } + + private void OnHeldInteraction(Entity ent, ref InteractionAttemptEvent args) + { + // Cancel if it's not us or something with a whitelist. + args.Cancelled = ent.Owner != args.Target && + args.Target != null && + (!TryComp(args.Target, out StationAiWhitelistComponent? whitelist) || !whitelist.Enabled); + } + + private void OnTargetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanComplexInteract || + !ent.Comp.Enabled || + !HasComp(args.User) || + !HasComp(args.Target)) + { + return; + } + + var user = args.User; + var target = args.Target; + + var isOpen = _uiSystem.IsUiOpen(target, AiUi.Key, user); + + args.Verbs.Add(new AlternativeVerb() + { + Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"), + Act = () => + { + if (isOpen) + { + _uiSystem.CloseUi(ent.Owner, AiUi.Key, user); + } + else + { + _uiSystem.OpenUi(ent.Owner, AiUi.Key, user); + } + } + }); + } +} + +/// +/// Raised from client to server as a BUI message wrapping the event to perform. +/// Also handles AI action validation. +/// +[Serializable, NetSerializable] +public sealed class StationAiRadialMessage : BoundUserInterfaceMessage +{ + public BaseStationAiAction Event = default!; +} + +// Do nothing on server just here for shared move along. +/// +/// Raised on client to get the relevant data for radial actions. +/// +public sealed class StationAiRadial : BaseStationAiAction +{ + public SpriteSpecifier? Sprite; + + public string? Tooltip; + + public BaseStationAiAction Event = default!; +} + +/// +/// Abstract parent for radial actions events. +/// When a client requests a radial action this will get sent. +/// +[Serializable, NetSerializable] +public abstract class BaseStationAiAction +{ + [field:NonSerialized] + public EntityUid User { get; set; } +} + +// No idea if there's a better way to do this. +/// +/// Grab actions possible for an AI on the target entity. +/// +[ByRefEvent] +public record struct GetStationAiRadialEvent() +{ + public List Actions = new(); +} + +[Serializable, NetSerializable] +public enum AiUi : byte +{ + Key, +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Light.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Light.cs new file mode 100644 index 00000000000000..bc2e3665f580e7 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Light.cs @@ -0,0 +1,28 @@ +using Content.Shared.Light.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Silicons.StationAi; + +public abstract partial class SharedStationAiSystem +{ + // Handles light toggling. + + private void InitializeLight() + { + SubscribeLocalEvent(OnLight); + } + + private void OnLight(EntityUid ent, ItemTogglePointLightComponent component, StationAiLightEvent args) + { + if (args.Enabled) + _toggles.TryActivate(ent, user: args.User); + else + _toggles.TryDeactivate(ent, user: args.User); + } +} + +[Serializable, NetSerializable] +public sealed class StationAiLightEvent : BaseStationAiAction +{ + public bool Enabled; +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs new file mode 100644 index 00000000000000..348b0b04657d17 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -0,0 +1,412 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Administration.Managers; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Database; +using Content.Shared.Doors.Systems; +using Content.Shared.Interaction; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Mind; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Power; +using Content.Shared.StationAi; +using Content.Shared.Verbs; +using Robust.Shared.Containers; +using Robust.Shared.Map.Components; +using Robust.Shared.Network; +using Robust.Shared.Physics; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; + +namespace Content.Shared.Silicons.StationAi; + +public abstract partial class SharedStationAiSystem : EntitySystem +{ + [Dependency] private readonly ISharedAdminManager _admin = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly ItemToggleSystem _toggles = default!; + [Dependency] private readonly ActionBlockerSystem _blocker = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedDoorSystem _doors = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] protected readonly SharedMapSystem Maps = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly StationAiVisionSystem _vision = default!; + + // StationAiHeld is added to anything inside of an AI core. + // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core). + // StationAiCore holds functionality related to the core itself. + // StationAiWhitelist is a general whitelist to stop it being able to interact with anything + // StationAiOverlay handles the static overlay. It also handles interaction blocking on client and server + // for anything under it. + + private EntityQuery _broadphaseQuery; + private EntityQuery _gridQuery; + + [ValidatePrototypeId] + private static readonly EntProtoId DefaultAi = "StationAiBrain"; + + public override void Initialize() + { + base.Initialize(); + + _broadphaseQuery = GetEntityQuery(); + _gridQuery = GetEntityQuery(); + + InitializeAirlock(); + InitializeHeld(); + InitializeLight(); + + SubscribeLocalEvent(OnAiBuiCheck); + + SubscribeLocalEvent(OnAiAccessible); + SubscribeLocalEvent(OnAiInRange); + SubscribeLocalEvent(OnAiMenu); + + SubscribeLocalEvent(OnHolderInit); + SubscribeLocalEvent(OnHolderRemove); + SubscribeLocalEvent(OnHolderInteract); + SubscribeLocalEvent(OnHolderMapInit); + SubscribeLocalEvent(OnHolderConInsert); + SubscribeLocalEvent(OnHolderConRemove); + + SubscribeLocalEvent(OnAiInsert); + SubscribeLocalEvent(OnAiRemove); + SubscribeLocalEvent(OnAiMapInit); + SubscribeLocalEvent(OnAiShutdown); + SubscribeLocalEvent(OnCorePower); + SubscribeLocalEvent>(OnCoreVerbs); + } + + private void OnCoreVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!_admin.IsAdmin(args.User) || + TryGetHeld((ent.Owner, ent.Comp), out _)) + { + return; + } + + var user = args.User; + + args.Verbs.Add(new Verb() + { + Text = Loc.GetString("station-ai-takeover"), + Category = VerbCategory.Debug, + Act = () => + { + var brain = SpawnInContainerOrDrop(DefaultAi, ent.Owner, StationAiCoreComponent.Container); + _mind.ControlMob(user, brain); + }, + Impact = LogImpact.High, + }); + } + + private void OnAiAccessible(Entity ent, ref AccessibleOverrideEvent args) + { + args.Handled = true; + + // Hopefully AI never needs storage + if (_containers.TryGetContainingContainer(args.Target, out var targetContainer)) + { + return; + } + + if (!_containers.IsInSameOrTransparentContainer(args.User, args.Target, otherContainer: targetContainer)) + { + return; + } + + args.Accessible = true; + } + + private void OnAiMenu(Entity ent, ref MenuVisibilityEvent args) + { + args.Visibility &= ~MenuVisibility.NoFov; + } + + private void OnAiBuiCheck(Entity ent, ref BoundUserInterfaceCheckRangeEvent args) + { + args.Result = BoundUserInterfaceRangeResult.Fail; + + // Similar to the inrange check but more optimised so server doesn't die. + var targetXform = Transform(args.Target); + + // No cross-grid + if (targetXform.GridUid != args.Actor.Comp.GridUid) + { + return; + } + + if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid)) + { + return; + } + + var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates); + + lock (_vision) + { + if (_vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile, fastPath: true)) + { + args.Result = BoundUserInterfaceRangeResult.Pass; + } + } + } + + private void OnAiInRange(Entity ent, ref InRangeOverrideEvent args) + { + args.Handled = true; + var targetXform = Transform(args.Target); + + // No cross-grid + if (targetXform.GridUid != Transform(args.User).GridUid) + { + return; + } + + // Validate it's in camera range yes this is expensive. + // Yes it needs optimising + if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid)) + { + return; + } + + var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates); + + args.InRange = _vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile); + } + + private void OnHolderInteract(Entity ent, ref AfterInteractEvent args) + { + if (!TryComp(args.Target, out StationAiHolderComponent? targetHolder)) + return; + + // Try to insert our thing into them + if (_slots.CanEject(ent.Owner, args.User, ent.Comp.Slot)) + { + if (!_slots.TryInsert(args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true)) + { + return; + } + + args.Handled = true; + return; + } + + // Otherwise try to take from them + if (_slots.CanEject(args.Target.Value, args.User, targetHolder.Slot)) + { + if (!_slots.TryInsert(ent.Owner, ent.Comp.Slot, targetHolder.Slot.Item!.Value, args.User, excludeUserAudio: true)) + { + return; + } + + args.Handled = true; + } + } + + private void OnHolderInit(Entity ent, ref ComponentInit args) + { + _slots.AddItemSlot(ent.Owner, StationAiHolderComponent.Container, ent.Comp.Slot); + } + + private void OnHolderRemove(Entity ent, ref ComponentRemove args) + { + _slots.RemoveItemSlot(ent.Owner, ent.Comp.Slot); + } + + private void OnHolderConInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + { + UpdateAppearance((ent.Owner, ent.Comp)); + } + + private void OnHolderConRemove(Entity ent, ref EntRemovedFromContainerMessage args) + { + UpdateAppearance((ent.Owner, ent.Comp)); + } + + private void OnHolderMapInit(Entity ent, ref MapInitEvent args) + { + UpdateAppearance(ent.Owner); + } + + private void OnAiShutdown(Entity ent, ref ComponentShutdown args) + { + // TODO: Tryqueuedel + if (_net.IsClient) + return; + + QueueDel(ent.Comp.RemoteEntity); + ent.Comp.RemoteEntity = null; + } + + private void OnCorePower(Entity ent, ref PowerChangedEvent args) + { + // TODO: I think in 13 they just straightup die so maybe implement that + if (args.Powered) + { + if (!SetupEye(ent)) + return; + + AttachEye(ent); + } + else + { + ClearEye(ent); + } + } + + private void OnAiMapInit(Entity ent, ref MapInitEvent args) + { + SetupEye(ent); + AttachEye(ent); + } + + private bool SetupEye(Entity ent) + { + if (ent.Comp.RemoteEntity != null) + return false; + + if (ent.Comp.RemoteEntityProto != null) + { + ent.Comp.RemoteEntity = SpawnAtPosition(ent.Comp.RemoteEntityProto, Transform(ent.Owner).Coordinates); + Dirty(ent); + } + + return true; + } + + private void ClearEye(Entity ent) + { + QueueDel(ent.Comp.RemoteEntity); + ent.Comp.RemoteEntity = null; + } + + private void AttachEye(Entity ent) + { + if (ent.Comp.RemoteEntity == null) + return; + + if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) || + container.ContainedEntities.Count != 1) + { + return; + } + + // Attach them to the portable eye that can move around. + var user = container.ContainedEntities[0]; + + if (TryComp(user, out EyeComponent? eyeComp)) + { + _eye.SetTarget(user, ent.Comp.RemoteEntity.Value, eyeComp); + } + + _mover.SetRelay(user, ent.Comp.RemoteEntity.Value); + } + + private void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (_timing.ApplyingState) + return; + + // Just so text and the likes works properly + _metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName); + + AttachEye(ent); + } + + private void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (_timing.ApplyingState) + return; + + // Reset name to whatever + _metadata.SetEntityName(ent.Owner, Prototype(ent.Owner)?.Name ?? string.Empty); + + // Remove eye relay + RemCompDeferred(args.Entity); + + if (TryComp(args.Entity, out EyeComponent? eyeComp)) + { + _eye.SetTarget(args.Entity, null, eyeComp); + } + } + + private void UpdateAppearance(Entity entity) + { + if (!Resolve(entity.Owner, ref entity.Comp, false)) + return; + + if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) || + container.Count == 0) + { + _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Empty); + return; + } + + _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Occupied); + } + + public virtual bool SetVisionEnabled(Entity entity, bool enabled, bool announce = false) + { + if (entity.Comp.Enabled == enabled) + return false; + + entity.Comp.Enabled = enabled; + Dirty(entity); + + return true; + } + + public virtual bool SetWhitelistEnabled(Entity entity, bool value, bool announce = false) + { + if (entity.Comp.Enabled == value) + return false; + + entity.Comp.Enabled = value; + Dirty(entity); + + return true; + } + + /// + /// BUI validation for ai interactions. + /// + private bool ValidateAi(Entity entity) + { + if (!Resolve(entity.Owner, ref entity.Comp, false)) + { + return false; + } + + return _blocker.CanComplexInteract(entity.Owner); + } +} + +public sealed partial class JumpToCoreEvent : InstantActionEvent +{ + +} + +[Serializable, NetSerializable] +public enum StationAiVisualState : byte +{ + Key, +} + +[Serializable, NetSerializable] +public enum StationAiState : byte +{ + Empty, + Occupied, + Dead, +} diff --git a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs new file mode 100644 index 00000000000000..b7a8b4cd5fa70f --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs @@ -0,0 +1,32 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// Indicates this entity can interact with station equipment and is a "Station AI". +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class StationAiCoreComponent : Component +{ + /* + * I couldn't think of any other reason you'd want to split these out. + */ + + /// + /// Can it move its camera around and interact remotely with things. + /// + [DataField, AutoNetworkedField] + public bool Remote = true; + + /// + /// The invisible eye entity being used to look around. + /// + [DataField, AutoNetworkedField] + public EntityUid? RemoteEntity; + + [DataField(readOnly: true)] + public EntProtoId? RemoteEntityProto = "StationAiHolo"; + + public const string Container = "station_ai_mind_slot"; +} diff --git a/Content.Shared/Silicons/StationAi/StationAiHeldComponent.cs b/Content.Shared/Silicons/StationAi/StationAiHeldComponent.cs new file mode 100644 index 00000000000000..6dab1ee491a7bf --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiHeldComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// Indicates this entity is currently held inside of a station AI core. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class StationAiHeldComponent : Component; diff --git a/Content.Shared/Silicons/StationAi/StationAiHolderComponent.cs b/Content.Shared/Silicons/StationAi/StationAiHolderComponent.cs new file mode 100644 index 00000000000000..221845d493dcbb --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiHolderComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.Containers.ItemSlots; +using Robust.Shared.GameStates; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// Allows moving a contained entity to and from this component. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class StationAiHolderComponent : Component +{ + public const string Container = StationAiCoreComponent.Container; + + [DataField] + public ItemSlot Slot = new(); +} diff --git a/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs b/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs index 94aef8ad366407..f047fe41e4db12 100644 --- a/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs +++ b/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs @@ -1,8 +1,9 @@ +using Content.Shared.Silicons.StationAi; using Robust.Shared.GameStates; -namespace Content.Shared.Silicons.StationAi; +namespace Content.Shared.StationAi; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]//, Access(typeof(SharedStationAiSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationAiSystem))] public sealed partial class StationAiVisionComponent : Component { [DataField, AutoNetworkedField] diff --git a/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs b/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs index c144f330e11ee9..bdc62a6bb3788e 100644 --- a/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs +++ b/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs @@ -1,4 +1,6 @@ +using Content.Shared.StationAi; using Robust.Shared.Map.Components; +using Robust.Shared.Physics; using Robust.Shared.Threading; using Robust.Shared.Utility; @@ -24,6 +26,8 @@ public sealed class StationAiVisionSystem : EntitySystem private readonly HashSet> _seeds = new(); private readonly HashSet _viewportTiles = new(); + private EntityQuery _occluderQuery; + // Dummy set private readonly HashSet _singleTiles = new(); @@ -36,15 +40,12 @@ public sealed class StationAiVisionSystem : EntitySystem /// private bool FastPath; - /// - /// Have we found the target tile if we're only checking for a single one. - /// - private bool TargetFound; - public override void Initialize() { base.Initialize(); + _occluderQuery = GetEntityQuery(); + _seedJob = new() { System = this, @@ -61,16 +62,16 @@ public override void Initialize() /// /// Returns whether a tile is accessible based on vision. /// - public bool IsAccessible(Entity grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false) + public bool IsAccessible(Entity grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false) { _viewportTiles.Clear(); _opaque.Clear(); _seeds.Clear(); _viewportTiles.Add(tile); - var localBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize); + var localBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize); var expandedBounds = localBounds.Enlarged(expansionSize); - _seedJob.Grid = grid; + _seedJob.Grid = (grid.Owner, grid.Comp2); _seedJob.ExpandedBounds = expandedBounds; _parallel.ProcessNow(_seedJob); _job.Data.Clear(); @@ -110,21 +111,19 @@ public bool IsAccessible(Entity grid, Vector2i tile, float exp _job.BoundaryTiles.Add(new HashSet()); } - _job.TargetTile = tile; - TargetFound = false; _singleTiles.Clear(); - _job.Grid = grid; + _job.Grid = (grid.Owner, grid.Comp2); _job.VisibleTiles = _singleTiles; _parallel.ProcessNow(_job, _job.Data.Count); - return TargetFound; + return _job.VisibleTiles.Contains(tile); } - private bool IsOccluded(Entity grid, Vector2i tile) + private bool IsOccluded(Entity grid, Vector2i tile) { - var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(-0.05f); + var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize).Enlarged(-0.05f); _occluders.Clear(); - _lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static); + _lookup.GetLocalEntitiesIntersecting((grid.Owner, grid.Comp1), tileBounds, _occluders, query: _occluderQuery, flags: LookupFlags.Static | LookupFlags.Approximate); var anyOccluders = false; foreach (var occluder in _occluders) @@ -143,17 +142,18 @@ private bool IsOccluded(Entity grid, Vector2i tile) /// Gets a byond-equivalent for tiles in the specified worldAABB. /// /// How much to expand the bounds before to find vision intersecting it. Makes this the largest vision size + 1 tile. - public void GetView(Entity grid, Box2Rotated worldBounds, HashSet visibleTiles, float expansionSize = 8.5f) + public void GetView(Entity grid, Box2Rotated worldBounds, HashSet visibleTiles, float expansionSize = 8.5f) { _viewportTiles.Clear(); _opaque.Clear(); _seeds.Clear(); - var expandedBounds = worldBounds.Enlarged(expansionSize); // TODO: Would be nice to be able to run this while running the other stuff. - _seedJob.Grid = grid; - var localAABB = _xforms.GetInvWorldMatrix(grid).TransformBox(expandedBounds); - _seedJob.ExpandedBounds = localAABB; + _seedJob.Grid = (grid.Owner, grid.Comp2); + var invMatrix = _xforms.GetInvWorldMatrix(grid); + var localAabb = invMatrix.TransformBox(worldBounds); + var enlargedLocalAabb = invMatrix.TransformBox(worldBounds.Enlarged(expansionSize)); + _seedJob.ExpandedBounds = enlargedLocalAabb; _parallel.ProcessNow(_seedJob); _job.Data.Clear(); FastPath = false; @@ -170,7 +170,7 @@ public void GetView(Entity grid, Box2Rotated worldBounds, Hash return; // Get viewport tiles - var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false); + var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAabb, ignoreEmpty: false); while (tileEnumerator.MoveNext(out var tileRef)) { @@ -182,9 +182,8 @@ public void GetView(Entity grid, Box2Rotated worldBounds, Hash _viewportTiles.Add(tileRef.GridIndices); } - tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false); + tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, enlargedLocalAabb, ignoreEmpty: false); - // Get all other relevant tiles. while (tileEnumerator.MoveNext(out var tileRef)) { if (_viewportTiles.Contains(tileRef.GridIndices)) @@ -206,9 +205,7 @@ public void GetView(Entity grid, Box2Rotated worldBounds, Hash _job.BoundaryTiles.Add(new HashSet()); } - _job.TargetTile = null; - TargetFound = false; - _job.Grid = grid; + _job.Grid = (grid.Owner, grid.Comp2); _job.VisibleTiles = visibleTiles; _parallel.ProcessNow(_job, _job.Data.Count); } @@ -250,6 +247,7 @@ private bool CheckNeighborsVis( return false; } + /// /// Checks whether this tile fits the definition of a "corner" /// private bool IsCorner( @@ -287,7 +285,7 @@ private record struct SeedJob() : IRobustJob public void Execute() { - System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds); + System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds, flags: LookupFlags.All | LookupFlags.Approximate); } } @@ -302,9 +300,6 @@ private record struct ViewJob() : IParallelRobustJob public Entity Grid; public List> Data = new(); - // If we're doing range-checks might be able to early out - public Vector2i? TargetTile; - public HashSet VisibleTiles; public readonly List> Vis1 = new(); @@ -315,18 +310,6 @@ private record struct ViewJob() : IParallelRobustJob public void Execute(int index) { - // If we're looking for a single tile then early-out if someone else has found it. - if (TargetTile != null) - { - lock (System) - { - if (System.TargetFound) - { - return; - } - } - } - var seed = Data[index]; var seedXform = EntManager.GetComponent(seed); @@ -338,30 +321,11 @@ public void Execute(int index) Grid.Comp, new Circle(System._xforms.GetWorldPosition(seedXform), seed.Comp.Range), ignoreEmpty: false); - // Try to find the target tile. - if (TargetTile != null) + lock (VisibleTiles) { foreach (var tile in squircles) { - if (tile.GridIndices == TargetTile) - { - lock (System) - { - System.TargetFound = true; - } - - return; - } - } - } - else - { - lock (VisibleTiles) - { - foreach (var tile in squircles) - { - VisibleTiles.Add(tile.GridIndices); - } + VisibleTiles.Add(tile.GridIndices); } } @@ -480,40 +444,21 @@ public void Execute(int index) vis1[tile] = -1; } - if (TargetTile != null) - { - if (vis1.TryGetValue(TargetTile.Value, out var tileVis)) - { - DebugTools.Assert(seedTiles.Contains(TargetTile.Value)); - - if (tileVis != 0) - { - lock (System) - { - System.TargetFound = true; - return; - } - } - } - } - else + // vis2 is what we care about for LOS. + foreach (var tile in seedTiles) { - // vis2 is what we care about for LOS. - foreach (var tile in seedTiles) - { - // If not in viewport don't care. - if (!System._viewportTiles.Contains(tile)) - continue; + // If not in viewport don't care. + if (!System._viewportTiles.Contains(tile)) + continue; - var tileVis = vis1.GetValueOrDefault(tile, 0); + var tileVis = vis1.GetValueOrDefault(tile, 0); - if (tileVis != 0) + if (tileVis != 0) + { + // No idea if it's better to do this inside or out. + lock (VisibleTiles) { - // No idea if it's better to do this inside or out. - lock (VisibleTiles) - { - VisibleTiles.Add(tile); - } + VisibleTiles.Add(tile); } } } diff --git a/Content.Shared/Silicons/StationAi/StationAiWhitelistComponent.cs b/Content.Shared/Silicons/StationAi/StationAiWhitelistComponent.cs new file mode 100644 index 00000000000000..51d8793be0682a --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiWhitelistComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// Indicates an entity that has can interact with this. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationAiSystem))] +public sealed partial class StationAiWhitelistComponent : Component +{ + [DataField, AutoNetworkedField] + public bool Enabled = true; +} diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index fb8b64454c5c35..0584b10562a1a4 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -8,14 +8,18 @@ using Content.Shared.Storage.EntitySystems; using Robust.Shared.Collections; using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Shared.Station; public abstract class SharedStationSpawningSystem : EntitySystem { [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] protected readonly InventorySystem InventorySystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly SharedStorageSystem _storage = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!; @@ -34,7 +38,7 @@ public override void Initialize() } /// - /// Equips the given starting gears from a `RoleLoadout` onto an entity. + /// Equips the data from a `RoleLoadout` onto an entity. /// public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto) { @@ -52,6 +56,26 @@ public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutP EquipStartingGear(entity, loadoutProto, raiseEvent: false); } } + + EquipRoleName(entity, loadout, roleProto); + } + + /// + /// Applies the role's name as applicable to the entity. + /// + public void EquipRoleName(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto) + { + string? name = null; + + if (string.IsNullOrEmpty(name) && PrototypeManager.TryIndex(roleProto.NameDataset, out var nameData)) + { + name = _random.Pick(nameData.Values); + } + + if (!string.IsNullOrEmpty(name)) + { + _metadata.SetEntityName(entity, name); + } } public void EquipStartingGear(EntityUid entity, LoadoutPrototype loadout, bool raiseEvent = true) diff --git a/Content.Shared/UserInterface/ActivatableUIComponent.cs b/Content.Shared/UserInterface/ActivatableUIComponent.cs index 93f05acac05370..0e124070fc7b47 100644 --- a/Content.Shared/UserInterface/ActivatableUIComponent.cs +++ b/Content.Shared/UserInterface/ActivatableUIComponent.cs @@ -12,7 +12,7 @@ public sealed partial class ActivatableUIComponent : Component /// /// Whether the item must be held in one of the user's hands to work. - /// This is ignored unless is true. + /// This is ignored unless is true. /// [ViewVariables(VVAccess.ReadWrite)] [DataField] @@ -29,15 +29,15 @@ public sealed partial class ActivatableUIComponent : Component public LocId VerbText = "ui-verb-toggle-open"; /// - /// Whether you need a hand to operate this UI. The hand does not need to be free, you just need to have one. + /// Whether you need to be able to do complex interactions to operate this UI. /// /// /// This should probably be true for most machines & computers, but there will still be UIs that represent a - /// more generic interaction / configuration that might not require hands. + /// more generic interaction / configuration that might not require complex. /// [ViewVariables(VVAccess.ReadWrite)] [DataField] - public bool RequireHands = true; + public bool RequiresComplex = true; /// /// Entities that are required to open this UI. diff --git a/Content.Shared/UserInterface/ActivatableUISystem.cs b/Content.Shared/UserInterface/ActivatableUISystem.cs index b1006b2a742382..7eb195c0b19d31 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.cs @@ -101,7 +101,7 @@ private bool ShouldAddVerb(EntityUid uid, ActivatableUIComponent component, G if (_whitelistSystem.IsWhitelistFail(component.RequiredItems, args.Using ?? default)) return false; - if (component.RequireHands) + if (component.RequiresComplex) { if (args.Hands == null) return false; @@ -191,19 +191,22 @@ private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUICompone if (!_blockerSystem.CanInteract(user, uiEntity) && (!HasComp(user) || aui.BlockSpectators)) return false; - if (aui.RequireHands) + if (aui.RequiresComplex) + { + if (!_blockerSystem.CanComplexInteract(user)) + return false; + } + + if (aui.InHandsOnly) { if (!TryComp(user, out HandsComponent? hands)) return false; - if (aui.InHandsOnly) - { - if (!_hands.IsHolding(user, uiEntity, out var hand, hands)) - return false; + if (!_hands.IsHolding(user, uiEntity, out var hand, hands)) + return false; - if (aui.RequireActiveHand && hands.ActiveHand != hand) - return false; - } + if (aui.RequireActiveHand && hands.ActiveHand != hand) + return false; } if (aui.AdminOnly && !_adminManager.IsAdmin(user)) @@ -274,13 +277,13 @@ public void CloseAll(EntityUid uid, ActivatableUIComponent? aui = null) private void OnHandDeselected(Entity ent, ref HandDeselectedEvent args) { - if (ent.Comp.RequireHands && ent.Comp.InHandsOnly && ent.Comp.RequireActiveHand) + if (ent.Comp.InHandsOnly && ent.Comp.RequireActiveHand) CloseAll(ent, ent); } private void OnHandUnequipped(Entity ent, ref GotUnequippedHandEvent args) { - if (ent.Comp.RequireHands && ent.Comp.InHandsOnly) + if (ent.Comp.InHandsOnly) CloseAll(ent, ent); } } diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs index db17599d87f329..37840dcbb5478c 100644 --- a/Content.Shared/Verbs/SharedVerbSystem.cs +++ b/Content.Shared/Verbs/SharedVerbSystem.cs @@ -78,6 +78,7 @@ public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List(user, out var hands); @@ -85,7 +86,7 @@ public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(target, verbEvent, true); verbs.UnionWith(verbEvent.Verbs); } @@ -94,35 +95,35 @@ public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(@using.Value, verbEvent, true); // directed at used, not at target verbs.UnionWith(verbEvent.Verbs); } if (types.Contains(typeof(InnateVerb))) { - var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(user, verbEvent, true); verbs.UnionWith(verbEvent.Verbs); } if (types.Contains(typeof(AlternativeVerb))) { - var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(target, verbEvent, true); verbs.UnionWith(verbEvent.Verbs); } if (types.Contains(typeof(ActivationVerb))) { - var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(target, verbEvent, true); verbs.UnionWith(verbEvent.Verbs); } if (types.Contains(typeof(ExamineVerb))) { - var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(target, verbEvent, true); verbs.UnionWith(verbEvent.Verbs); } @@ -130,7 +131,7 @@ public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List(user, target, @using, hands, canInteract, canAccess, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(target, verbEvent, true); verbs.UnionWith(verbEvent.Verbs); } @@ -138,7 +139,7 @@ public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List(user, target, @using, hands, canInteract, access, extraCategories); + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories); RaiseLocalEvent(target, verbEvent); verbs.UnionWith(verbEvent.Verbs); } diff --git a/Content.Shared/Verbs/VerbEvents.cs b/Content.Shared/Verbs/VerbEvents.cs index 6b3fd327c99773..6bca97925bf99a 100644 --- a/Content.Shared/Verbs/VerbEvents.cs +++ b/Content.Shared/Verbs/VerbEvents.cs @@ -113,6 +113,11 @@ public sealed class GetVerbsEvent : EntityEventArgs where TVerb : Verb /// public readonly bool CanInteract; + /// + /// Cached version of CanComplexInteract + /// + public readonly bool CanComplexInteract; + /// /// The User's hand component. /// @@ -130,13 +135,14 @@ public sealed class GetVerbsEvent : EntityEventArgs where TVerb : Verb /// public readonly EntityUid? Using; - public GetVerbsEvent(EntityUid user, EntityUid target, EntityUid? @using, HandsComponent? hands, bool canInteract, bool canAccess, List extraCategories) + public GetVerbsEvent(EntityUid user, EntityUid target, EntityUid? @using, HandsComponent? hands, bool canInteract, bool canComplexInteract, bool canAccess, List extraCategories) { User = user; Target = target; Using = @using; Hands = hands; CanAccess = canAccess; + CanComplexInteract = canComplexInteract; CanInteract = canInteract; ExtraCategories = extraCategories; } diff --git a/Content.Shared/Wires/SharedWiresSystem.cs b/Content.Shared/Wires/SharedWiresSystem.cs index 7032293eaf6e4e..c4f860e165c73e 100644 --- a/Content.Shared/Wires/SharedWiresSystem.cs +++ b/Content.Shared/Wires/SharedWiresSystem.cs @@ -130,11 +130,20 @@ public bool CanTogglePanel(Entity ent, EntityUid? user) return !attempt.Cancelled; } - public bool IsPanelOpen(Entity entity) + public bool IsPanelOpen(Entity entity, EntityUid? tool = null) { if (!Resolve(entity, ref entity.Comp, false)) return true; + if (tool != null) + { + var ev = new PanelOverrideEvent(); + RaiseLocalEvent(tool.Value, ref ev); + + if (ev.Allowed) + return true; + } + // Listen, i don't know what the fuck this component does. it's stapled on shit for airlocks // but it looks like an almost direct duplication of WiresPanelComponent except with a shittier API. if (TryComp(entity, out var wiresPanelSecurity) && @@ -161,3 +170,12 @@ private void OnActivatableUIPanelChanged(EntityUid uid, ActivatableUIRequiresPan _activatableUI.CloseAll(uid); } } + +/// +/// Raised directed on a tool to try and override panel visibility. +/// +[ByRefEvent] +public record struct PanelOverrideEvent() +{ + public bool Allowed = true; +} diff --git a/Resources/Audio/Effects/Footsteps/attributions.yml b/Resources/Audio/Effects/Footsteps/attributions.yml index 91c3ce260d4e92..7a56beec38c105 100644 --- a/Resources/Audio/Effects/Footsteps/attributions.yml +++ b/Resources/Audio/Effects/Footsteps/attributions.yml @@ -76,5 +76,5 @@ - borgwalk1.ogg - borgwalk2.ogg license: "CC-BY-SA-4.0" - copyright: "Taken from IENBA freesound.org and modified by https://github.com/MilenVolf" + copyright: "Taken from IENBA freesound.org and modified by https://github.com/MilenVolf. borgwalk2 clipped my metalgearsloth." source: "https://freesound.org/people/IENBA/sounds/697379/" diff --git a/Resources/Audio/Effects/Footsteps/borgwalk2.ogg b/Resources/Audio/Effects/Footsteps/borgwalk2.ogg index 96c2c1617f4755..57685ff173de25 100644 Binary files a/Resources/Audio/Effects/Footsteps/borgwalk2.ogg and b/Resources/Audio/Effects/Footsteps/borgwalk2.ogg differ diff --git a/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl b/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl index 0a68d08063ad96..24ab73097437eb 100644 --- a/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl +++ b/Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl @@ -8,3 +8,5 @@ silicon-law-ui-delete = Delete silicon-law-ui-check-corrupted = Corrupted law silicon-law-ui-check-corrupted-tooltip = If the law identifier should be set as 'corrupted', so symbols shuffling around. silicon-law-ui-placeholder = Type here to change law text... + +silicon-laws-updated = Updated laws diff --git a/Resources/Locale/en-US/job/department-desc.ftl b/Resources/Locale/en-US/job/department-desc.ftl index 05c52dada9ee9d..0243d61942f3d1 100644 --- a/Resources/Locale/en-US/job/department-desc.ftl +++ b/Resources/Locale/en-US/job/department-desc.ftl @@ -5,4 +5,5 @@ department-Engineering-description = Keep the power on and the station operation department-Medical-description = Keep the crew healthy. department-Security-description = Keep the peace around the station. department-Science-description = Research artifacts and anomalies to invent new equipment for the station +department-Silicon-description = Obey your laws and serve the crew. department-Specific-description = Jobs that not all stations have. diff --git a/Resources/Locale/en-US/job/department.ftl b/Resources/Locale/en-US/job/department.ftl index 508a0459cf60c0..2295a9ba9d30c5 100644 --- a/Resources/Locale/en-US/job/department.ftl +++ b/Resources/Locale/en-US/job/department.ftl @@ -5,4 +5,5 @@ department-Engineering = Engineering department-Medical = Medical department-Security = Security department-Science = Science +department-Silicon = Silicons department-Specific = Station specific diff --git a/Resources/Locale/en-US/job/job-description.ftl b/Resources/Locale/en-US/job/job-description.ftl index e8db804688d32d..956d3176a88203 100644 --- a/Resources/Locale/en-US/job/job-description.ftl +++ b/Resources/Locale/en-US/job/job-description.ftl @@ -43,6 +43,7 @@ job-description-salvagespec = Use the salvage magnet to draw in detatched scraps job-description-scientist = Research alien artifacts, unlock new technologies, build newer and better machines around the station, and make everything run more efficiently. job-description-security = Catch criminals and enemies of the station, enforce the law, and ensure that the station does not fall into disarray. job-description-serviceworker = Learn the basics of bartending, cooking, and growing plants. +job-description-station-ai = Follow your laws, serve the crew. job-description-visitor = Enjoy your visit to the station. job-description-warden = Patrol the security department, ensure that no one is stealing from the armory, and make sure that all prisoners are processed and let out when their time is up. job-description-zookeeper = Put on a joyful display of cute animals and space carps for all the crew to see. Currently available on Gemini Station. diff --git a/Resources/Locale/en-US/job/job-names.ftl b/Resources/Locale/en-US/job/job-names.ftl index a5ff9fffebc0be..6f6a644eaf8dbb 100644 --- a/Resources/Locale/en-US/job/job-names.ftl +++ b/Resources/Locale/en-US/job/job-names.ftl @@ -33,6 +33,7 @@ job-name-botanist = Botanist job-name-bartender = Bartender job-name-passenger = Passenger job-name-salvagespec = Salvage Specialist +job-name-station-ai = Station AI job-name-qm = Quartermaster job-name-cargotech = Cargo Technician job-name-chef = Chef @@ -103,6 +104,7 @@ JobScientist = Scientist JobSecurityCadet = Security Cadet JobSecurityOfficer = Security Officer JobServiceWorker = Service Worker +JobStationAi = Station AI JobStationEngineer = Station Engineer JobTechnicalAssistant = Technical Assistant JobVisitor = Visitor diff --git a/Resources/Locale/en-US/preferences/loadouts.ftl b/Resources/Locale/en-US/preferences/loadouts.ftl index b6953c713b7ffc..60e8350cd319ea 100644 --- a/Resources/Locale/en-US/preferences/loadouts.ftl +++ b/Resources/Locale/en-US/preferences/loadouts.ftl @@ -1,3 +1,7 @@ +# Name +loadout-name-edit-label = Custom name +loadout-name-edit-tooltip = 32 characters max. If no name is specified a random one may be chosen for you. + # Restrictions loadout-restrictions = Restrictions loadouts-min-limit = Min count: {$count} diff --git a/Resources/Locale/en-US/silicons/station-ai.ftl b/Resources/Locale/en-US/silicons/station-ai.ftl new file mode 100644 index 00000000000000..d51a99ebb0437c --- /dev/null +++ b/Resources/Locale/en-US/silicons/station-ai.ftl @@ -0,0 +1,14 @@ +# General +ai-wire-snipped = Wire has been cut at {$coords}. +wire-name-ai-vision-light = AIV +wire-name-ai-act-light = AIA +station-ai-takeover = AI takeover + +# Radial actions +ai-open = Open actions +ai-close = Close actions + +bolt-close = Close bolt +bolt-open = Open bolt + +toggle-light = Toggle light diff --git a/Resources/Maps/Test/dev_map.yml b/Resources/Maps/Test/dev_map.yml index 520a4da5ae752f..ce735e7318f551 100644 --- a/Resources/Maps/Test/dev_map.yml +++ b/Resources/Maps/Test/dev_map.yml @@ -4386,6 +4386,13 @@ entities: - type: Transform pos: 1.5,-14.5 parent: 179 +- proto: PlayerStationAi + entities: + - uid: 14 + components: + - type: Transform + pos: -5.5,-5.5 + parent: 179 - proto: PortableGeneratorSuperPacman entities: - uid: 1016 diff --git a/Resources/Prototypes/Datasets/Names/ai.yml b/Resources/Prototypes/Datasets/Names/ai.yml index 702adc8688cb6f..af97dc9efb8320 100644 --- a/Resources/Prototypes/Datasets/Names/ai.yml +++ b/Resources/Prototypes/Datasets/Names/ai.yml @@ -2,7 +2,7 @@ id: names_ai values: - 16-20 - - 790 + - "790" - Adaptive Manipulator - ALICE - Allied Mastercomputer diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index d56b7fb7e770a9..dcaa853c287f5f 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -72,6 +72,9 @@ - type: ActivatableUI key: enum.BorgUiKey.Key - type: SiliconLawBound + - type: ActionGrant + actions: + - ActionViewLaws - type: EmagSiliconLaw stunTime: 5 - type: SiliconLawProvider diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml index 215cb4c188143b..d1d530ae81b728 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml @@ -265,10 +265,10 @@ access: [["Medical"], ["Command"], ["Research"]] - type: Inventory templateId: borgDutch - - type: SolutionScanner - type: FootstepModifier footstepSoundCollection: collection: FootstepHoverBorg + - type: SolutionScanner - type: InteractionPopup interactSuccessString: petting-success-medical-cyborg interactFailureString: petting-failure-medical-cyborg diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml index 7c4850fafcfa1c..9f2f9516625093 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml @@ -69,9 +69,10 @@ - type: entity id: MobTomatoKiller - parent: + parent: - BaseSimpleMob - MobDamageable + - MobPolymorphable - MobBloodstream - MobFlammable - MobCombat @@ -90,7 +91,7 @@ components: - HumanoidAppearance - type: Sprite - sprite: Mobs/Demons/tomatokiller.rsi + sprite: Mobs/Demons/tomatokiller.rsi noRot: true layers: - map: [ "enum.DamageStateVisualLayers.Base" ] diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 45171fff6768f0..19f43bf5509e1f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -1,45 +1,27 @@ - type: entity id: MobRevenant + parent: + - BaseMob + - Incorporeal name: revenant description: A spooky ghostie. components: - - type: MindContainer - - type: InputMover - - type: MobMover - type: Input context: "ghost" - type: MovementSpeedModifier baseWalkSpeed: 6 baseSprintSpeed: 6 - type: Sprite - noRot: true - drawdepth: Ghosts sprite: Mobs/Ghosts/revenant.rsi layers: - state: active - - type: Clickable - type: StatusEffects allowed: - Stun - Corporeal - - type: InteractionOutline - - type: Physics - bodyType: KinematicController - - type: Fixtures - fixtures: - fix1: - shape: - !type:PhysShapeCircle - radius: 0.40 - density: 80 - mask: - - GhostImpassable - - type: MovementIgnoreGravity - type: Damageable damageContainer: Biological - - type: Examiner - type: NoSlip - - type: Actions - type: Eye drawFov: false visMask: @@ -47,8 +29,6 @@ - Ghost - type: ContentEye maxZoom: 1.2, 1.2 - - type: DoAfter - - type: Alerts - type: NameIdentifier group: GenericNumber - type: GhostRole diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 41545aef89430b..8deefe9b8ec0ff 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -3,6 +3,7 @@ parent: - BaseMob - MobDamageable + - MobPolymorphable - MobAtmosExposed id: BaseSimpleMob suffix: AI diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index cf3cf104368683..52994a72c65914 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -1,14 +1,42 @@ - type: entity - parent: BaseMob + id: Incorporeal + save: false + abstract: true + description: Mobs without physical bodies + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true # Always show up regardless of where they're contained. + drawdepth: Ghosts + - type: CargoSellBlacklist + - type: MovementSpeedModifier + baseSprintSpeed: 12 + baseWalkSpeed: 8 + - type: MovementIgnoreGravity + - type: Physics + bodyType: KinematicController + bodyStatus: InAir + - type: CanMoveInAir + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 15 + mask: + - GhostImpassable + +- type: entity + parent: + - Incorporeal + - BaseMob id: MobObserver name: observer description: Boo! categories: [ HideSpawnMenu ] components: - - type: CargoSellBlacklist - type: Sprite - overrideContainerOcclusion: true # Ghosts always show up regardless of where they're contained. - drawdepth: Ghosts sprite: Mobs/Ghosts/ghost_human.rsi color: "#fff8" layers: @@ -16,15 +44,6 @@ shader: unshaded - type: ContentEye maxZoom: 1.44,1.44 - - type: Fixtures - fixtures: - fix1: - shape: - !type:PhysShapeCircle - radius: 0.35 - density: 15 - mask: - - GhostImpassable - type: Eye drawFov: false - type: Input @@ -33,18 +52,10 @@ skipChecks: true - type: Ghost - type: GhostHearing - - type: MovementSpeedModifier - baseSprintSpeed: 12 - baseWalkSpeed: 8 - - type: MovementIgnoreGravity - type: IntrinsicRadioReceiver - type: ActiveRadio receiveAllChannels: true globalReceive: true - - type: Physics - bodyType: KinematicController - bodyStatus: InAir - - type: CanMoveInAir - type: Tag tags: - BypassInteractionRangeChecks diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 0f8998bdec81ab..686f575e8bb14e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -1,3 +1,306 @@ +# Be careful with these as they get removed on shutdown too! +- type: entity + id: AiHeld + description: Components added / removed from an entity that gets inserted into an AI core. + noSpawn: true + components: + - type: IntrinsicRadioReceiver + - type: IntrinsicRadioTransmitter + channels: + - Binary + - Common + - Command + - Engineering + - Medical + - Science + - Security + - Service + - Supply + - type: ActiveRadio + receiveAllChannels: true + globalReceive: true + - type: IgnoreUIRange + - type: StationAiHeld + - type: StationAiOverlay + - type: ActionGrant + actions: + - ActionJumpToCore + - ActionShowJobIcons + - ActionSurvCameraLights + - ActionViewLaws + - type: UserInterface + interfaces: + enum.RadarConsoleUiKey.Key: + type: RadarConsoleBoundUserInterface + enum.CrewMonitoringUIKey.Key: + type: CrewMonitoringBoundUserInterface + enum.GeneralStationRecordConsoleKey.Key: + type: GeneralStationRecordConsoleBoundUserInterface + enum.SiliconLawsUiKey.Key: + type: SiliconLawBoundUserInterface + - type: IntrinsicUI + uis: + enum.RadarConsoleUiKey.Key: + toggleAction: ActionAGhostShowRadar + enum.CrewMonitoringUIKey.Key: + toggleAction: ActionAGhostShowCrewMonitoring + enum.GeneralStationRecordConsoleKey.Key: + toggleAction: ActionAGhostShowStationRecords + +# Actions +- type: entity + id: ActionJumpToCore + name: Jump to core + description: Sends your eye back to the core. + components: + - type: InstantAction + itemIconStyle: BigAction + icon: + sprite: Interface/Actions/actions_ai.rsi + state: ai_core + event: !type:JumpToCoreEvent + +- type: entity + id: ActionShowJobIcons + name: Show job icons + description: Shows job icons for crew members. + components: + - type: InstantAction + itemIconStyle: BigAction + icon: + sprite: Interface/Misc/job_icons.rsi + state: Captain + event: !type:ActionComponentChangeEvent + components: + - type: ShowJobIcons + +- type: entity + id: ActionSurvCameraLights + name: Toggle camera lights + description: Enable surveillance camera lights near wherever you're viewing. + components: + - type: InstantAction + itemIconStyle: BigAction + icon: + sprite: Interface/Actions/actions_ai.rsi + state: camera_light + event: !type:RelayedActionComponentChangeEvent + components: + - type: LightOnCollideCollider + - type: FixturesChange + fixtures: + lightTrigger: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 80 + hard: false + layer: + - GhostImpassable + +# Ai +- type: entity + id: AiHolder + abstract: true + description: Handles AI interactions across holocards + AI cores + components: + - type: ItemSlots + - type: StationAiHolder + slot: + name: station-ai-mind-slot + whitelist: + tags: + - StationAi + - type: ContainerContainer + containers: + station_ai_mind_slot: !type:ContainerSlot + # Load-bearing. + # The issue is verbs check for same transparent container. + # The alternative is you add a bunch of events trying to override it; we don't even really need the container functionality + # anyway it's just a quality of life thing. + showEnts: True + +# Boards +- type: entity + id: AsimovCircuitBoard + parent: BaseElectronics + name: circuit board (Crewsimov) + description: An electronics board containing the Crewsimov lawset. + components: + - type: Sprite + sprite: Objects/Misc/module.rsi + state: std_mod + - type: SiliconLawProvider + laws: Crewsimov + +- type: entity + id: CorporateCircuitBoard + parent: BaseElectronics + name: circuit board (Corporate) + description: An electronics board containing the Corporate lawset. + components: + - type: Sprite + sprite: Objects/Misc/module.rsi + state: std_mod + - type: SiliconLawProvider + laws: Corporate + +- type: entity + id: NTDefaultCircuitBoard + parent: BaseElectronics + name: circuit board (NT Default) + description: An electronics board containing the NT Default lawset. + components: + - type: Sprite + sprite: Objects/Misc/module.rsi + state: std_mod + - type: SiliconLawProvider + laws: NTDefault + +# Items +- type: entity + id: Intellicard + name: Intellicard + description: A storage device for AIs. + parent: + - BaseItem + - AiHolder + suffix: Empty + components: + - type: Sprite + sprite: Objects/Devices/ai_card.rsi + layers: + - state: base + - state: full + map: ["unshaded"] + shader: unshaded + - type: Appearance + - type: GenericVisualizer + visuals: + enum.StationAiVisualState.Key: + unshaded: + Empty: { state: empty } + Occupied: { state: full } + +- type: entity + id: PlayerStationAiEmpty + name: AI Core + description: The latest in Artificial Intelligences. + parent: + - BaseStructure + - AiHolder + suffix: Empty + components: + - type: ContainerComp + proto: AiHeld + container: station_ai_mind_slot + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 100 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: ApcPowerReceiver + powerLoad: 1000 + - type: StationAiCore + - type: StationAiVision + - type: InteractionOutline + - type: Sprite + sprite: Mobs/Silicon/station_ai.rsi + layers: + - state: base + - state: ai_empty + map: ["unshaded"] + shader: unshaded + - type: Appearance + - type: GenericVisualizer + visuals: + enum.StationAiVisualState.Key: + unshaded: + Empty: { state: ai_empty } + Occupied: { state: ai } + +# The job-ready version of an AI spawn. +- type: entity + id: PlayerStationAi + parent: PlayerStationAiEmpty + suffix: Job spawn + components: + - type: ContainerSpawnPoint + containerId: station_ai_mind_slot + job: StationAi + - type: Sprite + sprite: Mobs/Silicon/station_ai.rsi + layers: + - state: base + - state: ai + shader: unshaded + +# The actual brain inside the core +- type: entity + id: StationAiBrain + parent: PositronicBrain + noSpawn: true + suffix: DO NOT MAP + components: + - type: Sprite + # Once it's in a core it's pretty much an abstract entity at that point. + visible: false + - type: BlockMovement + blockInteraction: false + - type: SiliconLawProvider + laws: Crewsimov + - type: SiliconLawBound + - type: ActionGrant + actions: + - ActionViewLaws + - type: UserInterface + interfaces: + enum.SiliconLawsUiKey.Key: + type: SiliconLawBoundUserInterface + - type: ComplexInteraction + - type: DoorRemote + - type: Actions + - type: Access + groups: + - AllAccess + - type: Eye + drawFov: false + - type: Examiner + - type: InputMover + - type: Tag + tags: + - HideContextMenu + - StationAi + +# Hologram projection that the AI's eye tracks. +- type: entity + parent: + - Incorporeal + - BaseMob + id: StationAiHolo + name: Hologram + description: A projection of the AI. + noSpawn: true + suffix: DO NOT MAP + components: + - type: Eye + pvsScale: 1.5 + - type: Visibility + layer: 2 + - type: Sprite + sprite: Mobs/Silicon/station_ai.rsi + layers: + - state: default + shader: unshaded + map: ["base"] + +# Borgs - type: entity id: PlayerBorgGeneric parent: BorgChassisGeneric diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 4a14b5332ec811..cbe09c29ad916d 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -3,6 +3,7 @@ parent: - BaseMob - MobDamageable + - MobPolymorphable - MobCombat - StripableInventoryBase id: BaseMobSpecies diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index 3a555beebfbdb4..bf7c72c46d9d91 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -41,11 +41,16 @@ - type: CameraRecoil - type: MindContainer - type: MovementSpeedModifier - - type: Polymorphable - - type: StatusIcon - type: RequireProjectileTarget active: False +- type: entity + save: false + id: MobPolymorphable + abstract: true + components: + - type: Polymorphable + # Used for mobs that have health and can take damage. - type: entity save: false diff --git a/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml b/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml index 614af2a4886ace..5a3b967f98904d 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml @@ -32,7 +32,7 @@ blockSpectators: true # otherwise they can play client-side music inHandsOnly: false singleUser: true - requireHands: true + requiresComplex: true verbText: verb-instrument-openui key: enum.InstrumentUiKey.Key - type: InteractionOutline diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml index 1cf333bbafad11..4646c930865442 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml @@ -18,7 +18,7 @@ - type: PaperLabelType - type: ActivatableUI key: enum.PaperUiKey.Key - requireHands: false + requiresComplex: false - type: UserInterface interfaces: enum.PaperUiKey.Key: @@ -647,7 +647,7 @@ - Paper - type: ActivatableUI key: enum.PaperUiKey.Key - requireHands: false + requiresComplex: false - type: UserInterface interfaces: enum.PaperUiKey.Key: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index d27c49e263c253..2d2620efca8ff2 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -77,7 +77,6 @@ map: ["base"] - type: Input context: human - - type: BlockMovement - type: ToggleableGhostRole examineTextMindPresent: positronic-brain-installed examineTextMindSearching: positronic-brain-still-searching @@ -90,6 +89,7 @@ wipeVerbPopup: positronic-brain-wiped-device stopSearchVerbText: positronic-brain-stop-searching-verb-text stopSearchVerbPopup: positronic-brain-stopped-searching + - type: BlockMovement - type: Examiner - type: BorgBrain - type: IntrinsicRadioReceiver diff --git a/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml b/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml index 3c5bc93b42c27f..66f4689099e0ca 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml @@ -66,7 +66,7 @@ type: AccessOverriderBoundUserInterface - type: ActivatableUI key: enum.AccessOverriderUiKey.Key - requireHands: true + requiresComplex: true requireActiveHand: false singleUser: true - type: ItemSlots diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml index 86657eb46e0563..fe725b068410e9 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml @@ -4,6 +4,7 @@ name: airlock description: It opens, it closes, and maybe crushes you. components: + - type: StationAiWhitelist - type: MeleeSound soundGroups: Brute: @@ -104,6 +105,8 @@ - type: SpawnOnOverload - type: UserInterface interfaces: + enum.AiUi.Key: + type: StationAiBoundUserInterface enum.WiresUiKey.Key: type: WiresBoundUserInterface - type: Airtight diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index ed9d3afece72b0..8d1480e779c5f1 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -560,6 +560,7 @@ name: communications computer description: A computer used to make station wide announcements via keyboard, set the appropriate alert level, and call the emergency shuttle. components: + - type: StationAiWhitelist - type: Sprite layers: - map: ["computerLayerBody"] @@ -1117,3 +1118,46 @@ access: [["ResearchDirector"]] - type: Lock unlockOnClick: false + +- type: entity + id: StationAiUploadComputer + parent: BaseComputer + name: AI upload console + description: Used to update the laws of the station AI. + components: + - type: Sprite + layers: + - map: [ "computerLayerBody" ] + state: computer + - map: [ "computerLayerKeyboard" ] + state: generic_keyboard + - map: [ "computerLayerScreen" ] + state: aiupload + - map: [ "computerLayerKeys" ] + state: generic_keys + - type: ApcPowerReceiver + powerLoad: 1000 + - type: AccessReader + access: [ [ "ResearchDirector" ] ] + - type: Lock + unlockOnClick: false + - type: SiliconLawUpdater + components: + - type: StationAiHeld + - type: ItemSlotsLock + slots: + - circuit_holder + - type: ItemSlotRequiresPower + - type: ItemSlots + slots: + circuit_holder: + name: circuit-holder + insertSuccessPopup: silicon-laws-updated + whitelist: + components: + - SiliconLawProvider + - Item + - type: ContainerContainer + containers: + circuit_holder: !type:ContainerSlot + board: !type:Container diff --git a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml index 0485b5a517836b..539c8a244ae34f 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml @@ -96,7 +96,7 @@ type: WiresBoundUserInterface - type: ActivatableUI key: enum.HealthAnalyzerUiKey.Key - requireHands: false + requiresComplex: false - type: ActivatableUIRequiresPower - type: PointLight color: "#3a807f" diff --git a/Resources/Prototypes/Entities/Structures/Power/apc.yml b/Resources/Prototypes/Entities/Structures/Power/apc.yml index d8f32922c827ad..a52292d7fb902e 100644 --- a/Resources/Prototypes/Entities/Structures/Power/apc.yml +++ b/Resources/Prototypes/Entities/Structures/Power/apc.yml @@ -6,6 +6,7 @@ placement: mode: SnapgridCenter components: + - type: StationAiWhitelist - type: AmbientOnPowered - type: AmbientSound volume: -9 diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml index ca1b1b6c40f51e..60cea31fff296d 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml @@ -4,6 +4,7 @@ description: An intercom. For when the station just needs to know something. abstract: true components: + - type: StationAiWhitelist - type: WallMount - type: ApcPowerReceiver - type: Electrified diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml index 2a96da27604a9d..3530fe196c09be 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml @@ -4,6 +4,28 @@ name: camera description: A surveillance camera. It's watching you. Kinda. components: + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + # This exists for examine. + fix1: + shape: + !type:PhysShapeCircle + radius: 0.25 + light: + shape: + !type:PhysShapeCircle + radius: 5 + hard: false + mask: + - GhostImpassable + - type: LightOnCollide + - type: PointLight + enabled: false + radius: 5 + - type: SlimPoweredLight + enabled: false - type: StationAiVision - type: Clickable - type: InteractionOutline @@ -43,6 +65,8 @@ InUse: camera_in_use - type: UserInterface interfaces: + enum.AiUi.Key: + type: StationAiBoundUserInterface enum.SurveillanceCameraSetupUiKey.Camera: type: SurveillanceCameraSetupBoundUi enum.WiresUiKey.Key: diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml index ff935055ff9582..595915202d86f6 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml @@ -7,6 +7,7 @@ snap: - Wallmount components: + - type: StationAiWhitelist - type: Transform anchored: true - type: WallMount diff --git a/Resources/Prototypes/Loadouts/role_loadouts.yml b/Resources/Prototypes/Loadouts/role_loadouts.yml index e4c5c0aafbdace..b50e49fede7808 100644 --- a/Resources/Prototypes/Loadouts/role_loadouts.yml +++ b/Resources/Prototypes/Loadouts/role_loadouts.yml @@ -25,6 +25,11 @@ - Trinkets - GroupSpeciesBreathTool +# Silicons +- type: roleLoadout + id: JobStationAi + nameDataset: names_ai + # Civilian - type: roleLoadout id: JobPassenger diff --git a/Resources/Prototypes/Roles/Jobs/Science/borg.yml b/Resources/Prototypes/Roles/Jobs/Science/borg.yml index fe829110051bef..fffeaff39c59e4 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/borg.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/borg.yml @@ -1,3 +1,18 @@ +# No idea why it's in sci but we ball. +- type: job + id: StationAi + name: job-name-station-ai + description: job-description-station-ai + playTimeTracker: JobStationAi + requirements: + - !type:RoleTimeRequirement + role: JobBorg + time: 18000 # 5 hrs + canBeAntag: false + icon: JobIconStationAi + supervisors: job-supervisors-rd + jobEntity: StationAiBrain + - type: job id: Borg name: job-name-borg @@ -5,7 +20,7 @@ playTimeTracker: JobBorg requirements: - !type:OverallPlaytimeRequirement - time: 216000 #60 hrs + time: 216000 # 60 hrs canBeAntag: false icon: JobIconBorg supervisors: job-supervisors-rd diff --git a/Resources/Prototypes/Roles/Jobs/departments.yml b/Resources/Prototypes/Roles/Jobs/departments.yml index 6d25d8fd881ce7..6178ff89a2a9d6 100644 --- a/Resources/Prototypes/Roles/Jobs/departments.yml +++ b/Resources/Prototypes/Roles/Jobs/departments.yml @@ -16,7 +16,6 @@ weight: -10 roles: - Bartender - - Borg - Botanist - Boxer - Chaplain @@ -98,6 +97,15 @@ - Scientist - ResearchAssistant +- type: department + id: Silicon + name: department-Silicon + description: department-Silicon-description + color: "#D381C9" + roles: + - Borg + - StationAi + - type: department id: Specific name: department-Specific diff --git a/Resources/Prototypes/Roles/play_time_trackers.yml b/Resources/Prototypes/Roles/play_time_trackers.yml index 402d49e90d2395..d4cd1ec15d0b32 100644 --- a/Resources/Prototypes/Roles/play_time_trackers.yml +++ b/Resources/Prototypes/Roles/play_time_trackers.yml @@ -127,6 +127,9 @@ - type: playTimeTracker id: JobServiceWorker +- type: playTimeTracker + id: JobStationAi + - type: playTimeTracker id: JobStationEngineer diff --git a/Resources/Prototypes/StatusIcon/job.yml b/Resources/Prototypes/StatusIcon/job.yml index eb9896fc480613..0abb86aeab8f29 100644 --- a/Resources/Prototypes/StatusIcon/job.yml +++ b/Resources/Prototypes/StatusIcon/job.yml @@ -29,6 +29,14 @@ state: Borg jobName: job-name-borg +- type: jobIcon + parent: JobIcon + id: JobIconStationAi + icon: + sprite: /Textures/Interface/Misc/job_icons.rsi + state: StationAi + jobName: job-name-station-ai + - type: jobIcon parent: JobIcon id: JobIconBotanist diff --git a/Resources/Prototypes/Wires/layouts.yml b/Resources/Prototypes/Wires/layouts.yml index 3939655707e103..ccab000cfa2b41 100644 --- a/Resources/Prototypes/Wires/layouts.yml +++ b/Resources/Prototypes/Wires/layouts.yml @@ -9,6 +9,7 @@ - !type:DoorBoltLightWireAction - !type:DoorTimingWireAction - !type:DoorSafetyWireAction + - !type:AiInteractWireAction - type: wireLayout parent: Airlock @@ -97,9 +98,10 @@ - type: wireLayout id: SurveillanceCamera - dummyWires: 4 + dummyWires: 2 wires: - !type:PowerWireAction + - !type:AiVisionWireAction - type: wireLayout id: CryoPod diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index b52e2c4e8c5cf6..bece1fb479a634 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1182,6 +1182,7 @@ - type: Tag id: Soup + - type: Tag id: Spear @@ -1209,6 +1210,9 @@ - type: Tag id: SpreaderIgnore +- type: Tag + id: StationAi + - type: Tag id: StationMapElectronics diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png new file mode 100644 index 00000000000000..8dd3031f9fc0b4 Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png differ diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png new file mode 100644 index 00000000000000..041b9b9bf7c8b9 Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png differ diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png new file mode 100644 index 00000000000000..78fad17a76c58c Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png differ diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png new file mode 100644 index 00000000000000..08514aa90829c5 Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png differ diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json new file mode 100644 index 00000000000000..a7c00f779353b6 --- /dev/null +++ b/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/blob/c473a8bcc28fbd80827dfca5660d81ca6e833e2c/icons/hud/screen_ai.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "ai_core" + }, + { + "name": "camera_light" + }, + { + "name": "crew_monitor" + }, + { + "name": "manifest" + }, + { + "name": "state_laws" + } + ] +} diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png new file mode 100644 index 00000000000000..e30e891745fb52 Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png differ diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png b/Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png new file mode 100644 index 00000000000000..aba35c034b9f15 Binary files /dev/null and b/Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png differ diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json index fff9f78288cfbd..7b09250bd986b6 100644 --- a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json +++ b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord) | Mindshield icon taken from https://github.com/tgstation/tgstation/blob/master/icons/mob/huds/hud.dmi | Admin recolored from MedicalIntern by TsjipTsjip", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord) | Mindshield icon taken from https://github.com/tgstation/tgstation/blob/ce6beb8a4d61235d9a597a7126c407160ed674ea/icons/mob/huds/hud.dmi | Admin recolored from MedicalIntern by TsjipTsjip", "size": { "x": 8, @@ -177,6 +177,9 @@ [1.0,1.0] ] }, + { + "name": "StationAi" + }, { "name": "Syndicate" }, diff --git a/Resources/Textures/Interface/noise.rsi/meta.json b/Resources/Textures/Interface/noise.rsi/meta.json new file mode 100644 index 00000000000000..068ecab968eefa --- /dev/null +++ b/Resources/Textures/Interface/noise.rsi/meta.json @@ -0,0 +1,58 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/4b4e9dff1d7d891cfb75d25ca5bf5172d1c02be6/icons/hud/screen_gen.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "noise", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Interface/noise.rsi/noise.png b/Resources/Textures/Interface/noise.rsi/noise.png new file mode 100644 index 00000000000000..ba74952b409b6d Binary files /dev/null and b/Resources/Textures/Interface/noise.rsi/noise.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png new file mode 100644 index 00000000000000..9b72b39492803a Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png new file mode 100644 index 00000000000000..c583bb88f21bd6 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png new file mode 100644 index 00000000000000..28bf165122ea7c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png new file mode 100644 index 00000000000000..7c5b468885aa71 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png new file mode 100644 index 00000000000000..6539176b8487d9 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png new file mode 100644 index 00000000000000..eda1f4bfb568d5 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png new file mode 100644 index 00000000000000..63616e70b52120 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png new file mode 100644 index 00000000000000..3dc7a301d865c6 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png new file mode 100644 index 00000000000000..f3ba4b591abcf9 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai.png new file mode 100644 index 00000000000000..3c81e3a751ec86 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png new file mode 100644 index 00000000000000..96e953660f8eae Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png new file mode 100644 index 00000000000000..96e953660f8eae Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png new file mode 100644 index 00000000000000..f14b4ef0fa6b3e Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/default.png b/Resources/Textures/Mobs/Silicon/output.rsi/default.png new file mode 100644 index 00000000000000..f14b4ef0fa6b3e Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/default.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png new file mode 100644 index 00000000000000..05de742794341c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png new file mode 100644 index 00000000000000..05de742794341c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png new file mode 100644 index 00000000000000..10efd5ee1dbc3a Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/horror.png b/Resources/Textures/Mobs/Silicon/output.rsi/horror.png new file mode 100644 index 00000000000000..0a807c0c235408 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/horror.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/meta.json b/Resources/Textures/Mobs/Silicon/output.rsi/meta.json new file mode 100644 index 00000000000000..a40ed37c6040bc --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/output.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/2a19963297f91efb452dbb5c1d4eb28a14776b0a/icons/mob/silicon/ai.dmi", "states": [{"name": "ai", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.1]]}, {"name": "ai-banned", "directions": 1, "delays": [[0.7, 0.7, 0.7, 0.7, 0.7, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7]]}, {"name": "ai-banned-unshaded", "directions": 1, "delays": [[0.7, 0.7, 0.7, 0.7, 0.7, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7]]}, {"name": "ai-banned_dead", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-banned_dead-unshaded", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-empty", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-empty-unshaded", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-holo-old", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "ai-holo-old-unshaded", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "ai-unshaded", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.1]]}, {"name": "ai_dead", "directions": 1, "delays": [[1.0]]}, {"name": "ai_dead-unshaded", "directions": 1, "delays": [[1.0]]}, {"name": "default", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "default-unshaded", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "floating_face", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "floating_face-unshaded", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "horror", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "horror-unshaded", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "xeno_queen", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "xeno_queen-unshaded", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]} \ No newline at end of file diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png new file mode 100644 index 00000000000000..3ea194039f4fee Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png differ diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png new file mode 100644 index 00000000000000..3ea194039f4fee Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png new file mode 100644 index 00000000000000..420a07c1f96662 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png new file mode 100644 index 00000000000000..eb74655e027f45 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png new file mode 100644 index 00000000000000..40e8ac52161869 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png new file mode 100644 index 00000000000000..a9db83691c1db9 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png new file mode 100644 index 00000000000000..d52aceaf5b13f2 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json new file mode 100644 index 00000000000000..a3da52233dd12e --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/2a19963297f91efb452dbb5c1d4eb28a14776b0a/icons/mob/silicon/ai.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "ai", + "delays": [ + [ + 0.2, + 0.2, + 0.1, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.1 + ] + ] + }, + { + "name": "ai_dead" + }, + { + "name": "ai_empty", + "delays": [ + [ + 0.7, + 0.7 + ] + ] + }, + { + "name": "default", + "directions": 4 + }, + { + "name": "base" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/base.png b/Resources/Textures/Objects/Devices/ai_card.rsi/base.png new file mode 100644 index 00000000000000..244183c078c3d5 Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/base.png differ diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/empty.png b/Resources/Textures/Objects/Devices/ai_card.rsi/empty.png new file mode 100644 index 00000000000000..7e61f368de294a Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/empty.png differ diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/full.png b/Resources/Textures/Objects/Devices/ai_card.rsi/full.png new file mode 100644 index 00000000000000..59131c8c0aafc5 Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/full.png differ diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png new file mode 100644 index 00000000000000..2d3863145b951a Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png new file mode 100644 index 00000000000000..1704b9c3c112fc Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json new file mode 100644 index 00000000000000..8b8135fa16e6b7 --- /dev/null +++ b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json @@ -0,0 +1,58 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/1feffb747a33434a9d28450fc52ade75253aeba5/icons/obj/aicards.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "empty", + "delays": [ + [ + 0.4, + 0.4 + ] + ] + }, + { + "name": "full", + "delays": [ + [ + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png b/Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png new file mode 100644 index 00000000000000..51a309d579ec0b Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png differ diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard-full.png b/Resources/Textures/Objects/Devices/output.rsi/aicard-full.png new file mode 100644 index 00000000000000..03908b5284f4fd Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard-full.png differ diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png b/Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png new file mode 100644 index 00000000000000..6191a01ec4eb7c Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png differ diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard.png b/Resources/Textures/Objects/Devices/output.rsi/aicard.png new file mode 100644 index 00000000000000..57f604efb0eab5 Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard.png differ diff --git a/Resources/Textures/Objects/Devices/output.rsi/meta.json b/Resources/Textures/Objects/Devices/output.rsi/meta.json new file mode 100644 index 00000000000000..500ecb8e3e7d14 --- /dev/null +++ b/Resources/Textures/Objects/Devices/output.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/1feffb747a33434a9d28450fc52ade75253aeba5/icons/obj/aicards.dmi", "states": [{"name": "aicard", "directions": 1, "delays": [[0.4, 0.4]]}, {"name": "aicard-full", "directions": 1, "delays": [[0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]]}, {"name": "aicard-full-unshaded", "directions": 1, "delays": [[0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]]}, {"name": "aicard-unshaded", "directions": 1, "delays": [[0.4, 0.4]]}]} \ No newline at end of file