using System.Diagnostics.CodeAnalysis; using Content.Client.Popups; using Content.Shared.Construction; using Content.Shared.Construction.Prototypes; using Content.Shared.Examine; using Content.Shared.Input; using Content.Shared.Interaction; using Content.Shared.Wall; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Prototypes; namespace Content.Client.Construction { /// /// The client-side implementation of the construction system, which is used for constructing entities in game. /// [UsedImplicitly] public sealed class ConstructionSystem : SharedConstructionSystem { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; private readonly Dictionary _ghosts = new(); private readonly Dictionary _guideCache = new(); private int _nextId; public bool CraftingEnabled { get; private set; } /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(HandlePlayerAttached); SubscribeNetworkEvent(HandleAckStructure); SubscribeNetworkEvent(OnConstructionGuideReceived); CommandBinds.Builder .Bind(ContentKeyFunctions.OpenCraftingMenu, new PointerInputCmdHandler(HandleOpenCraftingMenu, outsidePrediction:true)) .Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(HandleUse, outsidePrediction: true)) .Bind(ContentKeyFunctions.EditorFlipObject, new PointerInputCmdHandler(HandleFlip, outsidePrediction:true)) .Register(); SubscribeLocalEvent(HandleConstructionGhostExamined); } private void OnConstructionGuideReceived(ResponseConstructionGuide ev) { _guideCache[ev.ConstructionId] = ev.Guide; ConstructionGuideAvailable?.Invoke(this, ev.ConstructionId); } /// public override void Shutdown() { base.Shutdown(); CommandBinds.Unregister(); } public ConstructionGuide? GetGuide(ConstructionPrototype prototype) { if (_guideCache.TryGetValue(prototype.ID, out var guide)) return guide; RaiseNetworkEvent(new RequestConstructionGuide(prototype.ID)); return null; } private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostComponent component, ExaminedEvent args) { if (component.Prototype == null) return; args.PushMarkup(Loc.GetString( "construction-ghost-examine-message", ("name", component.Prototype.Name))); if (!_prototypeManager.TryIndex(component.Prototype.Graph, out ConstructionGraphPrototype? graph)) return; var startNode = graph.Nodes[component.Prototype.StartNode]; if (!graph.TryPath(component.Prototype.StartNode, component.Prototype.TargetNode, out var path) || !startNode.TryGetEdge(path[0].Name, out var edge)) { return; } edge.Steps[0].DoExamine(args); } public event EventHandler? CraftingAvailabilityChanged; public event EventHandler? ConstructionGuideAvailable; public event EventHandler? ToggleCraftingWindow; public event EventHandler? FlipConstructionPrototype; private void HandleAckStructure(AckStructureConstructionMessage msg) { ClearGhost(msg.GhostId); } private void HandlePlayerAttached(PlayerAttachSysMessage msg) { var available = IsCraftingAvailable(msg.AttachedEntity); UpdateCraftingAvailability(available); } private bool HandleOpenCraftingMenu(in PointerInputCmdHandler.PointerInputCmdArgs args) { if (args.State == BoundKeyState.Down) ToggleCraftingWindow?.Invoke(this, EventArgs.Empty); return true; } private bool HandleFlip(in PointerInputCmdHandler.PointerInputCmdArgs args) { if (args.State == BoundKeyState.Down) FlipConstructionPrototype?.Invoke(this, EventArgs.Empty); return true; } private void UpdateCraftingAvailability(bool available) { if (CraftingEnabled == available) return; CraftingAvailabilityChanged?.Invoke(this, new CraftingAvailabilityChangedArgs(available)); CraftingEnabled = available; } private static bool IsCraftingAvailable(EntityUid? entity) { if (entity == default) return false; // TODO: Decide if entity can craft, using capabilities or something return true; } private bool HandleUse(in PointerInputCmdHandler.PointerInputCmdArgs args) { if (!args.EntityUid.IsValid() || !args.EntityUid.IsClientSide()) return false; if (!EntityManager.TryGetComponent(args.EntityUid, out var ghostComp)) return false; TryStartConstruction(ghostComp.GhostId); return true; } /// /// Creates a construction ghost at the given location. /// public void SpawnGhost(ConstructionPrototype prototype, EntityCoordinates loc, Direction dir) => TrySpawnGhost(prototype, loc, dir, out _); /// /// Creates a construction ghost at the given location. /// public bool TrySpawnGhost( ConstructionPrototype prototype, EntityCoordinates loc, Direction dir, [NotNullWhen(true)] out EntityUid? ghost) { ghost = null; if (_playerManager.LocalPlayer?.ControlledEntity is not { } user || !user.IsValid()) { return false; } if (GhostPresent(loc)) return false; // This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?" var predicate = GetPredicate(prototype.CanBuildInImpassable, loc.ToMap(EntityManager)); if (!_interactionSystem.InRangeUnobstructed(user, loc, 20f, predicate: predicate)) return false; if (!CheckConstructionConditions(prototype, loc, dir, user, showPopup: true)) return false; ghost = EntityManager.SpawnEntity("constructionghost", loc); var comp = EntityManager.GetComponent(ghost.Value); comp.Prototype = prototype; comp.GhostId = _nextId++; EntityManager.GetComponent(ghost.Value).LocalRotation = dir.ToAngle(); _ghosts.Add(comp.GhostId, comp); var sprite = EntityManager.GetComponent(ghost.Value); sprite.Color = new Color(48, 255, 48, 128); for (int i = 0; i < prototype.Layers.Count; i++) { sprite.AddBlankLayer(i); // There is no way to actually check if this already exists, so we blindly insert a new one sprite.LayerSetSprite(i, prototype.Layers[i]); sprite.LayerSetShader(i, "unshaded"); sprite.LayerSetVisible(i, true); } if (prototype.CanBuildInImpassable) EnsureComp(ghost.Value).Arc = new(Math.Tau); return true; } private bool CheckConstructionConditions(ConstructionPrototype prototype, EntityCoordinates loc, Direction dir, EntityUid user, bool showPopup = false) { foreach (var condition in prototype.Conditions) { if (!condition.Condition(user, loc, dir)) { if (showPopup) { var message = condition.GenerateGuideEntry()?.Localization; if (message != null) { // Show the reason to the user: _popupSystem.PopupCoordinates(Loc.GetString(message), loc); } } return false; } } return true; } /// /// Checks if any construction ghosts are present at the given position /// private bool GhostPresent(EntityCoordinates loc) { foreach (var ghost in _ghosts) { if (EntityManager.GetComponent(ghost.Value.Owner).Coordinates.Equals(loc)) return true; } return false; } public void TryStartConstruction(int ghostId) { var ghost = _ghosts[ghostId]; if (ghost.Prototype == null) { throw new ArgumentException($"Can't start construction for a ghost with no prototype. Ghost id: {ghostId}"); } var transform = EntityManager.GetComponent(ghost.Owner); var msg = new TryStartStructureConstructionMessage(transform.Coordinates, ghost.Prototype.ID, transform.LocalRotation, ghostId); RaiseNetworkEvent(msg); } /// /// Starts constructing an item underneath the attached entity. /// public void TryStartItemConstruction(string prototypeName) { RaiseNetworkEvent(new TryStartItemConstructionMessage(prototypeName)); } /// /// Removes a construction ghost entity with the given ID. /// public void ClearGhost(int ghostId) { if (_ghosts.TryGetValue(ghostId, out var ghost)) { EntityManager.QueueDeleteEntity(ghost.Owner); _ghosts.Remove(ghostId); } } /// /// Removes all construction ghosts. /// public void ClearAllGhosts() { foreach (var (_, ghost) in _ghosts) { EntityManager.QueueDeleteEntity(ghost.Owner); } _ghosts.Clear(); } } public sealed class CraftingAvailabilityChangedArgs : EventArgs { public bool Available { get; } public CraftingAvailabilityChangedArgs(bool available) { Available = available; } } }