mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-10 10:51:14 +03:00
* Axes the easy IMapManager references * Axes the references to unported (obsolete) IMapManager methods * Converts salvageruler command to a LocalizedEntityCommand so it can use SharedMapSystem * Removes IMapManager from GameTicker This required axing the map nuke during restart, but that should be fine? Maps are entities so EntityManager.FlushEntities should delete them all and that's the preceding line * submodule update(?) --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
542 lines
16 KiB
C#
542 lines
16 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Shuttles.Systems;
|
|
using Content.Shared.Shuttles.BUIStates;
|
|
using Content.Shared.Shuttles.Components;
|
|
using Content.Shared.Shuttles.Systems;
|
|
using Content.Shared.Shuttles.UI.MapObjects;
|
|
using Content.Shared.Timing;
|
|
using Robust.Client.AutoGenerated;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.XAML;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Client.Shuttles.UI;
|
|
|
|
[GenerateTypedNameReferences]
|
|
public sealed partial class MapScreen : BoxContainer
|
|
{
|
|
[Dependency] private IEntityManager _entManager = default!;
|
|
[Dependency] private IGameTiming _timing = default!;
|
|
[Dependency] private IRobustRandom _random = default!;
|
|
private readonly SharedAudioSystem _audio;
|
|
private readonly SharedMapSystem _maps;
|
|
private readonly ShuttleSystem _shuttles;
|
|
private readonly SharedTransformSystem _xformSystem;
|
|
|
|
private EntityUid? _console;
|
|
private EntityUid? _shuttleEntity;
|
|
|
|
private FTLState _state;
|
|
private StartEndTime _ftlTime;
|
|
|
|
private List<ShuttleBeaconObject> _beacons = new();
|
|
private List<ShuttleExclusionObject> _exclusions = new();
|
|
|
|
private TimeSpan _nextPing;
|
|
private TimeSpan _pingCooldown = TimeSpan.FromSeconds(3);
|
|
private TimeSpan _nextMapDequeue;
|
|
|
|
private float _minMapDequeue = 0.05f;
|
|
private float _maxMapDequeue = 0.25f;
|
|
|
|
private StyleBoxFlat _ftlStyle;
|
|
|
|
public event Action<MapCoordinates, Angle>? RequestFTL;
|
|
public event Action<NetEntity, Angle>? RequestBeaconFTL;
|
|
|
|
private readonly Dictionary<MapId, BoxContainer> _mapHeadings = new();
|
|
private readonly Dictionary<MapId, List<IMapObject>> _mapObjects = new();
|
|
private readonly List<(MapId mapId, IMapObject mapobj)> _pendingMapObjects = new();
|
|
|
|
/// <summary>
|
|
/// Store the names of map object controls for re-sorting later.
|
|
/// </summary>
|
|
private Dictionary<Control, string> _mapObjectControls = new();
|
|
|
|
private List<Control> _sortChildren = new();
|
|
|
|
public MapScreen()
|
|
{
|
|
RobustXamlLoader.Load(this);
|
|
IoCManager.InjectDependencies(this);
|
|
|
|
_audio = _entManager.System<SharedAudioSystem>();
|
|
_maps = _entManager.System<SharedMapSystem>();
|
|
_shuttles = _entManager.System<ShuttleSystem>();
|
|
_xformSystem = _entManager.System<SharedTransformSystem>();
|
|
|
|
MapRebuildButton.OnPressed += MapRebuildPressed;
|
|
|
|
OnVisibilityChanged += OnVisChange;
|
|
|
|
MapFTLButton.OnToggled += FtlPreviewToggled;
|
|
|
|
_ftlStyle = new StyleBoxFlat(Color.LimeGreen);
|
|
FTLBar.ForegroundStyleBoxOverride = _ftlStyle;
|
|
|
|
// Just pass it on up.
|
|
MapRadar.RequestFTL += (coords, angle) =>
|
|
{
|
|
RequestFTL?.Invoke(coords, angle);
|
|
};
|
|
|
|
MapRadar.RequestBeaconFTL += (ent, angle) =>
|
|
{
|
|
RequestBeaconFTL?.Invoke(ent, angle);
|
|
};
|
|
|
|
MapBeaconsButton.OnToggled += args =>
|
|
{
|
|
MapRadar.ShowBeacons = args.Pressed;
|
|
};
|
|
}
|
|
|
|
public void UpdateState(ShuttleMapInterfaceState state)
|
|
{
|
|
// Only network the accumulator due to ping making the thing fonky.
|
|
// This should work better with predicting network states as they come in.
|
|
_beacons = state.Destinations;
|
|
_exclusions = state.Exclusions;
|
|
_state = state.FTLState;
|
|
_ftlTime = state.FTLTime;
|
|
MapRadar.InFtl = true;
|
|
MapFTLState.Text = Loc.GetString($"shuttle-console-ftl-state-{_state.ToString()}");
|
|
|
|
switch (_state)
|
|
{
|
|
case FTLState.Available:
|
|
SetFTLAllowed(true);
|
|
_ftlStyle.BackgroundColor = Color.FromHex("#80C71F");
|
|
MapRadar.InFtl = false;
|
|
break;
|
|
case FTLState.Starting:
|
|
SetFTLAllowed(false);
|
|
_ftlStyle.BackgroundColor = Color.FromHex("#169C9C");
|
|
break;
|
|
case FTLState.Travelling:
|
|
SetFTLAllowed(false);
|
|
_ftlStyle.BackgroundColor = Color.FromHex("#8932B8");
|
|
break;
|
|
case FTLState.Arriving:
|
|
SetFTLAllowed(false);
|
|
_ftlStyle.BackgroundColor = Color.FromHex("#F9801D");
|
|
break;
|
|
case FTLState.Cooldown:
|
|
SetFTLAllowed(false);
|
|
// Scroll to the FTL spot
|
|
if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
|
|
{
|
|
var targetOffset = _maps.GetGridPosition(_shuttleEntity.Value);
|
|
MapRadar.SetMap(shuttleXform.MapID, targetOffset, recentering: true);
|
|
}
|
|
|
|
_ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
|
|
MapRadar.InFtl = false;
|
|
break;
|
|
// Fallback in case no FTL state or the likes.
|
|
default:
|
|
SetFTLAllowed(false);
|
|
_ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
|
|
MapRadar.InFtl = false;
|
|
break;
|
|
}
|
|
|
|
if (IsPingBlocked())
|
|
{
|
|
MapRebuildButton.Disabled = true;
|
|
ClearMapObjects();
|
|
}
|
|
}
|
|
|
|
private void SetFTLAllowed(bool value)
|
|
{
|
|
if (value)
|
|
{
|
|
MapFTLButton.Disabled = false;
|
|
}
|
|
else
|
|
{
|
|
// Unselect FTL
|
|
MapFTLButton.Pressed = false;
|
|
MapRadar.FtlMode = false;
|
|
MapFTLButton.Disabled = true;
|
|
}
|
|
}
|
|
|
|
private void FtlPreviewToggled(BaseButton.ButtonToggledEventArgs obj)
|
|
{
|
|
MapRadar.FtlMode = obj.Pressed;
|
|
}
|
|
|
|
public void SetConsole(EntityUid? console)
|
|
{
|
|
_console = console;
|
|
}
|
|
|
|
public void SetShuttle(EntityUid? shuttle)
|
|
{
|
|
_shuttleEntity = shuttle;
|
|
MapRadar.SetShuttle(shuttle);
|
|
}
|
|
|
|
private void OnVisChange(Control obj)
|
|
{
|
|
if (!obj.Visible)
|
|
return;
|
|
|
|
// Centre map screen to the shuttle.
|
|
if (_shuttleEntity != null)
|
|
{
|
|
var mapPos = _xformSystem.GetMapCoordinates(_shuttleEntity.Value);
|
|
MapRadar.SetMap(mapPos.MapId, mapPos.Position);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does a sonar-like effect on the map.
|
|
/// </summary>
|
|
public void PingMap()
|
|
{
|
|
if (_console != null)
|
|
{
|
|
_audio.PlayEntity(new SoundPathSpecifier("/Audio/Effects/Shuttle/radar_ping.ogg"), Filter.Local(), _console.Value, true);
|
|
}
|
|
|
|
RebuildMapObjects();
|
|
BumpMapDequeue();
|
|
|
|
_nextPing = _timing.CurTime + _pingCooldown;
|
|
MapRebuildButton.Disabled = true;
|
|
}
|
|
|
|
private void BumpMapDequeue()
|
|
{
|
|
_nextMapDequeue = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(_minMapDequeue, _maxMapDequeue));
|
|
}
|
|
|
|
private void MapRebuildPressed(BaseButton.ButtonEventArgs obj)
|
|
{
|
|
PingMap();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all sector objects across all maps (e.g. if we start FTLing or need to re-ping).
|
|
/// </summary>
|
|
private void ClearMapObjects()
|
|
{
|
|
_mapObjectControls.Clear();
|
|
HyperspaceDestinations.RemoveAllChildren();
|
|
_pendingMapObjects.Clear();
|
|
_mapObjects.Clear();
|
|
_mapHeadings.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all map objects at time of ping and adds them to pending to be added over time.
|
|
/// </summary>
|
|
private void RebuildMapObjects()
|
|
{
|
|
ClearMapObjects();
|
|
|
|
if (_shuttleEntity == null)
|
|
return;
|
|
|
|
var mapComps = _entManager.AllEntityQueryEnumerator<MapComponent, TransformComponent, MetaDataComponent>();
|
|
MapId ourMap = MapId.Nullspace;
|
|
|
|
if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
|
|
{
|
|
ourMap = shuttleXform.MapID;
|
|
}
|
|
|
|
while (mapComps.MoveNext(out var mapUid, out var mapComp, out var mapXform, out var mapMetadata))
|
|
{
|
|
if (_console != null && !_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId, _console.Value))
|
|
{
|
|
continue;
|
|
}
|
|
var mapName = mapMetadata.EntityName;
|
|
|
|
if (string.IsNullOrEmpty(mapName))
|
|
{
|
|
mapName = Loc.GetString("shuttle-console-unknown");
|
|
}
|
|
|
|
var heading = new CollapsibleHeading(mapName);
|
|
|
|
heading.MinHeight = 32f;
|
|
heading.AddStyleClass(ContainerButton.StyleClassButton);
|
|
heading.HorizontalAlignment = HAlignment.Stretch;
|
|
heading.Label.HorizontalAlignment = HAlignment.Center;
|
|
heading.Label.HorizontalExpand = true;
|
|
heading.HorizontalExpand = true;
|
|
|
|
var gridContents = new BoxContainer()
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
VerticalExpand = true,
|
|
};
|
|
|
|
var body = new CollapsibleBody()
|
|
{
|
|
HorizontalAlignment = HAlignment.Stretch,
|
|
VerticalAlignment = VAlignment.Top,
|
|
HorizontalExpand = true,
|
|
Children =
|
|
{
|
|
gridContents
|
|
}
|
|
};
|
|
|
|
var mapButton = new Collapsible(heading, body);
|
|
|
|
heading.OnToggled += args =>
|
|
{
|
|
if (args.Pressed)
|
|
{
|
|
HideOtherCollapsibles(mapButton);
|
|
}
|
|
};
|
|
|
|
_mapHeadings.Add(mapComp.MapId, gridContents);
|
|
foreach (var grid in _maps.GetAllGrids(mapComp.MapId))
|
|
{
|
|
_entManager.TryGetComponent(grid.Owner, out IFFComponent? iffComp);
|
|
|
|
var gridObj = new GridMapObject()
|
|
{
|
|
Name = _entManager.GetComponent<MetaDataComponent>(grid.Owner).EntityName,
|
|
Entity = grid.Owner,
|
|
HideButton = iffComp != null && (iffComp.Flags & IFFFlags.HideLabel) != 0x0,
|
|
};
|
|
|
|
// Always show our shuttle immediately
|
|
if (grid.Owner == _shuttleEntity)
|
|
{
|
|
AddMapObject(mapComp.MapId, gridObj);
|
|
}
|
|
// If we can show it then add it to pending.
|
|
else if (!_shuttles.IsBeaconMap(mapUid) && (iffComp == null ||
|
|
(iffComp.Flags & IFFFlags.Hide) == 0x0) &&
|
|
!gridObj.HideButton)
|
|
{
|
|
_pendingMapObjects.Add((mapComp.MapId, gridObj));
|
|
}
|
|
}
|
|
|
|
foreach (var (beacon, _) in _shuttles.GetExclusions(mapComp.MapId, _exclusions))
|
|
{
|
|
if (beacon.HideButton)
|
|
continue;
|
|
|
|
_pendingMapObjects.Add((mapComp.MapId, beacon));
|
|
}
|
|
|
|
foreach (var (beacon, _) in _shuttles.GetBeacons(mapComp.MapId, _beacons))
|
|
{
|
|
if (beacon.HideButton)
|
|
continue;
|
|
|
|
_pendingMapObjects.Add((mapComp.MapId, beacon));
|
|
}
|
|
|
|
HyperspaceDestinations.AddChild(mapButton);
|
|
|
|
// Zoom in to our map
|
|
if (mapComp.MapId == MapRadar.ViewingMap)
|
|
{
|
|
mapButton.BodyVisible = true;
|
|
}
|
|
}
|
|
|
|
// Need to sort from furthest way to nearest (as we will pop from the end of the list first).
|
|
// Also prioritise those on our map first.
|
|
var shuttlePos = _xformSystem.GetWorldPosition(_shuttleEntity.Value);
|
|
|
|
_pendingMapObjects.Sort((x, y) =>
|
|
{
|
|
if (x.mapId == ourMap && y.mapId != ourMap)
|
|
return 1;
|
|
|
|
if (y.mapId == ourMap && x.mapId != ourMap)
|
|
return -1;
|
|
|
|
var yMapPos = _shuttles.GetMapCoordinates(y.mapobj);
|
|
var xMapPos = _shuttles.GetMapCoordinates(x.mapobj);
|
|
|
|
return (yMapPos.Position - shuttlePos).Length().CompareTo((xMapPos.Position - shuttlePos).Length());
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hides other maps upon the specified collapsible being selected (AKA hacky collapsible groups).
|
|
/// </summary>
|
|
private void HideOtherCollapsibles(Collapsible collapsible)
|
|
{
|
|
foreach (var child in HyperspaceDestinations.Children)
|
|
{
|
|
if (child is not Collapsible childCollapse || childCollapse == collapsible)
|
|
continue;
|
|
|
|
childCollapse.BodyVisible = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if we shouldn't be able to select the FTL button.
|
|
/// </summary>
|
|
private bool IsFTLBlocked()
|
|
{
|
|
switch (_state)
|
|
{
|
|
case FTLState.Available:
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if we shouldn't be able to select the Scan for Objects button.
|
|
/// </summary>
|
|
private bool IsPingBlocked()
|
|
{
|
|
return _state switch
|
|
{
|
|
FTLState.Available or FTLState.Cooldown => false,
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
private void OnMapObjectPress(IMapObject mapObject)
|
|
{
|
|
if (IsPingBlocked())
|
|
return;
|
|
|
|
var coordinates = _shuttles.GetMapCoordinates(mapObject);
|
|
|
|
// If it's our map then scroll, otherwise just set position there.
|
|
MapRadar.SetMap(coordinates.MapId, coordinates.Position, recentering: true);
|
|
}
|
|
|
|
public void SetMap(MapId mapId, Vector2 position)
|
|
{
|
|
MapRadar.SetMap(mapId, position);
|
|
MapRadar.Offset = position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a map object to the specified sector map.
|
|
/// </summary>
|
|
private void AddMapObject(MapId mapId, IMapObject mapObj)
|
|
{
|
|
var existing = _mapObjects.GetOrNew(mapId);
|
|
existing.Add(mapObj);
|
|
|
|
var gridContents = _mapHeadings[mapId];
|
|
|
|
var gridButton = new Button()
|
|
{
|
|
Text = mapObj.Name,
|
|
HorizontalExpand = true,
|
|
};
|
|
|
|
var gridContainer = new BoxContainer()
|
|
{
|
|
Children =
|
|
{
|
|
new Control()
|
|
{
|
|
MinWidth = 32f,
|
|
},
|
|
gridButton
|
|
}
|
|
};
|
|
|
|
_mapObjectControls.Add(gridContainer, mapObj.Name);
|
|
gridContents.AddChild(gridContainer);
|
|
|
|
gridButton.OnPressed += args =>
|
|
{
|
|
OnMapObjectPress(mapObj);
|
|
};
|
|
|
|
if (gridContents.ChildCount > 1)
|
|
{
|
|
// Re-sort the children
|
|
_sortChildren.Clear();
|
|
|
|
foreach (var child in gridContents.Children)
|
|
{
|
|
DebugTools.Assert(_mapObjectControls.ContainsKey(child));
|
|
_sortChildren.Add(child);
|
|
}
|
|
|
|
foreach (var child in _sortChildren)
|
|
{
|
|
child.Orphan();
|
|
}
|
|
|
|
_sortChildren.Sort((x, y) =>
|
|
{
|
|
var xText = _mapObjectControls[x];
|
|
var yText = _mapObjectControls[y];
|
|
|
|
return string.Compare(xText, yText, StringComparison.CurrentCultureIgnoreCase);
|
|
});
|
|
|
|
foreach (var control in _sortChildren)
|
|
{
|
|
gridContents.AddChild(control);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
|
|
var curTime = _timing.CurTime;
|
|
|
|
if (_nextMapDequeue < curTime && _pendingMapObjects.Count > 0)
|
|
{
|
|
var mapObj = _pendingMapObjects[^1];
|
|
_pendingMapObjects.RemoveAt(_pendingMapObjects.Count - 1);
|
|
AddMapObject(mapObj.mapId, mapObj.mapobj);
|
|
BumpMapDequeue();
|
|
}
|
|
|
|
if (!IsPingBlocked() && _nextPing < curTime)
|
|
{
|
|
MapRebuildButton.Disabled = false;
|
|
}
|
|
|
|
var progress = _ftlTime.ProgressAt(curTime);
|
|
FTLBar.Value = float.IsFinite(progress) ? progress : 1;
|
|
}
|
|
|
|
protected override void Draw(DrawingHandleScreen handle)
|
|
{
|
|
MapRadar.SetMapObjects(_mapObjects);
|
|
base.Draw(handle);
|
|
}
|
|
|
|
public void Startup()
|
|
{
|
|
if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
|
|
{
|
|
SetMap(shuttleXform.MapID, _maps.GetGridPosition((_shuttleEntity.Value, null, shuttleXform)));
|
|
}
|
|
}
|
|
}
|