mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
114 Commits
feature/st
...
v0.45.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea35e6c03f | ||
|
|
a6c6b22393 | ||
|
|
5e6e5cb5ea | ||
|
|
64d2a3ca28 | ||
|
|
bee4e499a8 | ||
|
|
55da2dc5e5 | ||
|
|
177eca0f90 | ||
|
|
ae229a03d8 | ||
|
|
2dc8093660 | ||
|
|
071acbc818 | ||
|
|
3f5982cac0 | ||
|
|
6f387402fe | ||
|
|
363e28561c | ||
|
|
4d63b54259 | ||
|
|
7fa05c7db3 | ||
|
|
c466f5f17f | ||
|
|
b6f302b71e | ||
|
|
7d9db6f7b2 | ||
|
|
8bedba2da7 | ||
|
|
837dde78c9 | ||
|
|
a01629969d | ||
|
|
15e13c7f92 | ||
|
|
3dd80b90eb | ||
|
|
cb8e295ad9 | ||
|
|
909163a923 | ||
|
|
6aa2408364 | ||
|
|
afc1eed7ae | ||
|
|
85c8dc05fe | ||
|
|
710371d7d1 | ||
|
|
9f651646d7 | ||
|
|
d2860c80a9 | ||
|
|
e1b9ae22b6 | ||
|
|
5d64f35c96 | ||
|
|
5cfea0cd97 | ||
|
|
a165556bf5 | ||
|
|
dee2881203 | ||
|
|
da0891a5f4 | ||
|
|
1ed9796700 | ||
|
|
578a967a31 | ||
|
|
14c0c58e87 | ||
|
|
766d909a08 | ||
|
|
9c50a217da | ||
|
|
4f9db0cdb7 | ||
|
|
3c19937750 | ||
|
|
c509764014 | ||
|
|
75e06e4060 | ||
|
|
1b909f71a1 | ||
|
|
1a8764f54b | ||
|
|
9152c97de7 | ||
|
|
45cb04f928 | ||
|
|
a700750d9e | ||
|
|
07d327eb8b | ||
|
|
2275ec9573 | ||
|
|
5848b449f6 | ||
|
|
67aa32e694 | ||
|
|
dbd2961b9f | ||
|
|
b77b49c667 | ||
|
|
0eabe62bdb | ||
|
|
31e2ea2770 | ||
|
|
7a636b3b87 | ||
|
|
98ce017b4a | ||
|
|
26b04f0d66 | ||
|
|
f4f2dea688 | ||
|
|
e9a0f9a4c1 | ||
|
|
de438ae94c | ||
|
|
9ec77f20ee | ||
|
|
da01040b52 | ||
|
|
dce2a5ddb2 | ||
|
|
5c99fbabf2 | ||
|
|
9d0846c0e9 | ||
|
|
035ecfb098 | ||
|
|
3693f5aee7 | ||
|
|
b859815b07 | ||
|
|
3701ca83e4 | ||
|
|
1473f1d34c | ||
|
|
889c140fb9 | ||
|
|
c8259915f8 | ||
|
|
a2a25fb296 | ||
|
|
938a9929ea | ||
|
|
7726075b9b | ||
|
|
03b3d1bbe7 | ||
|
|
17ec51b74c | ||
|
|
e92998d1ec | ||
|
|
6691512136 | ||
|
|
a80f4ad76c | ||
|
|
2c6f4cd80c | ||
|
|
51a4c6dcf2 | ||
|
|
1f402e581a | ||
|
|
17ea92bfda | ||
|
|
6a8266af7e | ||
|
|
f6b7606648 | ||
|
|
9cd8adae93 | ||
|
|
c5ba8b75c8 | ||
|
|
3d73cc7289 | ||
|
|
11aa062ee0 | ||
|
|
b4358a9e33 | ||
|
|
0cce4714a1 | ||
|
|
9c4e6a6595 | ||
|
|
1ddd541fe9 | ||
|
|
e45aa3f2fe | ||
|
|
99efdb6061 | ||
|
|
32f0ffdc79 | ||
|
|
cf166483c9 | ||
|
|
49631867f4 | ||
|
|
9f56eaec9a | ||
|
|
04f2b732a5 | ||
|
|
b8cfabc339 | ||
|
|
1cdd39202f | ||
|
|
3290720b4c | ||
|
|
49badb06cb | ||
|
|
2c6941e73b | ||
|
|
5e5883cb88 | ||
|
|
02c504445e | ||
|
|
4d5075a792 |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<PropertyGroup><Version>0.40.3.0</Version></PropertyGroup>
|
||||
<PropertyGroup><Version>0.45.7.0</Version></PropertyGroup>
|
||||
</Project>
|
||||
|
||||
3
Resources/EnginePrototypes/UserInterface/uiThemes.yml
Normal file
3
Resources/EnginePrototypes/UserInterface/uiThemes.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
- type: uiTheme
|
||||
id: Default
|
||||
path: /textures/interface/Default
|
||||
16
Resources/Locale/en-US/client-state-commands.ftl
Normal file
16
Resources/Locale/en-US/client-state-commands.ftl
Normal file
@@ -0,0 +1,16 @@
|
||||
# Loc strings for various entity state & client-side PVS related commands
|
||||
|
||||
cmd-reset-ent-help = Usage: resetent <Entity UID>
|
||||
cmd-reset-ent-desc = Reset an entity to the most recently received server state. This will also reset entities that have been detached to null-space.
|
||||
|
||||
cmd-reset-all-ents-help = Usage: resetallents
|
||||
cmd-reset-all-ents-desc = Resets all entities to the most recently received server state. This only impacts entities that have not been detached to null-space.
|
||||
|
||||
cmd-detach-ent-help = Usage: detachent <Entity UID>
|
||||
cmd-detach-ent-desc = Detach an entity to null-space, as if it had left PVS range.
|
||||
|
||||
cmd-local-delete-help = Usage: localdelete <Entity UID>
|
||||
cmd-local-delete-desc = Deletes an entity. Unlike the normal delete command, this is CLIENT-SIDE. Unless the entity is a client-side entity, this will likely cause errors.
|
||||
|
||||
cmd-full-state-reset-help = Usage: fullstatereset
|
||||
cmd-full-state-reset-desc = Discards any entity state information and requests a full-state from the server.
|
||||
@@ -1,10 +1,14 @@
|
||||
### Localization for engine console commands
|
||||
|
||||
## generic
|
||||
## generic command errors
|
||||
|
||||
cmd-invalid-arg-number-error = Invalid number of arguments.
|
||||
|
||||
cmd-parse-failure-integer = {$arg} is not a valid integer.
|
||||
cmd-parse-failure-float = {$arg} is not a valid float.
|
||||
cmd-parse-failure-bool = {$arg} is not a valid bool.
|
||||
cmd-parse-failure-uid = {$arg} is not a valid entity UID.
|
||||
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
|
||||
|
||||
|
||||
## 'help' command
|
||||
@@ -147,6 +151,8 @@ cmd-hint-loadmap-y-position = [y-position]
|
||||
cmd-hint-loadmap-rotation = [rotation]
|
||||
cmd-hint-loadmap-uids = [float]
|
||||
|
||||
cmd-hint-savebp-id = <Grid EntityID>
|
||||
|
||||
## 'flushcookies' command
|
||||
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
@@ -20,7 +21,14 @@ namespace Robust.Client.Animations
|
||||
}
|
||||
|
||||
var entity = (EntityUid) context;
|
||||
var component = IoCManager.Resolve<IEntityManager>().GetComponent(entity, ComponentType);
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (!entManager.TryGetComponent(entity, ComponentType, out var component))
|
||||
{
|
||||
// This gets checked when the animation is first played, but the component may also be removed while the animation plays
|
||||
Logger.Error($"Couldn't find component {ComponentType} on {entManager.ToPrettyString(entity)} for animation playback!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (component is IAnimationProperties properties)
|
||||
{
|
||||
|
||||
@@ -100,6 +100,8 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
private const string FallbackSoundfont = "/Midi/fallback.sf2";
|
||||
|
||||
private const string ContentCustomSoundfontDirectory = "/Audio/MidiCustom/";
|
||||
|
||||
private const float MaxDistanceForOcclusion = 1000;
|
||||
|
||||
private static ResourcePath CustomSoundfontDirectory = new ResourcePath("/soundfonts/");
|
||||
@@ -227,7 +229,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
try
|
||||
{
|
||||
renderer.LoadSoundfont(filepath, true);
|
||||
renderer.LoadSoundfont(filepath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -240,16 +242,16 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
renderer.LoadSoundfont(OsxSoundfont, true);
|
||||
renderer.LoadSoundfont(OsxSoundfont);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
renderer.LoadSoundfont(WindowsSoundfont, true);
|
||||
renderer.LoadSoundfont(WindowsSoundfont);
|
||||
}
|
||||
|
||||
// Load content-specific custom soundfonts, which could override the system/fallback soundfont.
|
||||
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
|
||||
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls") continue;
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
|
||||
@@ -354,7 +354,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadSoundfont(string filename, bool resetPresets = false)
|
||||
public void LoadSoundfont(string filename, bool resetPresets = true)
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
@@ -521,7 +521,10 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
return;
|
||||
|
||||
_rendererState.Controllers.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Control] = midiEvent.Value;
|
||||
_synth.CC(midiEvent.Channel, midiEvent.Control, midiEvent.Value);
|
||||
if(midiEvent.Control != 0x0)
|
||||
_synth.CC(midiEvent.Channel, midiEvent.Control, midiEvent.Value);
|
||||
else // Fluidsynth doesn't seem to respect CC0 as bank selection, so we have to do it manually.
|
||||
_synth.BankSelect(midiEvent.Channel, midiEvent.Value);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.ProgramChange:
|
||||
|
||||
@@ -18,6 +18,7 @@ using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Shared;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.LoaderApi;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -13,7 +14,7 @@ namespace Robust.Client
|
||||
{
|
||||
private IGameLoop? _mainLoop;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
|
||||
private static bool _hasStarted;
|
||||
|
||||
@@ -13,6 +13,7 @@ using Robust.Client.Placement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Client.WebViewHook;
|
||||
@@ -128,10 +129,7 @@ namespace Robust.Client
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
|
||||
|
||||
_resourceCache.PreloadTextures();
|
||||
_userInterfaceManager.Initialize();
|
||||
_eyeManager.Initialize();
|
||||
_networkManager.Initialize(false);
|
||||
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
|
||||
_serializer.Initialize();
|
||||
@@ -141,16 +139,18 @@ namespace Robust.Client
|
||||
_prototypeManager.LoadDirectory(new ResourcePath("/EnginePrototypes/"));
|
||||
_prototypeManager.LoadDirectory(Options.PrototypeDirectory);
|
||||
_prototypeManager.ResolveResults();
|
||||
_userInterfaceManager.Initialize();
|
||||
_eyeManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
_mapManager.Initialize();
|
||||
_gameStateManager.Initialize();
|
||||
_placementManager.Initialize();
|
||||
_viewVariablesManager.Initialize();
|
||||
_scriptClient.Initialize();
|
||||
|
||||
_client.Initialize();
|
||||
_discord.Initialize();
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
|
||||
_userInterfaceManager.PostInitialize();
|
||||
|
||||
if (_commandLineArgs?.Username != null)
|
||||
{
|
||||
@@ -517,7 +517,7 @@ namespace Robust.Client
|
||||
using (_prof.Group("Entity"))
|
||||
{
|
||||
// The last real tick is the current tick! This way we won't be in "prediction" mode.
|
||||
_gameTiming.LastRealTick = _gameTiming.CurTick;
|
||||
_gameTiming.LastRealTick = _gameTiming.LastProcessedTick = _gameTiming.CurTick;
|
||||
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Prometheus;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -19,8 +18,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
|
||||
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
|
||||
|
||||
@@ -47,6 +45,30 @@ namespace Robust.Client.GameObjects
|
||||
base.StartEntity(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dirty(EntityUid uid)
|
||||
{
|
||||
// Client only dirties during prediction
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(uid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dirty(Component component)
|
||||
{
|
||||
// Client only dirties during prediction
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(component);
|
||||
}
|
||||
|
||||
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
|
||||
{
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
||||
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
|
||||
else
|
||||
return base.ToPrettyString(uid);
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
|
||||
public override IEntityNetworkManager EntityNetManager => this;
|
||||
@@ -67,7 +89,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
using (histogram?.WithLabels("EntityNet").NewTimer())
|
||||
{
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick)
|
||||
{
|
||||
var (_, msg) = _queue.Take();
|
||||
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
|
||||
@@ -103,7 +125,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void HandleEntityNetworkMessage(MsgEntity message)
|
||||
{
|
||||
if (message.SourceTick <= _gameStateManager.CurServerTick)
|
||||
if (message.SourceTick <= _gameTiming.LastRealTick)
|
||||
{
|
||||
DispatchMsgEntity(message);
|
||||
return;
|
||||
|
||||
@@ -136,15 +136,15 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (!(curState is EyeComponentState state))
|
||||
if (curState is not EyeComponentState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawFov = state.DrawFov;
|
||||
// TODO: Should be a way for content to override lerping and lerp the zoom
|
||||
Zoom = state.Zoom;
|
||||
Offset = state.Offset;
|
||||
Rotation = state.Rotation;
|
||||
VisibilityMask = state.VisibilityMask;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -36,6 +37,11 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IReflectionManager reflection = default!;
|
||||
[Dependency] private readonly IEyeManager eyeManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="CVars.RenderSpriteDirectionBias"/>.
|
||||
/// </summary>
|
||||
public static double DirectionBias = -0.05;
|
||||
|
||||
[DataField("visible")]
|
||||
private bool _visible = true;
|
||||
|
||||
@@ -48,7 +54,7 @@ namespace Robust.Client.GameObjects
|
||||
if (_visible == value) return;
|
||||
_visible = value;
|
||||
|
||||
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
|
||||
QueueUpdateRenderTree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +84,7 @@ namespace Robust.Client.GameObjects
|
||||
get => scale;
|
||||
set
|
||||
{
|
||||
_bounds = _bounds.Scale(value / scale);
|
||||
scale = value;
|
||||
UpdateLocalMatrix();
|
||||
}
|
||||
@@ -156,6 +163,8 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
_layerMapShared = true;
|
||||
|
||||
QueueUpdateRenderTree();
|
||||
QueueUpdateIsInert();
|
||||
}
|
||||
}
|
||||
@@ -222,7 +231,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
if (_containerOccluded == value) return;
|
||||
_containerOccluded = value;
|
||||
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
|
||||
QueueUpdateRenderTree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +256,7 @@ namespace Robust.Client.GameObjects
|
||||
/// Whether or not to pass the screen texture to the <see cref="PostShader"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Should be false unless you really need it.
|
||||
/// Should be false unless you really need it.
|
||||
/// </remarks>
|
||||
[DataField("getScreenTexture")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
@@ -634,6 +643,8 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_bounds = _bounds.Union(layer.CalculateBoundingBox());
|
||||
}
|
||||
_bounds = _bounds.Scale(Scale);
|
||||
QueueUpdateRenderTree();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1340,6 +1351,26 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
[DataField("noRot")] private bool _screenLock = false;
|
||||
|
||||
/// <summary>
|
||||
/// If the sprite only has 1 direction should it snap at cardinals if rotated.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool SnapCardinals
|
||||
{
|
||||
get => _snapCardinals;
|
||||
set
|
||||
{
|
||||
if (value == _snapCardinals)
|
||||
return;
|
||||
|
||||
_snapCardinals = value;
|
||||
RebuildBounds();
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("snapCardinals")]
|
||||
private bool _snapCardinals = false;
|
||||
|
||||
[DataField("overrideDir")]
|
||||
private Direction _overrideDirection = Direction.East;
|
||||
|
||||
@@ -1372,36 +1403,30 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection)
|
||||
{
|
||||
// Reduce the angles to fix math shenanigans
|
||||
worldRotation = worldRotation.Reduced();
|
||||
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
|
||||
var cardinal = Angle.Zero;
|
||||
|
||||
if (worldRotation.Theta < 0)
|
||||
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
|
||||
// Reduce the angles to fix math shenanigans
|
||||
angle = angle.Reduced().FlipPositive();
|
||||
|
||||
// If we have a 1-directional sprite then snap it to try and always face it south if applicable.
|
||||
if (!NoRotation && SnapCardinals)
|
||||
{
|
||||
cardinal = angle.GetCardinalDir().ToAngle();
|
||||
}
|
||||
|
||||
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
|
||||
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
|
||||
var entityMatrix = Matrix3.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation);
|
||||
var entityMatrix = Matrix3.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation - cardinal);
|
||||
|
||||
Matrix3.Multiply(in LocalMatrix, in entityMatrix, out var transform);
|
||||
|
||||
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
|
||||
foreach (var layer in Layers)
|
||||
{
|
||||
layer.Render(drawingHandle, ref transform, angle, overrideDirection);
|
||||
}
|
||||
}
|
||||
|
||||
public static Angle CalcRectWorldAngle(Angle worldAngle, int numDirections)
|
||||
{
|
||||
var theta = worldAngle.Theta;
|
||||
var segSize = (Math.PI * 2) / (numDirections * 2);
|
||||
var segments = (int)(theta / segSize);
|
||||
var odd = segments % 2;
|
||||
var result = theta - (segments * segSize) - (odd * segSize);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public int GetLayerDirectionCount(ISpriteLayer layer)
|
||||
{
|
||||
if (!layer.RsiState.IsValid)
|
||||
@@ -1470,11 +1495,9 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState == null)
|
||||
if (curState is not SpriteComponentState thestate)
|
||||
return;
|
||||
|
||||
var thestate = (SpriteComponentState)curState;
|
||||
|
||||
Visible = thestate.Visible;
|
||||
DrawDepth = thestate.DrawDepth;
|
||||
scale = thestate.Scale;
|
||||
@@ -1504,20 +1527,24 @@ namespace Robust.Client.GameObjects
|
||||
LayerDatums = thestate.Layers;
|
||||
}
|
||||
|
||||
private void QueueUpdateIsInert()
|
||||
private void QueueUpdateRenderTree()
|
||||
{
|
||||
// Look this was an easy way to get bounds checks for layer updates.
|
||||
// If you really want it optimal you'll need to comb through all 2k lines of spritecomponent.
|
||||
if ((Owner != default ? entities : null)?.EventBus != null)
|
||||
UpdateBounds();
|
||||
|
||||
if (_inertUpdateQueued)
|
||||
if (TreeUpdateQueued || Owner == default || entities?.EventBus == null)
|
||||
return;
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
TreeUpdateQueued = true;
|
||||
entities.EventBus.RaiseLocalEvent(Owner, new UpdateSpriteTreeEvent());
|
||||
}
|
||||
|
||||
private void QueueUpdateIsInert()
|
||||
{
|
||||
if (_inertUpdateQueued || Owner == default || entities?.EventBus == null)
|
||||
return;
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
_inertUpdateQueued = true;
|
||||
// Yes that null check is valid because of that stupid fucking dummy IEntity.
|
||||
// Who thought that was a good idea.
|
||||
(Owner != default ? entities : null)?.EventBus?.RaiseEvent(EventSource.Local, new SpriteUpdateInertEvent {Sprite = this});
|
||||
entities.EventBus?.RaiseEvent(EventSource.Local, new SpriteUpdateInertEvent {Sprite = this});
|
||||
}
|
||||
|
||||
internal void DoUpdateIsInert()
|
||||
@@ -1600,18 +1627,10 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
eye ??= eyeManager.CurrentEye;
|
||||
|
||||
// we need to calculate bounding box taking into account all nested layers
|
||||
// because layers can have offsets, scale or rotation, we need to calculate a new BB
|
||||
// based on lowest bottomLeft and highest topRight points from all layers
|
||||
var box = Bounds;
|
||||
|
||||
// Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We
|
||||
// could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually
|
||||
// transform our box by the combination of these matrices:
|
||||
|
||||
if (Scale != Vector2.One)
|
||||
box = box.Scale(Scale);
|
||||
|
||||
var adjustedOffset = NoRotation
|
||||
? (-eye.Rotation).RotateVec(Offset)
|
||||
: worldRotation.RotateVec(Offset);
|
||||
@@ -1621,12 +1640,7 @@ namespace Robust.Client.GameObjects
|
||||
? Rotation - eye.Rotation
|
||||
: Rotation + worldRotation;
|
||||
|
||||
return new Box2Rotated(box.Translated(position), finalRotation, position);
|
||||
}
|
||||
|
||||
internal void UpdateBounds()
|
||||
{
|
||||
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
|
||||
return new Box2Rotated(Bounds.Translated(position), finalRotation, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1710,7 +1724,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_scale = value;
|
||||
UpdateLocalMatrix();
|
||||
_parent.UpdateBounds();
|
||||
_parent.RebuildBounds();
|
||||
}
|
||||
}
|
||||
internal Vector2 _scale = Vector2.One;
|
||||
@@ -1725,7 +1739,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_rotation = value;
|
||||
UpdateLocalMatrix();
|
||||
_parent.UpdateBounds();
|
||||
_parent.RebuildBounds();
|
||||
}
|
||||
}
|
||||
internal Angle _rotation = Angle.Zero;
|
||||
@@ -1752,7 +1766,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_offset = value;
|
||||
UpdateLocalMatrix();
|
||||
_parent.UpdateBounds();
|
||||
_parent.RebuildBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1963,6 +1977,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
_parent.QueueUpdateRenderTree();
|
||||
_parent.QueueUpdateIsInert();
|
||||
}
|
||||
|
||||
@@ -2003,6 +2018,7 @@ namespace Robust.Client.GameObjects
|
||||
State = default;
|
||||
Texture = texture;
|
||||
|
||||
_parent.QueueUpdateRenderTree();
|
||||
_parent.QueueUpdateIsInert();
|
||||
}
|
||||
|
||||
@@ -2029,35 +2045,40 @@ namespace Robust.Client.GameObjects
|
||||
public Box2 CalculateBoundingBox()
|
||||
{
|
||||
var textureSize = (Vector2) PixelSize / EyeManager.PixelsPerMeter;
|
||||
|
||||
// If the parent has locked rotation and we don't have any rotation,
|
||||
// we can take the quick path of just making a box the size of the texture.
|
||||
if (_parent.NoRotation && _rotation != 0)
|
||||
{
|
||||
return Box2.CenteredAround(Offset, textureSize).Scale(_scale);
|
||||
}
|
||||
|
||||
var longestSide = MathF.Max(textureSize.X, textureSize.Y);
|
||||
var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2));
|
||||
|
||||
// Build the bounding box based on how many directions the sprite has
|
||||
var box = (_rotation != 0, _actualState) switch
|
||||
{
|
||||
// If this layer has any form of arbitrary rotation, return a bounding box big enough to cover
|
||||
// any possible rotation.
|
||||
(true, _) => Box2.CenteredAround(Offset, new Vector2(longestRotatedSide, longestRotatedSide)),
|
||||
Vector2 size;
|
||||
|
||||
// Otherwise...
|
||||
// If we have only one direction or an invalid RSI state, create a simple bounding box with the size of the texture.
|
||||
(_, {Directions: RSI.State.DirectionType.Dir1} or null) => Box2.CenteredAround(Offset, textureSize),
|
||||
// If we have four cardinal directions, take the longest side of our texture and square it, then turn that into our bounding box.
|
||||
// This accounts for all possible rotations.
|
||||
(_, {Directions: RSI.State.DirectionType.Dir4}) => Box2.CenteredAround(Offset, new Vector2(longestSide, longestSide)),
|
||||
// If we have eight directions, find the maximum length of the texture (accounting for rotation), then square it to make
|
||||
// our bounding box.
|
||||
(_, {Directions: RSI.State.DirectionType.Dir8}) => Box2.CenteredAround(Offset, new Vector2(longestRotatedSide, longestRotatedSide)),
|
||||
};
|
||||
return _scale == Vector2.One ? box : box.Scale(_scale);
|
||||
// If this layer has any form of arbitrary rotation, return a bounding box big enough to cover
|
||||
// any possible rotation.
|
||||
if (_rotation != 0)
|
||||
{
|
||||
size = new Vector2(longestRotatedSide, longestRotatedSide);
|
||||
}
|
||||
else if (_parent.SnapCardinals)
|
||||
{
|
||||
DebugTools.Assert(_actualState == null || _actualState.Directions == RSI.State.DirectionType.Dir1);
|
||||
size = new Vector2(longestSide, longestSide);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Build the bounding box based on how many directions the sprite has
|
||||
size = (_actualState?.Directions) switch
|
||||
{
|
||||
// If we have four cardinal directions, take the longest side of our texture and square it, then turn that into our bounding box.
|
||||
// This accounts for all possible rotations.
|
||||
RSI.State.DirectionType.Dir4 => new Vector2(longestSide, longestSide),
|
||||
|
||||
// If we have eight directions, find the maximum length of the texture (accounting for rotation), then square it to make
|
||||
RSI.State.DirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide),
|
||||
|
||||
// If we have only one direction or an invalid RSI state, create a simple bounding box with the size of the texture.
|
||||
_ => textureSize
|
||||
};
|
||||
}
|
||||
|
||||
return Box2.CenteredAround(Offset, size * _scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2106,14 +2127,43 @@ namespace Robust.Client.GameObjects
|
||||
Matrix3.CreateRotation(-Direction.NorthWest.ToAngle())
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts an angle (between 0 and 2pi) to an RSI direction. This will slightly bias the angle to avoid flickering for
|
||||
/// 4-directional sprites.
|
||||
/// </summary>
|
||||
public static RSIDirection GetDirection(RSI.State.DirectionType dirType, Angle angle)
|
||||
{
|
||||
if (dirType == RSI.State.DirectionType.Dir1)
|
||||
return RSIDirection.South;
|
||||
else if (dirType == RSI.State.DirectionType.Dir8)
|
||||
return angle.GetDir().Convert(dirType);
|
||||
|
||||
// For 4-directional sprites, as entities are often moving & facing diagonally, we will slightly bias the
|
||||
// angle to avoid the sprite flickering.
|
||||
|
||||
// mod is -0.5 for angles between 0-90 and 180-270, and +0.5 for 90-180 and 270-360
|
||||
var mod = (Math.Floor(angle.Theta / MathHelper.PiOver2) % 2) - 0.5;
|
||||
|
||||
var modTheta = angle.Theta + mod * DirectionBias;
|
||||
|
||||
return ((int)Math.Round(modTheta / MathHelper.PiOver2) % 4) switch
|
||||
{
|
||||
0 => RSIDirection.South,
|
||||
1 => RSIDirection.East,
|
||||
2 => RSIDirection.North,
|
||||
_ => RSIDirection.West,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render a layer. This assumes that the input angle is between 0 and 2pi.
|
||||
/// </summary>
|
||||
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix, Angle angle, Direction? overrideDirection)
|
||||
{
|
||||
if (!Visible || Blank)
|
||||
return;
|
||||
|
||||
var dir = (_actualState == null || _actualState.Directions == RSI.State.DirectionType.Dir1)
|
||||
? RSIDirection.South
|
||||
: angle.ToRsiDirection(_actualState.Directions);
|
||||
var dir = _actualState == null ? RSIDirection.South : GetDirection(_actualState.Directions, angle);
|
||||
|
||||
// Set the drawing transform for this layer
|
||||
GetLayerDrawMatrix(dir, out var layerMatrix);
|
||||
@@ -2285,7 +2335,8 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SpriteUpdateEvent : EntityEventArgs
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
internal sealed class UpdateSpriteTreeEvent : EntityEventArgs
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -17,14 +16,12 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _netMan = default!;
|
||||
|
||||
private readonly Dictionary<object, BoundUserInterface> _openInterfaces =
|
||||
internal readonly Dictionary<Enum, BoundUserInterface> _openInterfaces =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<object, PrototypeData> _interfaces = new();
|
||||
|
||||
[DataField("interfaces", readOnly: true)]
|
||||
private List<PrototypeData> _interfaceData = new();
|
||||
internal readonly Dictionary<Enum, PrototypeData> _interfaces = new();
|
||||
|
||||
[ViewVariables]
|
||||
public IEnumerable<BoundUserInterface> Interfaces => _openInterfaces.Values;
|
||||
@@ -72,7 +69,7 @@ namespace Robust.Client.GameObjects
|
||||
// TODO: This type should be cached, but I'm too lazy.
|
||||
var type = _reflectionManager.LooseGetType(data.ClientType);
|
||||
var boundInterface =
|
||||
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
|
||||
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {this, wrapped.UiKey});
|
||||
boundInterface.Open();
|
||||
_openInterfaces[wrapped.UiKey] = boundInterface;
|
||||
|
||||
@@ -81,7 +78,7 @@ namespace Robust.Client.GameObjects
|
||||
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIOpenedEvent(wrapped.UiKey, Owner, playerSession), true);
|
||||
}
|
||||
|
||||
internal void Close(object uiKey, bool remoteCall)
|
||||
internal void Close(Enum uiKey, bool remoteCall)
|
||||
{
|
||||
if (!_openInterfaces.TryGetValue(uiKey, out var boundUserInterface))
|
||||
{
|
||||
@@ -98,10 +95,9 @@ namespace Robust.Client.GameObjects
|
||||
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIClosedEvent(uiKey, Owner, playerSession), true);
|
||||
}
|
||||
|
||||
internal void SendMessage(BoundUserInterfaceMessage message, object uiKey)
|
||||
internal void SendMessage(BoundUserInterfaceMessage message, Enum uiKey)
|
||||
{
|
||||
EntitySystem.Get<UserInterfaceSystem>()
|
||||
.Send(new BoundUIWrapMessage(Owner, message, uiKey));
|
||||
_netMan.SendSystemNetworkMessage(new BoundUIWrapMessage(Owner, message, uiKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +107,15 @@ namespace Robust.Client.GameObjects
|
||||
public abstract class BoundUserInterface : IDisposable
|
||||
{
|
||||
protected ClientUserInterfaceComponent Owner { get; }
|
||||
protected object UiKey { get; }
|
||||
|
||||
public readonly Enum UiKey;
|
||||
|
||||
/// <summary>
|
||||
/// The last received state object sent from the server.
|
||||
/// </summary>
|
||||
protected BoundUserInterfaceState? State { get; private set; }
|
||||
|
||||
protected BoundUserInterface(ClientUserInterfaceComponent owner, object uiKey)
|
||||
protected BoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey)
|
||||
{
|
||||
Owner = owner;
|
||||
UiKey = uiKey;
|
||||
@@ -149,7 +146,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Invoked to close the UI.
|
||||
/// </summary>
|
||||
protected void Close()
|
||||
public void Close()
|
||||
{
|
||||
Owner.Close(UiKey, false);
|
||||
}
|
||||
@@ -157,7 +154,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Sends a message to the server-side UI.
|
||||
/// </summary>
|
||||
protected void SendMessage(BoundUserInterfaceMessage message)
|
||||
public void SendMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
Owner.SendMessage(message, UiKey);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -9,6 +11,8 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
private readonly List<AnimationPlayerComponent> _activeAnimations = new();
|
||||
|
||||
[Dependency] private readonly IComponentFactory _compFact = default!;
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
for (var i = _activeAnimations.Count - 1; i >= 0; i--)
|
||||
@@ -76,6 +80,39 @@ namespace Robust.Client.GameObjects
|
||||
AddComponent(component);
|
||||
var playback = new AnimationPlaybackShared.AnimationPlayback(animation);
|
||||
|
||||
#if DEBUG
|
||||
// Component networking checks
|
||||
foreach (var track in animation.AnimationTracks)
|
||||
{
|
||||
if (track is not AnimationTrackComponentProperty compTrack)
|
||||
continue;
|
||||
|
||||
if (compTrack.ComponentType == null)
|
||||
{
|
||||
Logger.Error($"Attempted to play a component animation without any component specified.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(component.Owner, compTrack.ComponentType, out var animatedComp))
|
||||
{
|
||||
Logger.Error(
|
||||
$"Attempted to play a component animation, but the entity {ToPrettyString(component.Owner)} does not have the component to be animated: {compTrack.ComponentType}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.Owner.IsClientSide() || !animatedComp.NetSyncEnabled)
|
||||
continue;
|
||||
|
||||
var reg = _compFact.GetRegistration(animatedComp);
|
||||
|
||||
// In principle there is nothing wrong with this, as long as the property of the component being
|
||||
// animated is not part of the networked state and setting it does not dirty the component. Hence only a
|
||||
// warning in debug mode.
|
||||
if (reg.NetID != null)
|
||||
Logger.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(component.Owner)}");
|
||||
}
|
||||
#endif
|
||||
|
||||
component.PlayingAnimations.Add(key, playback);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,16 +96,21 @@ namespace Robust.Client.GameObjects
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
var spriteQuery = GetEntityQuery<SpriteComponent>();
|
||||
var metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
while (_queuedUpdates.TryDequeue(out var appearance))
|
||||
{
|
||||
if (appearance.Deleted)
|
||||
continue;
|
||||
|
||||
UnmarkDirty(appearance);
|
||||
|
||||
// If the entity is no longer within the clients PVS, don't bother updating.
|
||||
if ((metaQuery.GetComponent(appearance.Owner).Flags & MetaDataFlags.Detached) != 0)
|
||||
continue;
|
||||
|
||||
// Sprite comp is allowed to be null, so that things like spriteless point-lights can use this system
|
||||
spriteQuery.TryGetComponent(appearance.Owner, out var sprite);
|
||||
|
||||
OnChangeData(appearance.Owner, sprite, appearance);
|
||||
UnmarkDirty(appearance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -475,9 +475,9 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
|
||||
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
if (_timing.IsFirstTimePredicted || sound == null)
|
||||
return Play(sound, Filter.Local(), source, audioParams);
|
||||
else
|
||||
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
|
||||
@@ -503,7 +503,7 @@ namespace Robust.Client.GameObjects
|
||||
source.IsLooping = audioParams.Value.Loop;
|
||||
}
|
||||
|
||||
private sealed class PlayingStream : IPlayingAudioStream
|
||||
public sealed class PlayingStream : IPlayingAudioStream
|
||||
{
|
||||
public uint? NetIdentifier;
|
||||
public IClydeAudioSource Source = default!;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -9,6 +11,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Containers.ContainerManagerComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -34,11 +37,9 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void HandleEntityInitialized(EntityInitializedMessage ev)
|
||||
{
|
||||
if (!ExpectedEntities.TryGetValue(ev.Entity, out var container))
|
||||
if (!RemoveExpectedEntity(ev.Entity, out var container))
|
||||
return;
|
||||
|
||||
RemoveExpectedEntity(ev.Entity);
|
||||
|
||||
if (container.Deleted)
|
||||
return;
|
||||
|
||||
@@ -54,18 +55,12 @@ namespace Robust.Client.GameObjects
|
||||
var toDelete = new ValueList<string>();
|
||||
foreach (var (id, container) in component.Containers)
|
||||
{
|
||||
// TODO: This is usually O(n^2) to the amount of containers.
|
||||
foreach (var stateContainer in cast.ContainerSet)
|
||||
{
|
||||
if (stateContainer.Id == id)
|
||||
goto skip;
|
||||
}
|
||||
if (cast.Containers.ContainsKey(id))
|
||||
continue;
|
||||
|
||||
container.EmptyContainer(true, entMan: EntityManager);
|
||||
EmptyContainer(container, true);
|
||||
container.Shutdown();
|
||||
toDelete.Add(id);
|
||||
|
||||
skip: ;
|
||||
}
|
||||
|
||||
foreach (var dead in toDelete)
|
||||
@@ -75,7 +70,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// Add new containers and update existing contents.
|
||||
|
||||
foreach (var (containerType, id, showEnts, occludesLight, entityUids) in cast.ContainerSet)
|
||||
foreach (var (containerType, id, showEnts, occludesLight, entityUids) in cast.Containers.Values)
|
||||
{
|
||||
if (!component.Containers.TryGetValue(id, out var container))
|
||||
{
|
||||
@@ -114,13 +109,13 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
foreach (var entityUid in removedExpected)
|
||||
{
|
||||
RemoveExpectedEntity(entityUid);
|
||||
RemoveExpectedEntity(entityUid, out _);
|
||||
}
|
||||
|
||||
// Add new entities.
|
||||
foreach (var entity in entityUids)
|
||||
{
|
||||
if (!EntityManager.EntityExists(entity))
|
||||
if (!EntityManager.TryGetComponent(entity, out MetaDataComponent? meta))
|
||||
{
|
||||
AddExpectedEntity(entity, container);
|
||||
continue;
|
||||
@@ -133,14 +128,17 @@ namespace Robust.Client.GameObjects
|
||||
// from the container. It would then subsequently be parented to the container without ever being
|
||||
// re-inserted, leading to the client seeing what should be hidden entities attached to
|
||||
// containers/players.
|
||||
if (Transform(entity).MapID == MapId.Nullspace)
|
||||
if ((meta.Flags & MetaDataFlags.Detached) != 0)
|
||||
{
|
||||
AddExpectedEntity(entity, container);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!container.ContainedEntities.Contains(entity))
|
||||
container.Insert(entity);
|
||||
if (container.Contains(entity))
|
||||
continue;
|
||||
|
||||
RemoveExpectedEntity(entity, out _);
|
||||
container.Insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +156,7 @@ namespace Robust.Client.GameObjects
|
||||
if (message.OldParent != null && message.OldParent.Value.IsValid())
|
||||
return;
|
||||
|
||||
if (!ExpectedEntities.TryGetValue(message.Entity, out var container))
|
||||
if (!RemoveExpectedEntity(message.Entity, out var container))
|
||||
return;
|
||||
|
||||
if (xform.ParentUid != container.Owner)
|
||||
@@ -168,8 +166,6 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveExpectedEntity(message.Entity);
|
||||
|
||||
if (container.Deleted)
|
||||
return;
|
||||
|
||||
@@ -189,20 +185,35 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public void AddExpectedEntity(EntityUid uid, IContainer container)
|
||||
{
|
||||
if (ExpectedEntities.ContainsKey(uid))
|
||||
return;
|
||||
DebugTools.Assert(!TryComp(uid, out MetaDataComponent? meta) ||
|
||||
(meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
|
||||
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");
|
||||
|
||||
ExpectedEntities.Add(uid, container);
|
||||
if (!ExpectedEntities.TryAdd(uid, container))
|
||||
{
|
||||
// It is possible that we were expecting this entity in one container, but it has now moved to another
|
||||
// container, and this entity's state is just being applied before the old container is getting updated.
|
||||
var oldContainer = ExpectedEntities[uid];
|
||||
ExpectedEntities[uid] = container;
|
||||
DebugTools.Assert(oldContainer.ExpectedEntities.Contains(uid),
|
||||
$"Entity {ToPrettyString(uid)} is expected, but not expected in the given container? Container: {oldContainer.ID} in {ToPrettyString(oldContainer.Owner)}");
|
||||
oldContainer.ExpectedEntities.Remove(uid);
|
||||
}
|
||||
|
||||
DebugTools.Assert(!container.ExpectedEntities.Contains(uid),
|
||||
$"Contained entity {ToPrettyString(uid)} was not yet expected by the system, but was already expected by the container: {container.ID} in {ToPrettyString(container.Owner)}");
|
||||
container.ExpectedEntities.Add(uid);
|
||||
}
|
||||
|
||||
public void RemoveExpectedEntity(EntityUid uid)
|
||||
public bool RemoveExpectedEntity(EntityUid uid, [NotNullWhen(true)] out IContainer? container)
|
||||
{
|
||||
if (!ExpectedEntities.TryGetValue(uid, out var container))
|
||||
return;
|
||||
if (!ExpectedEntities.Remove(uid, out container))
|
||||
return false;
|
||||
|
||||
ExpectedEntities.Remove(uid);
|
||||
DebugTools.Assert(container.ExpectedEntities.Contains(uid),
|
||||
$"While removing expected contained entity {ToPrettyString(uid)}, the entity was missing from the container expected set. Container: {container.ID} in {ToPrettyString(container.Owner)}");
|
||||
container.ExpectedEntities.Remove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
|
||||
@@ -26,26 +26,11 @@ namespace Robust.Client.GameObjects
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
//WARN: Tightly couples this system with InputSystem, and assumes InputSystem exists and is initialized
|
||||
CommandBinds.Builder
|
||||
.Bind(EngineKeyFunctions.CameraRotateRight, new NullInputCmdHandler())
|
||||
.Bind(EngineKeyFunctions.CameraRotateLeft, new NullInputCmdHandler())
|
||||
.Register<EyeUpdateSystem>();
|
||||
|
||||
// Make sure this runs *after* entities have been moved by interpolation and movement.
|
||||
UpdatesAfter.Add(typeof(TransformSystem));
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Shutdown()
|
||||
{
|
||||
//WARN: Tightly couples this system with InputSystem, and assumes InputSystem exists and is initialized
|
||||
CommandBinds.Unregister<EyeUpdateSystem>();
|
||||
base.Shutdown();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
SubscribeLocalEvent<SpriteComponent, EntParentChangedMessage>(SpriteParentChanged);
|
||||
SubscribeLocalEvent<SpriteComponent, ComponentRemove>(RemoveSprite);
|
||||
SubscribeLocalEvent<SpriteComponent, SpriteUpdateEvent>(HandleSpriteUpdate);
|
||||
SubscribeLocalEvent<SpriteComponent, UpdateSpriteTreeEvent>(HandleSpriteUpdate);
|
||||
|
||||
SubscribeLocalEvent<PointLightComponent, EntParentChangedMessage>(LightParentChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
|
||||
@@ -107,10 +107,9 @@ namespace Robust.Client.GameObjects
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void HandleSpriteUpdate(EntityUid uid, SpriteComponent component, SpriteUpdateEvent args)
|
||||
private void HandleSpriteUpdate(EntityUid uid, SpriteComponent component, UpdateSpriteTreeEvent args)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
QueueSpriteUpdate(component);
|
||||
_spriteQueue.Add(component);
|
||||
}
|
||||
|
||||
private void AnythingMoved(ref MoveEvent args)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
@@ -15,6 +18,7 @@ namespace Robust.Client.GameObjects
|
||||
public sealed partial class SpriteSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly RenderingTreeSystem _treeSystem = default!;
|
||||
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
@@ -26,12 +30,19 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_proto.PrototypesReloaded += OnPrototypesReloaded;
|
||||
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
|
||||
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_proto.PrototypesReloaded -= OnPrototypesReloaded;
|
||||
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
|
||||
}
|
||||
|
||||
private void OnBiasChanged(double value)
|
||||
{
|
||||
SpriteComponent.DirectionBias = value;
|
||||
}
|
||||
|
||||
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JetBrains.Annotations;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameStates;
|
||||
@@ -15,68 +11,60 @@ namespace Robust.Client.GameStates;
|
||||
/// </summary>
|
||||
internal sealed class ClientDirtySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
[Dependency] private readonly IComponentFactory _compFact = default!;
|
||||
|
||||
// Entities that have removed networked components
|
||||
// could pool the ushort sets, but predicted component changes are rare... soo...
|
||||
internal readonly Dictionary<EntityUid, HashSet<ushort>> RemovedComponents = new();
|
||||
|
||||
private readonly Dictionary<GameTick, HashSet<EntityUid>> _dirtyEntities = new();
|
||||
|
||||
private ObjectPool<HashSet<EntityUid>> _dirtyPool =
|
||||
new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), 64);
|
||||
|
||||
// Keep it out of the pool because it's probably going to be a lot bigger.
|
||||
private HashSet<EntityUid> _dirty = new(256);
|
||||
internal readonly HashSet<EntityUid> DirtyEntities = new(256);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
EntityManager.EntityDirtied += OnEntityDirty;
|
||||
EntityManager.ComponentRemoved += OnCompRemoved;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
EntityManager.EntityDirtied -= OnEntityDirty;
|
||||
_dirtyEntities.Clear();
|
||||
EntityManager.ComponentRemoved -= OnCompRemoved;
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void OnCompRemoved(RemovedComponentEventArgs args)
|
||||
{
|
||||
var comp = args.BaseArgs.Component;
|
||||
if (!_timing.InPrediction || comp.Owner.IsClientSide() || !comp.NetSyncEnabled)
|
||||
return;
|
||||
|
||||
// Was this component added during prediction? If yes, then there is no need to re-add it when resetting.
|
||||
if (comp.CreationTick > _timing.LastRealTick)
|
||||
return;
|
||||
|
||||
// TODO if entity deletion ever gets predicted, then to speed this function up the component removal event
|
||||
// should probably get an arg that specifies whether removal is occurring because of entity deletion. AKA: I
|
||||
// don't want to have to fetch the meta-data component 10+ times for each entity that gets deleted. Currently
|
||||
// server-induced deletions should get ignored, as _timing.InPrediction will be false while applying game
|
||||
// states.
|
||||
|
||||
var netId = _compFact.GetRegistration(comp).NetID;
|
||||
if (netId != null)
|
||||
RemovedComponents.GetOrNew(comp.Owner).Add(netId.Value);
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
foreach (var (_, sets) in _dirtyEntities)
|
||||
{
|
||||
sets.Clear();
|
||||
_dirtyPool.Return(sets);
|
||||
}
|
||||
|
||||
_dirtyEntities.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> GetDirtyEntities(GameTick currentTick)
|
||||
{
|
||||
_dirty.Clear();
|
||||
|
||||
// This is just to avoid collection being modified during iteration unfortunately.
|
||||
foreach (var (tick, dirty) in _dirtyEntities)
|
||||
{
|
||||
if (tick < currentTick) continue;
|
||||
foreach (var ent in dirty)
|
||||
{
|
||||
_dirty.Add(ent);
|
||||
}
|
||||
}
|
||||
|
||||
return _dirty;
|
||||
DirtyEntities.Clear();
|
||||
RemovedComponents.Clear();
|
||||
}
|
||||
|
||||
private void OnEntityDirty(EntityUid e)
|
||||
{
|
||||
if (e.IsClientSide()) return;
|
||||
|
||||
var tick = _timing.CurTick;
|
||||
if (!_dirtyEntities.TryGetValue(tick, out var ents))
|
||||
{
|
||||
ents = _dirtyPool.Get();
|
||||
_dirtyEntities[tick] = ents;
|
||||
}
|
||||
|
||||
ents.Add(e);
|
||||
if (_timing.InPrediction && !e.IsClientSide())
|
||||
DirtyEntities.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -12,154 +14,149 @@ namespace Robust.Client.GameStates
|
||||
/// <inheritdoc />
|
||||
internal sealed class GameStateProcessor : IGameStateProcessor
|
||||
{
|
||||
private readonly IGameTiming _timing;
|
||||
private readonly IClientGameTiming _timing;
|
||||
|
||||
private readonly List<GameState> _stateBuffer = new();
|
||||
private GameState? _lastFullState;
|
||||
private bool _waitingForFull = true;
|
||||
private int _interpRatio;
|
||||
private GameTick _highestFromSequence;
|
||||
|
||||
private readonly Dictionary<EntityUid, Dictionary<uint, ComponentState>> _lastStateFullRep
|
||||
private readonly Dictionary<GameTick, List<EntityUid>> _pvsDetachMessages = new();
|
||||
|
||||
public GameState? LastFullState { get; private set; }
|
||||
public bool WaitingForFull => LastFullStateRequested.HasValue;
|
||||
public GameTick? LastFullStateRequested
|
||||
{
|
||||
get => _lastFullStateRequested;
|
||||
set
|
||||
{
|
||||
_lastFullStateRequested = value;
|
||||
LastFullState = null;
|
||||
}
|
||||
}
|
||||
|
||||
public GameTick? _lastFullStateRequested = GameTick.Zero;
|
||||
|
||||
private int _bufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
|
||||
/// </summary>
|
||||
internal readonly Dictionary<EntityUid, Dictionary<ushort, ComponentState>> _lastStateFullRep
|
||||
= new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinBufferSize => Interpolation ? 3 : 2;
|
||||
public int MinBufferSize => Interpolation ? 2 : 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TargetBufferSize => MinBufferSize + InterpRatio;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentBufferSize => CalculateBufferSize(_timing.CurTick);
|
||||
public int TargetBufferSize => MinBufferSize + BufferSize;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Interpolation { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int InterpRatio
|
||||
public int BufferSize
|
||||
{
|
||||
get => _interpRatio;
|
||||
set => _interpRatio = value < 0 ? 0 : value;
|
||||
get => _bufferSize;
|
||||
set => _bufferSize = value < 0 ? 0 : value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Extrapolation { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Logging { get; set; }
|
||||
|
||||
public GameTick LastProcessedRealState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
|
||||
/// </summary>
|
||||
/// <param name="timing">Timing information of the current state.</param>
|
||||
public GameStateProcessor(IGameTiming timing)
|
||||
public GameStateProcessor(IClientGameTiming timing)
|
||||
{
|
||||
_timing = timing;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddNewState(GameState state)
|
||||
public bool AddNewState(GameState state)
|
||||
{
|
||||
// any state from tick 0 is a full state, and needs to be handled different
|
||||
if (state.FromSequence == GameTick.Zero)
|
||||
{
|
||||
// this is a newer full state, so discard the older one.
|
||||
if (_lastFullState == null || (_lastFullState != null && _lastFullState.ToSequence < state.ToSequence))
|
||||
{
|
||||
_lastFullState = state;
|
||||
|
||||
if (Logging)
|
||||
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: DispatchTick will be modifying CurTick, this is NOT thread safe.
|
||||
var lastTick = new GameTick(_timing.CurTick.Value - 1);
|
||||
|
||||
if (state.ToSequence <= lastTick && !_waitingForFull) // CurTick isn't set properly when WaitingForFull
|
||||
// Check for old states.
|
||||
if (state.ToSequence <= _timing.LastRealTick)
|
||||
{
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
Logger.DebugS("net.state", $"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// lets check for a duplicate state now.
|
||||
for (var i = 0; i < _stateBuffer.Count; i++)
|
||||
// Check for a duplicate states.
|
||||
foreach (var bufferState in _stateBuffer)
|
||||
{
|
||||
var iState = _stateBuffer[i];
|
||||
|
||||
if (state.ToSequence != iState.ToSequence)
|
||||
if (state.ToSequence != bufferState.ToSequence)
|
||||
continue;
|
||||
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
Logger.DebugS("net.state", $"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Are we expecting a full state?
|
||||
if (!WaitingForFull)
|
||||
{
|
||||
// This is a good state that we will be using.
|
||||
_stateBuffer.Add(state);
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// this is a good state that we will be using.
|
||||
_stateBuffer.Add(state);
|
||||
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
|
||||
{
|
||||
LastFullState = state;
|
||||
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
if (Logging)
|
||||
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
|
||||
{
|
||||
if (Logging)
|
||||
Logger.InfoS("net", $"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_stateBuffer.Add(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
/// <summary>
|
||||
/// Attempts to get the current and next states to apply.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the processor is not currently waiting for a full state, the states to apply depends on <see
|
||||
/// cref="IGameTiming.LastProcessedTick"/>.
|
||||
/// </remarks>
|
||||
/// <returns>Returns true if the states should be applied.</returns>
|
||||
public bool TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
{
|
||||
bool applyNextState;
|
||||
if (_waitingForFull)
|
||||
{
|
||||
applyNextState = CalculateFullState(out curState, out nextState, TargetBufferSize);
|
||||
}
|
||||
else // this will be how almost all states are calculated
|
||||
{
|
||||
applyNextState = CalculateDeltaState(curTick, out curState, out nextState);
|
||||
}
|
||||
var applyNextState = WaitingForFull
|
||||
? TryGetFullState(out curState, out nextState)
|
||||
: TryGetDeltaState(out curState, out nextState);
|
||||
|
||||
if (applyNextState && !curState!.Extrapolated)
|
||||
LastProcessedRealState = curState.ToSequence;
|
||||
|
||||
if (!_waitingForFull)
|
||||
if (curState != null)
|
||||
{
|
||||
if (!applyNextState)
|
||||
_timing.CurTick = LastProcessedRealState;
|
||||
|
||||
// This will slightly speed up or slow down the client tickrate based on the contents of the buffer.
|
||||
// CalcNextState should have just cleaned out any old states, so the buffer contains [t-1(last), t+0(cur), t+1(next), t+2, t+3, ..., t+n]
|
||||
// we can use this info to properly time our tickrate so it does not run fast or slow compared to the server.
|
||||
_timing.TickTimingAdjustment = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f;
|
||||
}
|
||||
else
|
||||
{
|
||||
_timing.TickTimingAdjustment = 0f;
|
||||
}
|
||||
|
||||
if (applyNextState)
|
||||
{
|
||||
DebugTools.Assert(curState!.Extrapolated || curState.FromSequence <= LastProcessedRealState,
|
||||
DebugTools.Assert(curState.FromSequence <= curState.ToSequence,
|
||||
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
|
||||
|
||||
if (Logging)
|
||||
{
|
||||
Logger.DebugS("net.state", $"Applying State: ext={curState!.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
|
||||
}
|
||||
Logger.DebugS("net.state", $"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
|
||||
}
|
||||
|
||||
var cState = curState!;
|
||||
curState = cState;
|
||||
|
||||
return applyNextState;
|
||||
}
|
||||
|
||||
public void UpdateFullRep(GameState state)
|
||||
{
|
||||
// Logger.Debug($"UPDATE FULL REP: {string.Join(", ", state.EntityStates?.Select(e => e.Uid) ?? Enumerable.Empty<EntityUid>())}");
|
||||
// Note: the most recently received server state currently doesn't include pvs-leave messages (detaching
|
||||
// transform to null-space). This is because a client should never predict an entity being moved back from
|
||||
// null-space, so there should be no need to reset it back there.
|
||||
|
||||
if (state.FromSequence == GameTick.Zero)
|
||||
{
|
||||
@@ -178,7 +175,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
|
||||
{
|
||||
compData = new Dictionary<uint, ComponentState>();
|
||||
compData = new Dictionary<ushort, ComponentState>();
|
||||
_lastStateFullRep.Add(entityState.Uid, compData);
|
||||
}
|
||||
|
||||
@@ -196,167 +193,138 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
private bool CalculateFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState, int targetBufferSize)
|
||||
private bool TryGetFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
{
|
||||
if (_lastFullState != null)
|
||||
nextState = null;
|
||||
curState = null;
|
||||
|
||||
if (LastFullState == null)
|
||||
return false;
|
||||
|
||||
// remove any old states we find to keep the buffer clean
|
||||
// also look for the next state if we are interpolating.
|
||||
var nextTick = LastFullState.ToSequence + 1;
|
||||
for (var i = 0; i < _stateBuffer.Count; i++)
|
||||
{
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Resync CurTick to: {_lastFullState.ToSequence}");
|
||||
var state = _stateBuffer[i];
|
||||
|
||||
var curTick = _timing.CurTick = _lastFullState.ToSequence;
|
||||
|
||||
if (Interpolation)
|
||||
if (state.ToSequence < LastFullState.ToSequence)
|
||||
{
|
||||
// look for the next state
|
||||
var lastTick = new GameTick(curTick.Value - 1);
|
||||
var nextTick = new GameTick(curTick.Value + 1);
|
||||
nextState = null;
|
||||
|
||||
for (var i = 0; i < _stateBuffer.Count; i++)
|
||||
{
|
||||
var state = _stateBuffer[i];
|
||||
if (state.ToSequence == nextTick)
|
||||
{
|
||||
nextState = state;
|
||||
}
|
||||
else if (state.ToSequence < lastTick) // remove any old states we find to keep the buffer clean
|
||||
{
|
||||
_stateBuffer.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// we let the buffer fill up before starting to tick
|
||||
if (nextState != null && _stateBuffer.Count >= targetBufferSize)
|
||||
{
|
||||
curState = _lastFullState;
|
||||
_waitingForFull = false;
|
||||
return true;
|
||||
}
|
||||
_stateBuffer.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
else if (_stateBuffer.Count >= targetBufferSize)
|
||||
else if (Interpolation && state.ToSequence == nextTick)
|
||||
{
|
||||
curState = _lastFullState;
|
||||
nextState = default;
|
||||
_waitingForFull = false;
|
||||
return true;
|
||||
nextState = state;
|
||||
}
|
||||
}
|
||||
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{targetBufferSize})");
|
||||
// we let the buffer fill up before starting to tick
|
||||
if (_stateBuffer.Count >= TargetBufferSize)
|
||||
{
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Resync CurTick to: {LastFullState.ToSequence}");
|
||||
|
||||
// waiting for full state or buffer to fill
|
||||
curState = default;
|
||||
nextState = default;
|
||||
curState = LastFullState;
|
||||
return true;
|
||||
}
|
||||
|
||||
// waiting for buffer to fill
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{TargetBufferSize})");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CalculateDeltaState(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
internal void AddLeavePvsMessage(MsgStateLeavePvs message)
|
||||
{
|
||||
var lastTick = new GameTick(curTick.Value - 1);
|
||||
var nextTick = new GameTick(curTick.Value + 1);
|
||||
// Late message may still need to be processed,
|
||||
DebugTools.Assert(message.Entities.Count > 0);
|
||||
_pvsDetachMessages.TryAdd(message.Tick, message.Entities);
|
||||
}
|
||||
|
||||
public List<(GameTick Tick, List<EntityUid> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
|
||||
{
|
||||
var result = new List<(GameTick Tick, List<EntityUid> Entities)>();
|
||||
foreach (var (tick, entities) in _pvsDetachMessages)
|
||||
{
|
||||
if (tick > toTick)
|
||||
continue;
|
||||
|
||||
if (budget >= entities.Count)
|
||||
{
|
||||
budget -= entities.Count;
|
||||
_pvsDetachMessages.Remove(tick);
|
||||
result.Add((tick, entities));
|
||||
continue;
|
||||
}
|
||||
|
||||
var index = entities.Count - budget;
|
||||
result.Add((tick, entities.GetRange(index, budget)));
|
||||
entities.RemoveRange(index, budget);
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryGetDeltaState(out GameState? curState, out GameState? nextState)
|
||||
{
|
||||
curState = null;
|
||||
nextState = null;
|
||||
|
||||
var targetCurTick = _timing.LastProcessedTick + 1;
|
||||
var targetNextTick = _timing.LastProcessedTick + 2;
|
||||
|
||||
GameTick? futureStateLowestFromSeq = null;
|
||||
uint lastStateInput = 0;
|
||||
|
||||
for (var i = 0; i < _stateBuffer.Count; i++)
|
||||
{
|
||||
var state = _stateBuffer[i];
|
||||
|
||||
// remember there are no duplicate ToSequence states in the list.
|
||||
if (state.ToSequence == curTick)
|
||||
if (state.ToSequence == targetCurTick && state.FromSequence <= _timing.LastRealTick)
|
||||
{
|
||||
curState = state;
|
||||
_highestFromSequence = state.FromSequence;
|
||||
continue;
|
||||
}
|
||||
else if (Interpolation && state.ToSequence == nextTick)
|
||||
{
|
||||
|
||||
if (Interpolation && state.ToSequence == targetNextTick)
|
||||
nextState = state;
|
||||
|
||||
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
|
||||
{
|
||||
futureStateLowestFromSeq = state.FromSequence;
|
||||
}
|
||||
}
|
||||
else if (state.ToSequence > curTick)
|
||||
if (state.ToSequence > targetCurTick && (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence))
|
||||
{
|
||||
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
|
||||
{
|
||||
futureStateLowestFromSeq = state.FromSequence;
|
||||
}
|
||||
futureStateLowestFromSeq = state.FromSequence;
|
||||
continue;
|
||||
}
|
||||
else if (state.ToSequence == lastTick)
|
||||
{
|
||||
lastStateInput = state.LastProcessedInput;
|
||||
}
|
||||
else if (state.ToSequence < _highestFromSequence) // remove any old states we find to keep the buffer clean
|
||||
|
||||
// remove any old states we find to keep the buffer clean
|
||||
if (state.ToSequence <= _timing.LastRealTick)
|
||||
{
|
||||
_stateBuffer.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we can ACTUALLY apply this state.
|
||||
// Can happen that we can't if there is a hole and we're doing extrapolation.
|
||||
if (curState != null && curState.FromSequence > LastProcessedRealState)
|
||||
curState = null;
|
||||
|
||||
// can't find current state, but we do have a future state.
|
||||
if (!Extrapolation && curState == null && futureStateLowestFromSeq != null
|
||||
&& futureStateLowestFromSeq <= LastProcessedRealState)
|
||||
{
|
||||
//this is not actually extrapolation
|
||||
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
|
||||
return true; // keep moving, we have a future state
|
||||
}
|
||||
|
||||
// we won't extrapolate, and curState was not found, buffer is empty
|
||||
if (!Extrapolation && curState == null)
|
||||
return false;
|
||||
|
||||
// we found both the states to interpolate between, this should almost always be true.
|
||||
if (Interpolation && curState != null)
|
||||
return true;
|
||||
|
||||
if (!Interpolation && curState != null && nextState != null)
|
||||
return true;
|
||||
|
||||
if (curState == null)
|
||||
{
|
||||
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
|
||||
}
|
||||
|
||||
if (nextState == null && Interpolation)
|
||||
{
|
||||
nextState = ExtrapolateState(_highestFromSequence, nextTick, lastStateInput);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a completely fake GameState.
|
||||
/// </summary>
|
||||
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence, uint lastInput)
|
||||
{
|
||||
var state = new GameState(fromSequence, toSequence, lastInput, default, default, default, null);
|
||||
state.Extrapolated = true;
|
||||
return state;
|
||||
// Even if we can't find current state, maybe we have a future state?
|
||||
return curState != null || (futureStateLowestFromSeq != null && futureStateLowestFromSeq <= _timing.LastRealTick);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
_stateBuffer.Clear();
|
||||
_lastFullState = null;
|
||||
_waitingForFull = true;
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = GameTick.Zero;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data)
|
||||
public void RequestFullState()
|
||||
{
|
||||
_stateBuffer.Clear();
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = _timing.LastRealTick;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data)
|
||||
{
|
||||
foreach (var (uid, compData) in data)
|
||||
{
|
||||
@@ -372,20 +340,39 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity)
|
||||
public Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity)
|
||||
{
|
||||
return _lastStateFullRep[entity];
|
||||
}
|
||||
|
||||
public bool TryGetLastServerStates(EntityUid entity,
|
||||
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary)
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
|
||||
{
|
||||
return _lastStateFullRep.TryGetValue(entity, out dictionary);
|
||||
}
|
||||
|
||||
public int CalculateBufferSize(GameTick fromTick)
|
||||
{
|
||||
return _stateBuffer.Count(s => s.ToSequence >= fromTick);
|
||||
bool foundState;
|
||||
var nextTick = fromTick;
|
||||
|
||||
do
|
||||
{
|
||||
foundState = false;
|
||||
|
||||
foreach (var state in _stateBuffer)
|
||||
{
|
||||
if (state.ToSequence > nextTick && state.FromSequence <= nextTick)
|
||||
{
|
||||
foundState = true;
|
||||
nextTick += 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
while (foundState);
|
||||
|
||||
return (int) (nextTick.Value - fromTick.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
@@ -27,18 +28,10 @@ namespace Robust.Client.GameStates
|
||||
int TargetBufferSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of game states currently in the state buffer.
|
||||
/// Number of applicable game states currently in the state buffer.
|
||||
/// </summary>
|
||||
int CurrentBufferSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current tick of the last server game state applied.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this to synchronize server-sent simulation events with the client's game loop.
|
||||
/// </remarks>
|
||||
GameTick CurServerTick { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the buffer size is this many states larger than the target buffer size,
|
||||
/// apply the overflow of states in a single tick.
|
||||
@@ -57,6 +50,11 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
event Action<GameStateAppliedArgs> GameStateApplied;
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked whenever a pvs-leave message is received.
|
||||
/// </summary>
|
||||
public event Action<MsgStateLeavePvs>? PvsLeave;
|
||||
|
||||
/// <summary>
|
||||
/// One time initialization of the service.
|
||||
/// </summary>
|
||||
@@ -78,6 +76,11 @@ namespace Robust.Client.GameStates
|
||||
/// <param name="message">Message being dispatched.</param>
|
||||
void InputCommandDispatched(FullInputCmdMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Requests a full state from the server. This should override even implicit entity data.
|
||||
/// </summary>
|
||||
public void RequestFullState(EntityUid? missingEntity = null);
|
||||
|
||||
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -17,8 +17,8 @@ namespace Robust.Client.GameStates
|
||||
/// Minimum number of states needed in the buffer for everything to work.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With interpolation enabled minimum is 3 states in buffer for the system to work (last, cur, next).
|
||||
/// Without interpolation enabled minimum is 2 states in buffer for the system to work (last, cur).
|
||||
/// With interpolation enabled minimum is 2 states in buffer for the system to work (cur, next).
|
||||
/// Without interpolation enabled minimum is 2 states in buffer for the system to work (cur).
|
||||
/// </remarks>
|
||||
int MinBufferSize { get; }
|
||||
|
||||
@@ -28,12 +28,6 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
int TargetBufferSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of game states currently in the state buffer.
|
||||
/// </summary>
|
||||
/// <seealso cref="CalculateBufferSize"/>
|
||||
int CurrentBufferSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is frame interpolation turned on?
|
||||
/// </summary>
|
||||
@@ -46,29 +40,22 @@ namespace Robust.Client.GameStates
|
||||
/// For Lan, set this to 0. For Excellent net conditions, set this to 1. For normal network conditions,
|
||||
/// set this to 2. For worse conditions, set it higher.
|
||||
/// </remarks>
|
||||
int InterpRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the client clock runs ahead of the server and the buffer gets emptied, should fake extrapolated states be generated?
|
||||
/// </summary>
|
||||
bool Extrapolation { get; set; }
|
||||
int BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is debug logging enabled? This will dump debug info about every state to the log.
|
||||
/// </summary>
|
||||
bool Logging { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last REAL server tick that has been processed.
|
||||
/// i.e. not incremented on extrapolation.
|
||||
/// </summary>
|
||||
GameTick LastProcessedRealState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new state into the processor. These are usually from networking or replays.
|
||||
/// </summary>
|
||||
/// <param name="state">Newly received state.</param>
|
||||
void AddNewState(GameState state);
|
||||
/// <returns>Returns true if the state was accepted and should be acknowledged</returns>
|
||||
bool AddNewState(GameState state);
|
||||
//> usually from replays
|
||||
//replays when
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the current and next state to apply for a given game tick.
|
||||
@@ -77,7 +64,7 @@ namespace Robust.Client.GameStates
|
||||
/// <param name="curState">Current state for the given tick. This can be null.</param>
|
||||
/// <param name="nextState">Current state for tick + 1. This can be null.</param>
|
||||
/// <returns>Was the function able to correctly calculate the states for the given tick?</returns>
|
||||
bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState);
|
||||
bool TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState);
|
||||
|
||||
/// <summary>
|
||||
/// Resets the processor back to its initial state.
|
||||
@@ -96,21 +83,22 @@ namespace Robust.Client.GameStates
|
||||
/// The data to merge.
|
||||
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
|
||||
/// </param>
|
||||
void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data);
|
||||
void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data);
|
||||
|
||||
/// <summary>
|
||||
/// Get the last state data from the server for an entity.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary (net ID -> ComponentState)</returns>
|
||||
Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity);
|
||||
Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the size of the game state buffer from a given tick.
|
||||
/// Calculate the number of applicable states in the game state buffer from a given tick.
|
||||
/// This includes only applicable states. If there is a gap, future buffers are not included.
|
||||
/// </summary>
|
||||
/// <param name="fromTick">The tick to calculate from.</param>
|
||||
int CalculateBufferSize(GameTick fromTick);
|
||||
|
||||
bool TryGetLastServerStates(EntityUid entity,
|
||||
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary);
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
@@ -21,21 +21,20 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
sealed class NetEntityOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
|
||||
private const uint TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
|
||||
private const int _maxEnts = 128; // maximum number of entities to track.
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
private readonly Font _font;
|
||||
private readonly int _lineHeight;
|
||||
private readonly List<NetEntity> _netEnts = new();
|
||||
private readonly Dictionary<EntityUid, NetEntData> _netEnts = new();
|
||||
|
||||
public NetEntityOverlay()
|
||||
{
|
||||
@@ -45,87 +44,58 @@ namespace Robust.Client.GameStates
|
||||
_lineHeight = _font.GetLineHeight(1);
|
||||
|
||||
_gameStateManager.GameStateApplied += HandleGameStateApplied;
|
||||
_gameStateManager.PvsLeave += OnPvsLeave;
|
||||
}
|
||||
|
||||
private void OnPvsLeave(MsgStateLeavePvs msg)
|
||||
{
|
||||
if (msg.Tick.Value + TrafficHistorySize < _gameTiming.LastRealTick.Value)
|
||||
return;
|
||||
|
||||
foreach (var uid in msg.Entities)
|
||||
{
|
||||
if (!_netEnts.TryGetValue(uid, out var netEnt))
|
||||
continue;
|
||||
|
||||
if (netEnt.LastUpdate < msg.Tick)
|
||||
{
|
||||
netEnt.InPVS = false;
|
||||
netEnt.LastUpdate = msg.Tick;
|
||||
}
|
||||
|
||||
netEnt.Traffic.Add(msg.Tick, NetEntData.EntState.PvsLeave);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGameStateApplied(GameStateAppliedArgs args)
|
||||
{
|
||||
if(_gameTiming.InPrediction) // we only care about real server states.
|
||||
return;
|
||||
|
||||
// Shift traffic history down one
|
||||
for (var i = 0; i < _netEnts.Count; i++)
|
||||
{
|
||||
var traffic = _netEnts[i].Traffic;
|
||||
for (int j = 1; j < TrafficHistorySize; j++)
|
||||
{
|
||||
traffic[j - 1] = traffic[j];
|
||||
}
|
||||
|
||||
traffic[^1] = 0;
|
||||
}
|
||||
|
||||
var gameState = args.AppliedState;
|
||||
|
||||
if(gameState.EntityStates.HasContents)
|
||||
if (!gameState.EntityStates.HasContents)
|
||||
return;
|
||||
|
||||
foreach (var entityState in gameState.EntityStates.Span)
|
||||
{
|
||||
// Loop over every entity that gets updated this state and record the traffic
|
||||
foreach (var entityState in gameState.EntityStates.Span)
|
||||
if (!_netEnts.TryGetValue(entityState.Uid, out var netEnt))
|
||||
{
|
||||
var newEnt = true;
|
||||
for(var i=0;i<_netEnts.Count;i++)
|
||||
{
|
||||
var netEnt = _netEnts[i];
|
||||
|
||||
if (netEnt.Id != entityState.Uid)
|
||||
continue;
|
||||
|
||||
//TODO: calculate size of state and record it here.
|
||||
netEnt.Traffic[^1] = 1;
|
||||
netEnt.LastUpdate = gameState.ToSequence;
|
||||
newEnt = false;
|
||||
_netEnts[i] = netEnt; // copy struct back
|
||||
break;
|
||||
}
|
||||
|
||||
if (!newEnt)
|
||||
if (_netEnts.Count >= _maxEnts)
|
||||
continue;
|
||||
|
||||
var newNetEnt = new NetEntity(entityState.Uid);
|
||||
newNetEnt.Traffic[^1] = 1;
|
||||
newNetEnt.LastUpdate = gameState.ToSequence;
|
||||
_netEnts.Add(newNetEnt);
|
||||
_netEnts[entityState.Uid] = netEnt = new();
|
||||
}
|
||||
}
|
||||
|
||||
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
|
||||
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
var pvsCenter = _eyeManager.CurrentEye.Position;
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange*2, pvsRange*2));
|
||||
|
||||
int timeout = _gameTiming.TickRate * 3;
|
||||
for (int i = 0; i < _netEnts.Count; i++)
|
||||
{
|
||||
var netEnt = _netEnts[i];
|
||||
|
||||
if(_entityManager.EntityExists(netEnt.Id))
|
||||
if (!netEnt.InPVS && netEnt.LastUpdate < gameState.ToSequence)
|
||||
{
|
||||
//TODO: Whoever is working on PVS remake, change the InPVS detection.
|
||||
var uid = netEnt.Id;
|
||||
var position = _entityManager.GetComponent<TransformComponent>(uid).MapPosition;
|
||||
netEnt.InPVS = !pvsEnabled || (pvsBox.Contains(position.Position) && position.MapId == pvsCenter.MapId);
|
||||
_netEnts[i] = netEnt; // copy struct back
|
||||
continue;
|
||||
netEnt.InPVS = true;
|
||||
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.PvsEnter);
|
||||
}
|
||||
else
|
||||
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.Data);
|
||||
|
||||
netEnt.Exists = false;
|
||||
if (netEnt.LastUpdate.Value + timeout < _gameTiming.LastRealTick.Value)
|
||||
{
|
||||
_netEnts.RemoveAt(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (netEnt.LastUpdate < gameState.ToSequence)
|
||||
netEnt.LastUpdate = gameState.ToSequence;
|
||||
|
||||
_netEnts[i] = netEnt; // copy struct back
|
||||
//TODO: calculate size of state and record it here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,145 +109,128 @@ namespace Robust.Client.GameStates
|
||||
case OverlaySpace.ScreenSpace:
|
||||
DrawScreen(args);
|
||||
break;
|
||||
case OverlaySpace.WorldSpace:
|
||||
DrawWorld(args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWorld(in OverlayDrawArgs args)
|
||||
{
|
||||
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
|
||||
if(!pvsEnabled)
|
||||
return;
|
||||
|
||||
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
var pvsCenter = _eyeManager.CurrentEye.Position;
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange * 2, pvsRange * 2));
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
worldHandle.DrawRect(pvsBox, Color.Red, false);
|
||||
}
|
||||
|
||||
private void DrawScreen(in OverlayDrawArgs args)
|
||||
{
|
||||
// remember, 0,0 is top left of ui with +X right and +Y down
|
||||
var screenHandle = args.ScreenHandle;
|
||||
|
||||
for (int i = 0; i < _netEnts.Count; i++)
|
||||
int i = 0;
|
||||
foreach (var (uid, netEnt) in _netEnts)
|
||||
{
|
||||
var netEnt = _netEnts[i];
|
||||
var uid = netEnt.Id;
|
||||
|
||||
if (!_entityManager.EntityExists(uid))
|
||||
{
|
||||
_netEnts.RemoveSwap(i);
|
||||
i--;
|
||||
_netEnts.Remove(uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
var xPos = 100;
|
||||
var yPos = 10 + _lineHeight * i;
|
||||
var name = $"({netEnt.Id}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
|
||||
var color = CalcTextColor(ref netEnt);
|
||||
var yPos = 10 + _lineHeight * i++;
|
||||
var name = $"({uid}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
|
||||
var color = netEnt.TextColor(_gameTiming);
|
||||
screenHandle.DrawString(_font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
|
||||
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
|
||||
DrawTrafficBox(screenHandle, netEnt, xPos, yPos);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTrafficBox(DrawingHandleScreen handle, ref NetEntity netEntity, int x, int y)
|
||||
private void DrawTrafficBox(DrawingHandleScreen handle, NetEntData netEntity, int x, int y)
|
||||
{
|
||||
handle.DrawRect(UIBox2.FromDimensions(x+1, y, TrafficHistorySize + 1, _lineHeight), new Color(32, 32, 32, 128));
|
||||
handle.DrawRect(UIBox2.FromDimensions(x + 1, y, TrafficHistorySize + 1, _lineHeight), new Color(32, 32, 32, 128));
|
||||
handle.DrawRect(UIBox2.FromDimensions(x, y, TrafficHistorySize + 2, _lineHeight), Color.Gray.WithAlpha(0.15f), false);
|
||||
|
||||
var traffic = netEntity.Traffic;
|
||||
|
||||
//TODO: Local peak size, actually scale the peaks
|
||||
for (int i = 0; i < TrafficHistorySize; i++)
|
||||
for (uint i = 1; i <= TrafficHistorySize; i++)
|
||||
{
|
||||
if(traffic[i] == 0)
|
||||
if (!traffic.TryGetValue(_gameTiming.LastRealTick + (i - TrafficHistorySize), out var tickData))
|
||||
continue;
|
||||
|
||||
var color = tickData switch
|
||||
{
|
||||
NetEntData.EntState.Data => Color.Green,
|
||||
NetEntData.EntState.PvsLeave => Color.Orange,
|
||||
NetEntData.EntState.PvsEnter => Color.Cyan,
|
||||
_ => throw new Exception("Unexpected value")
|
||||
};
|
||||
|
||||
var xPos = x + 1 + i;
|
||||
var yPosA = y + 1;
|
||||
var yPosB = yPosA + _lineHeight - 1;
|
||||
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), Color.Green);
|
||||
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), color);
|
||||
}
|
||||
}
|
||||
|
||||
private Color CalcTextColor(ref NetEntity ent)
|
||||
{
|
||||
if(!ent.Exists)
|
||||
return Color.Gray; // Entity is deleted, will be removed from list soon.
|
||||
|
||||
if(!ent.InPVS)
|
||||
return Color.Red; // Entity still exists outside PVS, but not updated anymore.
|
||||
|
||||
if(_gameTiming.LastRealTick < ent.LastUpdate + _gameTiming.TickRate)
|
||||
return Color.Blue; //Entity in PVS generating ongoing traffic.
|
||||
|
||||
return Color.Green; // Entity in PVS, but not updated recently.
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
|
||||
_gameStateManager.PvsLeave -= OnPvsLeave;
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private struct NetEntity
|
||||
private sealed class NetEntData
|
||||
{
|
||||
public GameTick LastUpdate;
|
||||
public readonly EntityUid Id;
|
||||
public readonly int[] Traffic;
|
||||
public bool Exists;
|
||||
public bool InPVS;
|
||||
public GameTick LastUpdate = GameTick.Zero;
|
||||
public readonly OverflowDictionary<GameTick, EntState> Traffic = new((int) TrafficHistorySize);
|
||||
public bool Exists = true;
|
||||
public bool InPVS = true;
|
||||
|
||||
public NetEntity(EntityUid id)
|
||||
public Color TextColor(IClientGameTiming timing)
|
||||
{
|
||||
LastUpdate = GameTick.Zero;
|
||||
Id = id;
|
||||
Traffic = new int[TrafficHistorySize];
|
||||
Exists = true;
|
||||
InPVS = true;
|
||||
if (!InPVS)
|
||||
return Color.Orange; // Entity still exists outside PVS, but not updated anymore.
|
||||
|
||||
if (timing.LastRealTick < LastUpdate + timing.TickRate)
|
||||
return Color.Blue; //Entity in PVS generating ongoing traffic.
|
||||
|
||||
return Color.Green; // Entity in PVS, but not updated recently.
|
||||
}
|
||||
|
||||
public enum EntState : byte
|
||||
{
|
||||
Nothing = 0,
|
||||
Data = 1,
|
||||
PvsLeave = 2,
|
||||
PvsEnter = 3
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NetEntityReportCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "net_entityreport";
|
||||
public string Help => "net_entityreport <0|1>";
|
||||
public string Help => "net_entityreport";
|
||||
public string Description => "Toggles the net entity report panel.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!byte.TryParse(args[0], out var iValue))
|
||||
{
|
||||
shell.WriteError("Invalid argument: Needs to be 0 or 1.");
|
||||
return;
|
||||
}
|
||||
|
||||
var bValue = iValue > 0;
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
if (!overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
{
|
||||
overlayMan.AddOverlay(new NetEntityOverlay());
|
||||
shell.WriteLine("Enabled network entity report overlay.");
|
||||
}
|
||||
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
else
|
||||
{
|
||||
overlayMan.RemoveOverlay(typeof(NetEntityOverlay));
|
||||
shell.WriteLine("Disabled network entity report overlay.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NetShowGraphCommand : IConsoleCommand
|
||||
{
|
||||
// Yeah commands should be localized, but I'm lazy and this is really just a debug command.
|
||||
public string Command => "net_refresh";
|
||||
public string Help => "net_refresh";
|
||||
public string Description => "requests a full server state";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<IClientGameStateManager>().RequestFullState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
@@ -28,6 +29,7 @@ namespace Robust.Client.GameStates
|
||||
private const int MidrangePayloadBps = 33600 / 8; // mid-range line
|
||||
private const int BytesPerPixel = 2; // If you are running the game on a DSL connection, you can scale the graph to fit your absurd bandwidth.
|
||||
private const int LowerGraphOffset = 100; // Offset on the Y axis in pixels of the lower lag/interp graph.
|
||||
private const int LeftMargin = 500; // X offset, to avoid interfering with the f3 menu.
|
||||
private const int MsPerPixel = 4; // Latency Milliseconds per pixel, for scaling the graph.
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -84,38 +86,46 @@ namespace Robust.Client.GameStates
|
||||
var sb = new StringBuilder();
|
||||
foreach (var entState in entStates.Span)
|
||||
{
|
||||
if (entState.Uid == WatchEntId)
|
||||
{
|
||||
if(entState.ComponentChanges.HasContents)
|
||||
{
|
||||
sb.Append($"\n Changes:");
|
||||
foreach (var compChange in entState.ComponentChanges.Span)
|
||||
{
|
||||
var registration = _componentFactory.GetRegistration(compChange.NetID);
|
||||
var create = compChange.Created ? 'C' : '\0';
|
||||
var mod = !(compChange.Created || compChange.Created) ? 'M' : '\0';
|
||||
var del = compChange.Deleted ? 'D' : '\0';
|
||||
sb.Append($"\n [{create}{mod}{del}]{compChange.NetID}:{registration.Name}");
|
||||
if (entState.Uid != WatchEntId)
|
||||
continue;
|
||||
|
||||
if(compChange.State is not null)
|
||||
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
|
||||
}
|
||||
}
|
||||
if (!entState.ComponentChanges.HasContents)
|
||||
{
|
||||
sb.Append("\n Entered PVS");
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append($"\n Changes:");
|
||||
foreach (var compChange in entState.ComponentChanges.Span)
|
||||
{
|
||||
var registration = _componentFactory.GetRegistration(compChange.NetID);
|
||||
var create = compChange.Created ? 'C' : '\0';
|
||||
var mod = !(compChange.Created || compChange.Created) ? 'M' : '\0';
|
||||
var del = compChange.Deleted ? 'D' : '\0';
|
||||
sb.Append($"\n [{create}{mod}{del}]{compChange.NetID}:{registration.Name}");
|
||||
|
||||
if (compChange.State is not null)
|
||||
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
|
||||
}
|
||||
}
|
||||
entStateString = sb.ToString();
|
||||
}
|
||||
|
||||
foreach (var ent in args.Detached)
|
||||
{
|
||||
if (ent != WatchEntId)
|
||||
continue;
|
||||
|
||||
conShell.WriteLine($"watchEnt: Left PVS at tick {args.AppliedState.ToSequence}, eid={WatchEntId}" + "\n");
|
||||
}
|
||||
|
||||
var entDeletes = args.AppliedState.EntityDeletions;
|
||||
if (entDeletes.HasContents)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var entDelete in entDeletes.Span)
|
||||
{
|
||||
if (entDelete == WatchEntId)
|
||||
{
|
||||
entDelString = "\n Deleted";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,17 +165,16 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
// remember, 0,0 is top left of ui with +X right and +Y down
|
||||
|
||||
var leftMargin = 300;
|
||||
var width = HistorySize;
|
||||
var height = 500;
|
||||
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
|
||||
var handle = args.ScreenHandle;
|
||||
|
||||
// bottom payload line
|
||||
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
|
||||
handle.DrawLine(new Vector2(LeftMargin, height), new Vector2(LeftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
|
||||
|
||||
// bottom lag line
|
||||
handle.DrawLine(new Vector2(leftMargin, height + LowerGraphOffset), new Vector2(leftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
|
||||
handle.DrawLine(new Vector2(LeftMargin, height + LowerGraphOffset), new Vector2(LeftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
|
||||
|
||||
int lastLagY = -1;
|
||||
int lastLagMs = -1;
|
||||
@@ -175,7 +184,7 @@ namespace Robust.Client.GameStates
|
||||
var state = _history[i];
|
||||
|
||||
// draw the payload size
|
||||
var xOff = leftMargin + i;
|
||||
var xOff = LeftMargin + i;
|
||||
var yoff = height - state.Payload / BytesPerPixel;
|
||||
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
|
||||
|
||||
@@ -211,25 +220,25 @@ namespace Robust.Client.GameStates
|
||||
|
||||
// average payload line
|
||||
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
|
||||
handle.DrawLine(new Vector2(leftMargin, avgyoff), new Vector2(leftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
handle.DrawLine(new Vector2(LeftMargin, avgyoff), new Vector2(LeftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
|
||||
// top payload warning line
|
||||
var warnYoff = height - _warningPayloadSize / BytesPerPixel;
|
||||
handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
|
||||
// mid payload line
|
||||
var midYoff = height - _midrangePayloadSize / BytesPerPixel;
|
||||
handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
handle.DrawLine(new Vector2(LeftMargin, midYoff), new Vector2(LeftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
|
||||
// payload text
|
||||
handle.DrawString(_font, new Vector2(leftMargin + width, warnYoff), "56K");
|
||||
handle.DrawString(_font, new Vector2(leftMargin + width, midYoff), "33.6K");
|
||||
handle.DrawString(_font, new Vector2(LeftMargin + width, warnYoff), "56K");
|
||||
handle.DrawString(_font, new Vector2(LeftMargin + width, midYoff), "33.6K");
|
||||
|
||||
// interp text info
|
||||
if(lastLagY != -1)
|
||||
handle.DrawString(_font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
|
||||
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
|
||||
|
||||
handle.DrawString(_font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
|
||||
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
@@ -242,32 +251,19 @@ namespace Robust.Client.GameStates
|
||||
private sealed class NetShowGraphCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "net_graph";
|
||||
public string Help => "net_graph <0|1>";
|
||||
public string Help => "net_graph";
|
||||
public string Description => "Toggles the net statistics pannel.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!byte.TryParse(args[0], out var iValue))
|
||||
{
|
||||
shell.WriteLine("Invalid argument: Needs to be 0 or 1.");
|
||||
return;
|
||||
}
|
||||
|
||||
var bValue = iValue > 0;
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
if(!overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
{
|
||||
overlayMan.AddOverlay(new NetGraphOverlay());
|
||||
shell.WriteLine("Enabled network overlay.");
|
||||
}
|
||||
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
else
|
||||
{
|
||||
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
|
||||
shell.WriteLine("Disabled network overlay.");
|
||||
@@ -283,13 +279,12 @@ namespace Robust.Client.GameStates
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
EntityUid eValue;
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 1 argument.");
|
||||
return;
|
||||
eValue = IoCManager.Resolve<IPlayerManager>().LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var eValue))
|
||||
else if (!EntityUid.TryParse(args[0], out eValue))
|
||||
{
|
||||
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
|
||||
return;
|
||||
@@ -297,12 +292,13 @@ namespace Robust.Client.GameStates
|
||||
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
if (!overlayMan.TryGetOverlay(out NetGraphOverlay? overlay))
|
||||
{
|
||||
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
|
||||
|
||||
netOverlay.WatchEntId = eValue;
|
||||
overlay = new();
|
||||
overlayMan.AddOverlay(overlay);
|
||||
}
|
||||
|
||||
overlay.WatchEntId = eValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Robust.Client.GameStates
|
||||
handle.UseShader(_shader);
|
||||
var worldHandle = (DrawingHandleWorld) handle;
|
||||
var viewport = args.WorldAABB;
|
||||
var timing = IoCManager.Resolve<IGameTiming>();
|
||||
foreach (var boundingBox in _entityManager.EntityQuery<IPhysBody>(true))
|
||||
{
|
||||
// all entities have a TransformComponent
|
||||
@@ -48,10 +49,9 @@ namespace Robust.Client.GameStates
|
||||
var aabb = boundingBox.GetWorldAABB();
|
||||
|
||||
// if not on screen, or too small, continue
|
||||
if (!aabb.Translated(transform.WorldPosition).Intersects(viewport) || aabb.IsEmpty())
|
||||
if (!aabb.Intersects(viewport) || aabb.IsEmpty())
|
||||
continue;
|
||||
|
||||
var timing = IoCManager.Resolve<IGameTiming>();
|
||||
timing.InSimulation = true;
|
||||
|
||||
var boxOffset = transform.LerpDestination.Value - transform.LocalPosition;
|
||||
@@ -60,40 +60,26 @@ namespace Robust.Client.GameStates
|
||||
timing.InSimulation = false;
|
||||
|
||||
worldHandle.DrawLine(transform.WorldPosition, boxPosWorld, Color.Yellow);
|
||||
worldHandle.DrawRect(aabb.Translated(boxPosWorld), Color.Yellow.WithAlpha(0.5f), false);
|
||||
|
||||
worldHandle.DrawRect(aabb.Translated(boxOffset), Color.Yellow.WithAlpha(0.5f), false);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NetShowInterpCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "net_draw_interp";
|
||||
public string Help => "net_draw_interp <0|1>";
|
||||
public string Help => "net_draw_interp";
|
||||
public string Description => "Toggles the debug drawing of the network interpolation.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!byte.TryParse(args[0], out var iValue))
|
||||
{
|
||||
shell.WriteLine("Invalid argument: Needs to be 0 or 1.");
|
||||
return;
|
||||
}
|
||||
|
||||
var bValue = iValue > 0;
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (bValue && !overlayMan.HasOverlay<NetInterpOverlay>())
|
||||
if (!overlayMan.HasOverlay<NetInterpOverlay>())
|
||||
{
|
||||
overlayMan.AddOverlay(new NetInterpOverlay());
|
||||
shell.WriteLine("Enabled network interp overlay.");
|
||||
}
|
||||
else if (overlayMan.HasOverlay<NetInterpOverlay>())
|
||||
else
|
||||
{
|
||||
overlayMan.RemoveOverlay<NetInterpOverlay>();
|
||||
shell.WriteLine("Disabled network interp overlay.");
|
||||
|
||||
@@ -142,6 +142,8 @@ namespace Robust.Client.Graphics.Audio
|
||||
|
||||
if (_openALContext != ALContext.Null)
|
||||
{
|
||||
ALC.MakeContextCurrent(ALContext.Null);
|
||||
|
||||
ALC.DestroyContext(_openALContext);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
// Clear screen to correct color.
|
||||
ClearFramebuffer(_userInterfaceManager.GetMainClearColor());
|
||||
ClearFramebuffer(ConvertClearFromSrgb(_userInterfaceManager.GetMainClearColor()));
|
||||
|
||||
using (DebugGroup("UI"))
|
||||
using (_prof.Group("UI"))
|
||||
@@ -325,7 +325,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
|
||||
name: nameof(entityPostRenderTarget));
|
||||
}
|
||||
|
||||
|
||||
_renderHandle.UseRenderTarget(entityPostRenderTarget);
|
||||
_renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
|
||||
|
||||
@@ -449,7 +449,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
BindRenderTargetFull(RtToLoaded(rt));
|
||||
if (clearColor is not null)
|
||||
ClearFramebuffer(clearColor.Value);
|
||||
ClearFramebuffer(ConvertClearFromSrgb(clearColor.Value));
|
||||
|
||||
SetViewportImmediate(Box2i.FromDimensions(Vector2i.Zero, rt.Size));
|
||||
_updateUniformConstants(rt.Size);
|
||||
CalcScreenMatrices(rt.Size, out var proj, out var view);
|
||||
|
||||
@@ -382,6 +382,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: sRGB IS IN LINEAR IF FRAMEBUFFER_SRGB IS ACTIVE.
|
||||
private void ClearFramebuffer(Color color, int stencil = 0, ClearBufferMask mask = ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit)
|
||||
{
|
||||
GL.ClearColor(color.ConvertOpenTK());
|
||||
@@ -392,6 +393,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
private Color ConvertClearFromSrgb(Color color)
|
||||
{
|
||||
if (!_hasGLSrgb)
|
||||
return color;
|
||||
|
||||
return Color.FromSrgb(color);
|
||||
}
|
||||
|
||||
private (GLShaderProgram, LoadedShader) ActivateShaderInstance(ClydeHandle handle)
|
||||
{
|
||||
var instance = _shaderInstances[handle];
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace Robust.Client.Graphics
|
||||
bool RemoveOverlay(Type overlayClass);
|
||||
bool RemoveOverlay<T>() where T : Overlay;
|
||||
|
||||
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
|
||||
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
|
||||
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
|
||||
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
|
||||
|
||||
Overlay GetOverlay(Type overlayClass);
|
||||
T GetOverlay<T>() where T : Overlay;
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Robust.Client.Input
|
||||
common.AddFunction(EngineKeyFunctions.Walk);
|
||||
common.AddFunction(EngineKeyFunctions.CameraRotateRight);
|
||||
common.AddFunction(EngineKeyFunctions.CameraRotateLeft);
|
||||
common.AddFunction(EngineKeyFunctions.CameraReset);
|
||||
|
||||
common.AddFunction(EngineKeyFunctions.GuiTabNavigateNext);
|
||||
common.AddFunction(EngineKeyFunctions.GuiTabNavigatePrev);
|
||||
|
||||
@@ -55,6 +55,9 @@ namespace Robust.Client.Placement
|
||||
/// </summary>
|
||||
private bool _placenextframe;
|
||||
|
||||
// Massive hack to avoid creating a billion grids for now.
|
||||
private bool _gridFrameBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Allows various types of placement as singular, line, or grid placement where placement mode allows this type of placement
|
||||
/// </summary>
|
||||
@@ -106,7 +109,7 @@ namespace Robust.Client.Placement
|
||||
/// </summary>
|
||||
public List<IDirectionalTextureProvider>? CurrentTextures {
|
||||
set {
|
||||
PreparePlacementTexList(value, value != null, null);
|
||||
PreparePlacementTexList(value, !Hijack?.CanRotate ?? value != null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +262,7 @@ namespace Robust.Client.Placement
|
||||
if (!CurrentPermission!.IsTile)
|
||||
HandlePlacement();
|
||||
|
||||
_gridFrameBuffer = false;
|
||||
_placenextframe = false;
|
||||
return true;
|
||||
}))
|
||||
@@ -394,6 +398,7 @@ namespace Robust.Client.Placement
|
||||
DeactivateSpecialPlacement();
|
||||
break;
|
||||
case PlacementTypes.Grid:
|
||||
_gridFrameBuffer = true;
|
||||
foreach (var coordinate in CurrentMode!.GridCoordinates())
|
||||
{
|
||||
RequestPlacement(coordinate);
|
||||
@@ -570,8 +575,10 @@ namespace Robust.Client.Placement
|
||||
_pendingTileChanges.RemoveAll(c => c.Item2 < _time.RealTime);
|
||||
|
||||
// continues tile placement but placement of entities only occurs on mouseUp
|
||||
if (_placenextframe && CurrentPermission!.IsTile)
|
||||
if (_placenextframe && CurrentPermission!.IsTile && !_gridFrameBuffer)
|
||||
{
|
||||
HandlePlacement();
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateLineMode()
|
||||
@@ -710,7 +717,7 @@ namespace Robust.Client.Placement
|
||||
}
|
||||
sc.NoRotation = noRot;
|
||||
|
||||
if (prototype?.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp) == true)
|
||||
if (prototype != null && prototype.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp))
|
||||
{
|
||||
sc.Scale = spriteComp.Scale;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -91,16 +92,15 @@ namespace Robust.Client.Player
|
||||
!metaData.EntityDeleted)
|
||||
{
|
||||
entMan.GetComponent<EyeComponent>(previous.Value).Current = false;
|
||||
|
||||
// notify ECS Systems
|
||||
entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default));
|
||||
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
|
||||
}
|
||||
|
||||
ControlledEntity = default;
|
||||
ControlledEntity = null;
|
||||
InternalSession.AttachedEntity = null;
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default));
|
||||
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
|
||||
EntityDetached?.Invoke(new EntityDetachedEventArgs(previous.Value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace Robust.Client.Player
|
||||
/// <inheritdoc />
|
||||
public void Startup()
|
||||
{
|
||||
DebugTools.Assert(LocalPlayer == null);
|
||||
LocalPlayer = new LocalPlayer();
|
||||
|
||||
var msgList = new MsgPlayerListReq();
|
||||
@@ -97,6 +98,7 @@ namespace Robust.Client.Player
|
||||
/// <inheritdoc />
|
||||
public void Shutdown()
|
||||
{
|
||||
LocalPlayer?.DetachEntity();
|
||||
LocalPlayer = null;
|
||||
_sessions.Clear();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
@@ -20,6 +21,7 @@ namespace Robust.Client.Prototypes
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
|
||||
private readonly List<FileSystemWatcher> _watchers = new();
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
@@ -61,6 +63,10 @@ namespace Robust.Client.Prototypes
|
||||
msg.Paths = _reloadQueue.ToArray();
|
||||
_netManager.ClientSendMessage(msg);
|
||||
|
||||
// Reloading prototypes modifies entities. This currently causes some state management debug asserts to
|
||||
// fail. To avoid this, we set `IGameTiming.ApplyingState` to true, even though this isn't really applying a
|
||||
// server state.
|
||||
using var _ = _timing.StartStateApplicationArea();
|
||||
ReloadPrototypes(_reloadQueue);
|
||||
|
||||
_reloadQueue.Clear();
|
||||
|
||||
@@ -3,11 +3,11 @@ namespace Robust.Client.State
|
||||
// Dummy state that is only used to make sure there always is *a* state.
|
||||
public sealed class DefaultState : State
|
||||
{
|
||||
public override void Startup()
|
||||
protected override void Startup()
|
||||
{
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
protected override void Shutdown()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ namespace Robust.Client.State
|
||||
event Action<StateChangedEventArgs> OnStateChanged;
|
||||
|
||||
State CurrentState { get; }
|
||||
void RequestStateChange<T>() where T : State, new();
|
||||
T RequestStateChange<T>() where T : State, new();
|
||||
void FrameUpdate(FrameEventArgs e);
|
||||
void RequestStateChange(Type type);
|
||||
State RequestStateChange(Type type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
using System;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.State
|
||||
{
|
||||
public abstract class State
|
||||
{
|
||||
/// <summary>
|
||||
/// Screen is being (re)enabled.
|
||||
/// </summary>
|
||||
public abstract void Startup();
|
||||
//[Optional] The UIScreen attached to this gamestate
|
||||
protected virtual Type? LinkedScreenType => null;
|
||||
|
||||
/// <summary>
|
||||
/// Screen is being disabled (NOT Destroyed).
|
||||
/// Game switching to this state
|
||||
/// </summary>
|
||||
public abstract void Shutdown();
|
||||
|
||||
internal void StartupInternal(IUserInterfaceManager userInterfaceManager)
|
||||
{
|
||||
if (LinkedScreenType != null)
|
||||
{
|
||||
if (!LinkedScreenType.IsAssignableTo(typeof(UIScreen))) throw new Exception("Linked Screen type is invalid");
|
||||
userInterfaceManager.LoadScreenInternal(LinkedScreenType);
|
||||
}
|
||||
Startup();
|
||||
}
|
||||
|
||||
protected abstract void Startup();
|
||||
|
||||
/// <summary>
|
||||
/// Game switching away from this state
|
||||
/// </summary>
|
||||
|
||||
internal void ShutdownInternal(IUserInterfaceManager userInterfaceManager)
|
||||
{
|
||||
if (LinkedScreenType != null)
|
||||
{
|
||||
userInterfaceManager.UnloadScreen();
|
||||
}
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
protected abstract void Shutdown();
|
||||
|
||||
public virtual void FrameUpdate(FrameEventArgs e) { }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Robust.Shared.Log;
|
||||
using System;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.State
|
||||
@@ -8,7 +9,7 @@ namespace Robust.Client.State
|
||||
internal sealed class StateManager : IStateManager
|
||||
{
|
||||
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
|
||||
|
||||
[Dependency] private readonly IUserInterfaceManager _interfaceManager = default!;
|
||||
public event Action<StateChangedEventArgs>? OnStateChanged;
|
||||
public State CurrentState { get; private set; }
|
||||
|
||||
@@ -22,35 +23,34 @@ namespace Robust.Client.State
|
||||
CurrentState?.FrameUpdate(e);
|
||||
}
|
||||
|
||||
public void RequestStateChange<T>() where T : State, new()
|
||||
public T RequestStateChange<T>() where T : State, new()
|
||||
{
|
||||
RequestStateChange(typeof(T));
|
||||
return (T) RequestStateChange(typeof(T));
|
||||
}
|
||||
|
||||
public void RequestStateChange(Type type)
|
||||
public State RequestStateChange(Type type)
|
||||
{
|
||||
if(!typeof(State).IsAssignableFrom(type))
|
||||
throw new ArgumentException($"Needs to be derived from {typeof(State).FullName}", nameof(type));
|
||||
|
||||
if (CurrentState?.GetType() != type)
|
||||
{
|
||||
SwitchToState(type);
|
||||
}
|
||||
return CurrentState?.GetType() == type ? CurrentState : SwitchToState(type);
|
||||
}
|
||||
|
||||
private void SwitchToState(Type type)
|
||||
private State SwitchToState(Type type)
|
||||
{
|
||||
Logger.Debug($"Switching to state {type}");
|
||||
|
||||
var newState = _typeFactory.CreateInstance<State>(type);
|
||||
|
||||
var old = CurrentState;
|
||||
CurrentState?.Shutdown();
|
||||
CurrentState?.ShutdownInternal(_interfaceManager);
|
||||
|
||||
CurrentState = newState;
|
||||
CurrentState.Startup();
|
||||
CurrentState.StartupInternal(_interfaceManager);
|
||||
|
||||
OnStateChanged?.Invoke(new StateChangedEventArgs(old, CurrentState));
|
||||
|
||||
return CurrentState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace Robust.Client.Timing
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
|
||||
public override bool InPrediction => !ApplyingState && CurTick > LastRealTick;
|
||||
|
||||
/// <inheritdoc />
|
||||
public GameTick LastRealTick { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public GameTick LastProcessedTick { get; set; }
|
||||
|
||||
public override TimeSpan ServerTime
|
||||
{
|
||||
get
|
||||
|
||||
@@ -5,6 +5,20 @@ namespace Robust.Client.Timing
|
||||
{
|
||||
public interface IClientGameTiming : IGameTiming
|
||||
{
|
||||
/// <summary>
|
||||
/// This is functionally the clients "current-tick" before prediction, and represents the target value for <see
|
||||
/// cref="LastRealTick"/>. This value should increment by at least one every tick. It may increase by more than
|
||||
/// that if we apply several server states within a single tick.
|
||||
/// </summary>
|
||||
GameTick LastProcessedTick { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last real non-extrapolated server state that was applied. Without networking issues, this tick should
|
||||
/// always correspond to <see cref="LastRealTick"/>, however if there is a missing states or the buffer has run
|
||||
/// out, this value may be smaller..
|
||||
/// </summary>
|
||||
GameTick LastRealTick { get; set; }
|
||||
|
||||
void StartPastPrediction();
|
||||
void EndPastPrediction();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Metadata;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -69,6 +70,22 @@ namespace Robust.Client.UserInterface
|
||||
// _nameScope = nameScope;
|
||||
//}
|
||||
|
||||
public UITheme Theme { get; set; }
|
||||
|
||||
protected virtual void OnThemeUpdated(){}
|
||||
internal void ThemeUpdateRecursive()
|
||||
{
|
||||
var curTheme = IoCManager.Resolve<IUserInterfaceManager>().CurrentTheme;
|
||||
if (Theme == curTheme) return; //don't update themes if the themes are up to date
|
||||
Theme = curTheme;
|
||||
OnThemeUpdated();
|
||||
foreach (var child in Children)
|
||||
{
|
||||
// Don't descent into children that have a style sheet since those aren't affected.
|
||||
child.ThemeUpdateRecursive();
|
||||
}
|
||||
}
|
||||
|
||||
public NameScope? FindNameScope()
|
||||
{
|
||||
foreach (var control in this.GetSelfAndLogicalAncestors())
|
||||
@@ -453,6 +470,7 @@ namespace Robust.Client.UserInterface
|
||||
UserInterfaceManagerInternal = IoCManager.Resolve<IUserInterfaceManagerInternal>();
|
||||
StyleClasses = new StyleClassCollection(this);
|
||||
Children = new OrderedChildCollection(this);
|
||||
Theme = UserInterfaceManagerInternal.CurrentTheme;
|
||||
XamlChildren = Children;
|
||||
}
|
||||
|
||||
|
||||
10
Robust.Client/UserInterface/Controllers/IOnStateChanged.cs
Normal file
10
Robust.Client/UserInterface/Controllers/IOnStateChanged.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface implemented by <see cref="UIController"/>s
|
||||
/// Implements both <see cref="IOnStateEntered{T}"/> and <see cref="IOnStateExited{T}"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The state type</typeparam>
|
||||
public interface IOnStateChanged<T> : IOnStateEntered<T>, IOnStateExited<T> where T : State.State
|
||||
{
|
||||
}
|
||||
16
Robust.Client/UserInterface/Controllers/IOnStateEntered.cs
Normal file
16
Robust.Client/UserInterface/Controllers/IOnStateEntered.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface implemented by <see cref="UIController"/>s
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The state type</typeparam>
|
||||
public interface IOnStateEntered<T> where T : State.State
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by <see cref="UserInterfaceManager.OnStateChanged"/>
|
||||
/// on <see cref="UIController"/>s that implement this method when a state
|
||||
/// of the specified type is entered
|
||||
/// </summary>
|
||||
/// <param name="state">The state that was entered</param>
|
||||
void OnStateEntered(T state);
|
||||
}
|
||||
16
Robust.Client/UserInterface/Controllers/IOnStateExited.cs
Normal file
16
Robust.Client/UserInterface/Controllers/IOnStateExited.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface implemented by <see cref="UIController"/>s
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The state type</typeparam>
|
||||
public interface IOnStateExited<T> where T : State.State
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by <see cref="UserInterfaceManager.OnStateChanged"/>
|
||||
/// on <see cref="UIController"/>s that implement this method when a state
|
||||
/// of the specified type is exited
|
||||
/// </summary>
|
||||
/// <param name="state">The state that was exited</param>
|
||||
void OnStateExited(T state);
|
||||
}
|
||||
12
Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs
Normal file
12
Robust.Client/UserInterface/Controllers/IOnSystemChanged.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface implemented by <see cref="UIController"/>s
|
||||
/// Implements both <see cref="IOnSystemLoaded{T}"/> and <see cref="IOnSystemUnloaded{T}"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity system type</typeparam>
|
||||
public interface IOnSystemChanged<T> : IOnSystemLoaded<T>, IOnSystemUnloaded<T> where T : IEntitySystem
|
||||
{
|
||||
}
|
||||
18
Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs
Normal file
18
Robust.Client/UserInterface/Controllers/IOnSystemLoaded.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface implemented by <see cref="UIController"/>s
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity system type</typeparam>
|
||||
public interface IOnSystemLoaded<T> where T : IEntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by <see cref="UserInterfaceManager.OnSystemLoaded"/>
|
||||
/// on <see cref="UIController"/>s that implement this method when a system
|
||||
/// of the specified type is loaded
|
||||
/// </summary>
|
||||
/// <param name="system">The system that was loaded</param>
|
||||
void OnSystemLoaded(T system);
|
||||
}
|
||||
18
Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs
Normal file
18
Robust.Client/UserInterface/Controllers/IOnSystemUnloaded.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface implemented by <see cref="UIController"/>s
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity system type</typeparam>
|
||||
public interface IOnSystemUnloaded<T> where T : IEntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by <see cref="UserInterfaceManager.OnSystemUnloaded"/>
|
||||
/// on <see cref="UIController"/>s that implement this method when a system
|
||||
/// of the specified type is unloaded
|
||||
/// </summary>
|
||||
/// <param name="system">The system that was unloaded</param>
|
||||
void OnSystemUnloaded(T system);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
public partial interface IUserInterfaceManager
|
||||
{
|
||||
public T GetUIController<T>() where T : UIController, new();
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using static Robust.Client.UserInterface.Controls.LineEdit;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers.Implementations;
|
||||
|
||||
public sealed class EntitySpawningUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IPlacementManager _placement = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
[Dependency] private readonly IResourceCache _resources = default!;
|
||||
|
||||
private EntitySpawnWindow? _window;
|
||||
private readonly List<EntityPrototype> _shownEntities = new();
|
||||
private bool _init;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
DebugTools.Assert(_init == false);
|
||||
_init = true;
|
||||
|
||||
_placement.DirectionChanged += OnDirectionChanged;
|
||||
_placement.PlacementChanged += ClearSelection;
|
||||
}
|
||||
|
||||
// The indices of the visible prototypes last time UpdateVisiblePrototypes was ran.
|
||||
// This is inclusive, so end is the index of the last prototype, not right after it.
|
||||
private (int start, int end) _lastEntityIndices;
|
||||
|
||||
private void OnEntityEraseToggled(ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_placement.Clear();
|
||||
// Only toggle the eraser back if the button is pressed.
|
||||
if(args.Pressed)
|
||||
_placement.ToggleEraser();
|
||||
// clearing will toggle the erase button off...
|
||||
args.Button.Pressed = args.Pressed;
|
||||
_window.OverrideMenu.Disabled = args.Pressed;
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureWindow();
|
||||
|
||||
if (_window!.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.Open();
|
||||
UpdateEntityDirectionLabel();
|
||||
_window.SearchBar.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureWindow()
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
|
||||
_window = UIManager.CreateWindow<EntitySpawnWindow>();
|
||||
LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft);
|
||||
_window.OnClose += WindowClosed;
|
||||
_window.EraseButton.Pressed = _placement.Eraser;
|
||||
_window.EraseButton.OnToggled += OnEntityEraseToggled;
|
||||
_window.OverrideMenu.OnItemSelected += OnEntityOverrideSelected;
|
||||
_window.SearchBar.OnTextChanged += OnEntitySearchChanged;
|
||||
_window.ClearButton.OnPressed += OnEntityClearPressed;
|
||||
_window.PrototypeScrollContainer.OnScrolled += UpdateVisiblePrototypes;
|
||||
_window.OnResized += UpdateVisiblePrototypes;
|
||||
BuildEntityList();
|
||||
}
|
||||
|
||||
public void CloseWindow()
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
private void WindowClosed()
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
if (_window.SelectedButton != null)
|
||||
{
|
||||
_window.SelectedButton.ActualButton.Pressed = false;
|
||||
_window.SelectedButton = null;
|
||||
}
|
||||
|
||||
_placement.Clear();
|
||||
}
|
||||
|
||||
private void ClearSelection(object? sender, EventArgs e)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
if (_window.SelectedButton != null)
|
||||
{
|
||||
_window.SelectedButton.ActualButton.Pressed = false;
|
||||
_window.SelectedButton = null;
|
||||
}
|
||||
|
||||
_window.EraseButton.Pressed = false;
|
||||
_window.OverrideMenu.Disabled = false;
|
||||
}
|
||||
|
||||
private void OnEntityOverrideSelected(OptionButton.ItemSelectedEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_window.OverrideMenu.SelectId(args.Id);
|
||||
|
||||
if (_placement.CurrentMode != null)
|
||||
{
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = EntitySpawnWindow.InitOpts[args.Id],
|
||||
EntityType = _placement.CurrentPermission!.EntityType,
|
||||
Range = 2,
|
||||
IsTile = _placement.CurrentPermission.IsTile
|
||||
};
|
||||
|
||||
_placement.Clear();
|
||||
_placement.BeginPlacing(newObjInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEntitySearchChanged(LineEditEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_placement.Clear();
|
||||
BuildEntityList(args.Text);
|
||||
_window.ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
|
||||
}
|
||||
|
||||
private void OnEntityClearPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_placement.Clear();
|
||||
_window.SearchBar.Clear();
|
||||
BuildEntityList("");
|
||||
}
|
||||
|
||||
private void BuildEntityList(string? searchStr = null)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_shownEntities.Clear();
|
||||
_window.PrototypeList.RemoveAllChildren();
|
||||
// Reset last prototype indices so it automatically updates the entire list.
|
||||
_lastEntityIndices = (0, -1);
|
||||
_window.PrototypeList.RemoveAllChildren();
|
||||
_window.SelectedButton = null;
|
||||
searchStr = searchStr?.ToLowerInvariant();
|
||||
|
||||
foreach (var prototype in _prototypes.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (prototype.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prototype.NoSpawn)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (searchStr != null && !DoesEntityMatchSearch(prototype, searchStr))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_shownEntities.Add(prototype);
|
||||
}
|
||||
|
||||
_shownEntities.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
_window.PrototypeList.TotalItemCount = _shownEntities.Count;
|
||||
UpdateVisiblePrototypes();
|
||||
}
|
||||
|
||||
private static bool DoesEntityMatchSearch(EntityPrototype prototype, string searchStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchStr))
|
||||
return true;
|
||||
|
||||
if (prototype.ID.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (prototype.EditorSuffix != null &&
|
||||
prototype.EditorSuffix.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (string.IsNullOrEmpty(prototype.Name))
|
||||
return false;
|
||||
|
||||
if (prototype.Name.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateEntityDirectionLabel()
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_window.RotationLabel.Text = _placement.Direction.ToString();
|
||||
}
|
||||
|
||||
private void OnDirectionChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateEntityDirectionLabel();
|
||||
}
|
||||
|
||||
// Update visible buttons in the prototype list.
|
||||
private void UpdateVisiblePrototypes()
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
// Calculate index of first prototype to render based on current scroll.
|
||||
var height = _window.MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation;
|
||||
var offset = Math.Max(-_window.PrototypeList.Position.Y, 0);
|
||||
var startIndex = (int) Math.Floor(offset / height);
|
||||
_window.PrototypeList.ItemOffset = startIndex;
|
||||
|
||||
var (prevStart, prevEnd) = _lastEntityIndices;
|
||||
|
||||
// Calculate index of final one.
|
||||
var endIndex = startIndex - 1;
|
||||
var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button.
|
||||
|
||||
while (spaceUsed < _window.PrototypeList.Parent!.Height)
|
||||
{
|
||||
spaceUsed += height;
|
||||
endIndex += 1;
|
||||
}
|
||||
|
||||
endIndex = Math.Min(endIndex, _shownEntities.Count - 1);
|
||||
|
||||
if (endIndex == prevEnd && startIndex == prevStart)
|
||||
{
|
||||
// Nothing changed so bye.
|
||||
return;
|
||||
}
|
||||
|
||||
_lastEntityIndices = (startIndex, endIndex);
|
||||
|
||||
// Delete buttons at the start of the list that are no longer visible (scrolling down).
|
||||
for (var i = prevStart; i < startIndex && i <= prevEnd; i++)
|
||||
{
|
||||
var control = (EntitySpawnButton) _window.PrototypeList.GetChild(0);
|
||||
DebugTools.Assert(control.Index == i);
|
||||
_window.PrototypeList.RemoveChild(control);
|
||||
}
|
||||
|
||||
// Delete buttons at the end of the list that are no longer visible (scrolling up).
|
||||
for (var i = prevEnd; i > endIndex && i >= prevStart; i--)
|
||||
{
|
||||
var control = (EntitySpawnButton) _window.PrototypeList.GetChild(_window.PrototypeList.ChildCount - 1);
|
||||
DebugTools.Assert(control.Index == i);
|
||||
_window.PrototypeList.RemoveChild(control);
|
||||
}
|
||||
|
||||
// Create buttons at the start of the list that are now visible (scrolling up).
|
||||
for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--)
|
||||
{
|
||||
InsertEntityButton(_shownEntities[i], true, i);
|
||||
}
|
||||
|
||||
// Create buttons at the end of the list that are now visible (scrolling down).
|
||||
for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++)
|
||||
{
|
||||
InsertEntityButton(_shownEntities[i], false, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertEntityButton(EntityPrototype prototype, bool insertFirst, int index)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
var textures = SpriteComponent.GetPrototypeTextures(prototype, _resources).Select(o => o.Default).ToList();
|
||||
var button = _window.InsertEntityButton(prototype, insertFirst, index, textures);
|
||||
|
||||
button.ActualButton.OnToggled += OnEntityButtonToggled;
|
||||
}
|
||||
|
||||
private void OnEntityButtonToggled(ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
var item = (EntitySpawnButton) args.Button.Parent!;
|
||||
if (_window.SelectedButton == item)
|
||||
{
|
||||
_window.SelectedButton = null;
|
||||
_window.SelectedPrototype = null;
|
||||
_placement.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_window.SelectedButton != null)
|
||||
{
|
||||
_window.SelectedButton.ActualButton.Pressed = false;
|
||||
}
|
||||
|
||||
_window.SelectedButton = null;
|
||||
_window.SelectedPrototype = null;
|
||||
|
||||
var overrideMode = EntitySpawnWindow.InitOpts[_window.OverrideMenu.SelectedId];
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = overrideMode != "Default" ? overrideMode : item.Prototype.PlacementMode,
|
||||
EntityType = item.PrototypeID,
|
||||
Range = 2,
|
||||
IsTile = false
|
||||
};
|
||||
|
||||
_placement.BeginPlacing(newObjInfo);
|
||||
|
||||
_window.SelectedButton = item;
|
||||
_window.SelectedPrototype = item.Prototype;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers.Implementations;
|
||||
|
||||
public sealed class TileSpawningUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IPlacementManager _placement = default!;
|
||||
[Dependency] private readonly IResourceCache _resources = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tiles = default!;
|
||||
|
||||
private TileSpawnWindow? _window;
|
||||
private bool _init;
|
||||
|
||||
private readonly List<ITileDefinition> _shownTiles = new();
|
||||
private bool _clearingTileSelections;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
DebugTools.Assert(_init == false);
|
||||
_init = true;
|
||||
_placement.PlacementChanged += ClearTileSelection;
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureWindow();
|
||||
|
||||
if (_window!.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.Open();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureWindow()
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_window = UIManager.CreateWindow<TileSpawnWindow>();
|
||||
LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft);
|
||||
_window.SearchBar.GrabKeyboardFocus();
|
||||
_window.ClearButton.OnPressed += OnTileClearPressed;
|
||||
_window.SearchBar.OnTextChanged += OnTileSearchChanged;
|
||||
_window.TileList.OnItemSelected += OnTileItemSelected;
|
||||
_window.TileList.OnItemDeselected += OnTileItemDeselected;
|
||||
BuildTileList();
|
||||
}
|
||||
|
||||
public void CloseWindow()
|
||||
{
|
||||
if (_window == null || _window.Disposed) return;
|
||||
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
private void ClearTileSelection(object? sender, EventArgs e)
|
||||
{
|
||||
if (_window == null || _window.Disposed) return;
|
||||
_clearingTileSelections = true;
|
||||
_window.TileList.ClearSelected();
|
||||
_clearingTileSelections = false;
|
||||
}
|
||||
|
||||
private void OnTileClearPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed) return;
|
||||
|
||||
_window.TileList.ClearSelected();
|
||||
_placement.Clear();
|
||||
_window.SearchBar.Clear();
|
||||
BuildTileList(string.Empty);
|
||||
_window.ClearButton.Disabled = true;
|
||||
}
|
||||
|
||||
private void OnTileSearchChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed) return;
|
||||
|
||||
_window.TileList.ClearSelected();
|
||||
_placement.Clear();
|
||||
BuildTileList(args.Text);
|
||||
_window.ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
|
||||
}
|
||||
|
||||
private void OnTileItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
var definition = _shownTiles[args.ItemIndex];
|
||||
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = "AlignTileAny",
|
||||
TileType = definition.TileId,
|
||||
Range = 400,
|
||||
IsTile = true
|
||||
};
|
||||
|
||||
_placement.BeginPlacing(newObjInfo);
|
||||
}
|
||||
|
||||
private void OnTileItemDeselected(ItemList.ItemListDeselectedEventArgs args)
|
||||
{
|
||||
if (_clearingTileSelections)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_placement.Clear();
|
||||
}
|
||||
|
||||
private void BuildTileList(string? searchStr = null)
|
||||
{
|
||||
if (_window == null || _window.Disposed) return;
|
||||
|
||||
_window.TileList.Clear();
|
||||
|
||||
IEnumerable<ITileDefinition> tileDefs = _tiles;
|
||||
|
||||
if (!string.IsNullOrEmpty(searchStr))
|
||||
{
|
||||
tileDefs = tileDefs.Where(s =>
|
||||
s.Name.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
s.ID.Contains(searchStr, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
tileDefs = tileDefs.OrderBy(d => d.Name);
|
||||
|
||||
_shownTiles.Clear();
|
||||
_shownTiles.AddRange(tileDefs);
|
||||
|
||||
foreach (var entry in _shownTiles)
|
||||
{
|
||||
Texture? texture = null;
|
||||
var path = entry.Sprite?.ToString();
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
texture = _resources.GetResource<TextureResource>(path);
|
||||
}
|
||||
_window.TileList.AddItem(entry.Name, texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Robust.Client/UserInterface/Controllers/UIController.cs
Normal file
25
Robust.Client/UserInterface/Controllers/UIController.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
// Notices your UIController, *UwU Whats this?*
|
||||
/// <summary>
|
||||
/// Each <see cref="UIController"/> is instantiated as a singleton by <see cref="UserInterfaceManager"/>
|
||||
/// <see cref="UIController"/> can use <see cref="DependencyAttribute"/> for regular IoC dependencies
|
||||
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
|
||||
/// injected once they are created.
|
||||
/// </summary>
|
||||
public abstract class UIController
|
||||
{
|
||||
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Serialization.Manager.Definition.DataDefinition;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal partial class UserInterfaceManager
|
||||
{
|
||||
/// <summary>
|
||||
/// All registered <see cref="UIController"/> instances indexed by type
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, UIController> _uiControllers = new();
|
||||
|
||||
/// <summary>
|
||||
/// Implementations of <see cref="IOnStateEntered{T}"/> to invoke when a state is entered
|
||||
/// State Type -> (UIController, Caller)
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, Dictionary<UIController, StateChangedCaller>> _onStateEnteredDelegates = new();
|
||||
|
||||
/// <summary>
|
||||
/// Implementations of <see cref="IOnStateExited{T}"/> to invoke when a state is exited
|
||||
/// State Type -> (UIController, Caller)
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, Dictionary<UIController, StateChangedCaller>> _onStateExitedDelegates = new();
|
||||
|
||||
/// <summary>
|
||||
/// Implementations of <see cref="IOnSystemLoaded{T}"/> to invoke when an entity system is loaded
|
||||
/// Entity System Type -> (UIController, Caller)
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, Dictionary<UIController, SystemChangedCaller>> _onSystemLoadedDelegates = new();
|
||||
|
||||
/// <summary>
|
||||
/// Implementations of <see cref="IOnSystemUnloaded{T}"/> to invoke when an entity system is unloaded
|
||||
/// Entity System Type -> (UIController, Caller)
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, Dictionary<UIController, SystemChangedCaller>> _onSystemUnloadedDelegates = new();
|
||||
|
||||
/// <summary>
|
||||
/// Field -> Controller -> Field assigner delegate
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, Dictionary<Type, AssignField<UIController, object?>>> _assignerRegistry = new();
|
||||
|
||||
private delegate void StateChangedCaller(object controller, State.State state);
|
||||
|
||||
private delegate void SystemChangedCaller(object controller, IEntitySystem system);
|
||||
|
||||
private StateChangedCaller EmitStateChangedCaller(Type controller, Type state, bool entered)
|
||||
{
|
||||
if (controller.IsValueType)
|
||||
{
|
||||
throw new ArgumentException($"Value type controllers are not supported. Controller: {controller}");
|
||||
}
|
||||
|
||||
if (state.IsValueType)
|
||||
{
|
||||
throw new ArgumentException($"Value type states are not supported. State: {state}");
|
||||
}
|
||||
|
||||
var method = new DynamicMethod(
|
||||
"StateChangedCaller",
|
||||
typeof(void),
|
||||
new[] {typeof(object), typeof(State.State)},
|
||||
true
|
||||
);
|
||||
|
||||
var generator = method.GetILGenerator();
|
||||
|
||||
Type onStateChangedType;
|
||||
MethodInfo onStateChangedMethod;
|
||||
if (entered)
|
||||
{
|
||||
onStateChangedType = typeof(IOnStateEntered<>).MakeGenericType(state);
|
||||
onStateChangedMethod =
|
||||
controller.GetMethod(nameof(IOnStateEntered<State.State>.OnStateEntered), new[] {state})
|
||||
?? throw new NullReferenceException();
|
||||
}
|
||||
else
|
||||
{
|
||||
onStateChangedType = typeof(IOnStateExited<>).MakeGenericType(state);
|
||||
onStateChangedMethod =
|
||||
controller.GetMethod(nameof(IOnStateExited<State.State>.OnStateExited), new[] {state})
|
||||
?? throw new NullReferenceException();
|
||||
}
|
||||
|
||||
generator.Emit(OpCodes.Ldarg_0); // controller
|
||||
generator.Emit(OpCodes.Castclass, onStateChangedType);
|
||||
|
||||
generator.Emit(OpCodes.Ldarg_1); // state
|
||||
generator.Emit(OpCodes.Castclass, state);
|
||||
|
||||
generator.Emit(OpCodes.Callvirt, onStateChangedMethod);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
|
||||
return method.CreateDelegate<StateChangedCaller>();
|
||||
}
|
||||
|
||||
private SystemChangedCaller EmitSystemChangedCaller(Type controller, Type system, bool loaded)
|
||||
{
|
||||
if (controller.IsValueType)
|
||||
{
|
||||
throw new ArgumentException($"Value type controllers are not supported. Controller: {controller}");
|
||||
}
|
||||
|
||||
if (system.IsValueType)
|
||||
{
|
||||
throw new ArgumentException($"Value type systems are not supported. System: {system}");
|
||||
}
|
||||
|
||||
var method = new DynamicMethod(
|
||||
"SystemChangedCaller",
|
||||
typeof(void),
|
||||
new[] {typeof(object), typeof(IEntitySystem)},
|
||||
true
|
||||
);
|
||||
|
||||
var generator = method.GetILGenerator();
|
||||
|
||||
Type onSystemChangedType;
|
||||
MethodInfo onSystemChangedMethod;
|
||||
if (loaded)
|
||||
{
|
||||
onSystemChangedType = typeof(IOnSystemLoaded<>).MakeGenericType(system);
|
||||
onSystemChangedMethod =
|
||||
controller.GetMethod(nameof(IOnSystemLoaded<IEntitySystem>.OnSystemLoaded), new[] {system})
|
||||
?? throw new NullReferenceException();
|
||||
}
|
||||
else
|
||||
{
|
||||
onSystemChangedType = typeof(IOnSystemUnloaded<>).MakeGenericType(system);
|
||||
onSystemChangedMethod =
|
||||
controller.GetMethod(nameof(IOnSystemUnloaded<IEntitySystem>.OnSystemUnloaded), new[] {system})
|
||||
?? throw new NullReferenceException();
|
||||
}
|
||||
|
||||
generator.Emit(OpCodes.Ldarg_0); // controller
|
||||
generator.Emit(OpCodes.Castclass, onSystemChangedType);
|
||||
|
||||
generator.Emit(OpCodes.Ldarg_1); // system
|
||||
generator.Emit(OpCodes.Castclass, system);
|
||||
|
||||
generator.Emit(OpCodes.Callvirt, onSystemChangedMethod);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
|
||||
return method.CreateDelegate<SystemChangedCaller>();
|
||||
}
|
||||
|
||||
private void RegisterUIController(Type type, UIController controller)
|
||||
{
|
||||
_uiControllers.Add(type, controller);
|
||||
}
|
||||
|
||||
private ref UIController GetUIControllerRef(Type type)
|
||||
{
|
||||
return ref CollectionsMarshal.GetValueRefOrNullRef(_uiControllers, type);
|
||||
}
|
||||
|
||||
private UIController GetUIController(Type type)
|
||||
{
|
||||
return _uiControllers[type];
|
||||
}
|
||||
|
||||
public T GetUIController<T>() where T : UIController, new()
|
||||
{
|
||||
return (T) GetUIController(typeof(T));
|
||||
}
|
||||
|
||||
private void _setupControllers()
|
||||
{
|
||||
foreach (var uiControllerType in _reflectionManager.GetAllChildren<UIController>())
|
||||
{
|
||||
if (uiControllerType.IsAbstract)
|
||||
continue;
|
||||
|
||||
var newController = _typeFactory.CreateInstanceUnchecked<UIController>(uiControllerType);
|
||||
|
||||
RegisterUIController(uiControllerType, newController);
|
||||
|
||||
foreach (var fieldInfo in uiControllerType.GetAllPropertiesAndFields())
|
||||
{
|
||||
if (!fieldInfo.HasAttribute<UISystemDependencyAttribute>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var backingField = fieldInfo;
|
||||
if (fieldInfo is SpecificPropertyInfo property)
|
||||
{
|
||||
if (property.TryGetBackingField(out var field))
|
||||
{
|
||||
backingField = field;
|
||||
}
|
||||
else
|
||||
{
|
||||
var setter = property.PropertyInfo.GetSetMethod(true);
|
||||
if (setter == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Property with {nameof(UISystemDependencyAttribute)} attribute did not have a backing field nor setter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Do not do anything if the field isn't an entity system
|
||||
if (!typeof(IEntitySystem).IsAssignableFrom(backingField.FieldType))
|
||||
continue;
|
||||
|
||||
var typeDict = _assignerRegistry.GetOrNew(fieldInfo.FieldType);
|
||||
var assigner = EmitFieldAssigner<UIController>(uiControllerType, fieldInfo.FieldType, backingField);
|
||||
typeDict.Add(uiControllerType, assigner);
|
||||
}
|
||||
|
||||
foreach (var @interface in uiControllerType.GetInterfaces())
|
||||
{
|
||||
if (!@interface.IsGenericType)
|
||||
continue;
|
||||
|
||||
var typeDefinition = @interface.GetGenericTypeDefinition();
|
||||
var genericType = @interface.GetGenericArguments()[0];
|
||||
if (typeDefinition == typeof(IOnStateEntered<>))
|
||||
{
|
||||
var enteredCaller = EmitStateChangedCaller(uiControllerType, genericType, true);
|
||||
_onStateEnteredDelegates.GetOrNew(genericType).Add(newController, enteredCaller);
|
||||
}
|
||||
else if (typeDefinition == typeof(IOnStateExited<>))
|
||||
{
|
||||
var exitedCaller = EmitStateChangedCaller(uiControllerType, genericType, false);
|
||||
_onStateExitedDelegates.GetOrNew(genericType).Add(newController, exitedCaller);
|
||||
}
|
||||
else if (typeDefinition == typeof(IOnSystemLoaded<>))
|
||||
{
|
||||
var loadedCaller = EmitSystemChangedCaller(uiControllerType, genericType, true);
|
||||
_onSystemLoadedDelegates.GetOrNew(genericType).Add(newController, loadedCaller);
|
||||
}
|
||||
else if (typeDefinition == typeof(IOnSystemUnloaded<>))
|
||||
{
|
||||
var unloadedCaller = EmitSystemChangedCaller(uiControllerType, genericType, false);
|
||||
_onSystemUnloadedDelegates.GetOrNew(genericType).Add(newController, unloadedCaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_systemManager.SystemLoaded += OnSystemLoaded;
|
||||
_systemManager.SystemUnloaded += OnSystemUnloaded;
|
||||
|
||||
_stateManager.OnStateChanged += OnStateChanged;
|
||||
}
|
||||
|
||||
private void _initializeControllers()
|
||||
{
|
||||
foreach (var controller in _uiControllers.Values)
|
||||
{
|
||||
controller.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateControllers(FrameEventArgs args)
|
||||
{
|
||||
foreach (var controller in _uiControllers.Values)
|
||||
{
|
||||
controller.FrameUpdate(args);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO hud refactor optimize this to use an array
|
||||
// TODO hud refactor BEFORE MERGE cleanup subscriptions for all implementations when switching out of gameplay state
|
||||
private void OnStateChanged(StateChangedEventArgs args)
|
||||
{
|
||||
if (_onStateExitedDelegates.TryGetValue(args.OldState.GetType(), out var exitedDelegates))
|
||||
{
|
||||
foreach (var (controller, caller) in exitedDelegates)
|
||||
{
|
||||
caller(controller, args.OldState);
|
||||
}
|
||||
}
|
||||
|
||||
if (_onStateEnteredDelegates.TryGetValue(args.NewState.GetType(), out var enteredDelegates))
|
||||
{
|
||||
foreach (var (controller, caller) in enteredDelegates)
|
||||
{
|
||||
caller(controller, args.NewState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSystemLoaded(object? sender, SystemChangedArgs args)
|
||||
{
|
||||
var systemType = args.System.GetType();
|
||||
|
||||
if (_assignerRegistry.TryGetValue(systemType, out var assigners))
|
||||
{
|
||||
foreach (var (controllerType, assigner) in assigners)
|
||||
{
|
||||
assigner(ref GetUIControllerRef(controllerType), args.System);
|
||||
}
|
||||
}
|
||||
|
||||
if (_onSystemLoadedDelegates.TryGetValue(systemType, out var delegates))
|
||||
{
|
||||
foreach (var (controller, caller) in delegates)
|
||||
{
|
||||
caller(controller, args.System);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSystemUnloaded(object? system, SystemChangedArgs args)
|
||||
{
|
||||
var systemType = args.System.GetType();
|
||||
|
||||
if (_onSystemUnloadedDelegates.TryGetValue(systemType, out var delegates))
|
||||
{
|
||||
foreach (var (controller, caller) in delegates)
|
||||
{
|
||||
caller(controller, args.System);
|
||||
}
|
||||
}
|
||||
|
||||
if (_assignerRegistry.TryGetValue(systemType, out var assigners))
|
||||
{
|
||||
foreach (var (controllerType, assigner) in assigners)
|
||||
{
|
||||
assigner(ref GetUIControllerRef(controllerType), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
private Vector2 _desiredSize;
|
||||
|
||||
public bool CloseOnClick { get; set; } = true;
|
||||
|
||||
public bool CloseOnEscape { get; set; } = true;
|
||||
|
||||
public void Open(UIBox2? box = null, Vector2? altPos = null)
|
||||
{
|
||||
if (Visible)
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[Virtual]
|
||||
public class ScrollContainer : Container
|
||||
{
|
||||
private bool _queueScrolled = false;
|
||||
private bool _vScrollEnabled = true;
|
||||
private bool _hScrollEnabled = true;
|
||||
|
||||
@@ -23,6 +24,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public bool ReturnMeasure { get; set; } = false;
|
||||
|
||||
public event Action? OnScrolled;
|
||||
|
||||
public ScrollContainer()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
@@ -204,6 +207,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override void ArrangeCore(UIBox2 finalRect)
|
||||
{
|
||||
base.ArrangeCore(finalRect);
|
||||
|
||||
if (!_queueScrolled) return;
|
||||
|
||||
OnScrolled?.Invoke();
|
||||
_queueScrolled = false;
|
||||
}
|
||||
|
||||
protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
base.MouseWheel(args);
|
||||
@@ -261,6 +274,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
InvalidateArrange();
|
||||
_queueScrolled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -10,7 +9,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[Virtual]
|
||||
public class SpriteView : Control
|
||||
{
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private SpriteSystem? _spriteSystem;
|
||||
|
||||
private Vector2 _scale = (1, 1);
|
||||
|
||||
@@ -36,21 +35,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// </remarks>
|
||||
public Direction? OverrideDirection { get; set; }
|
||||
|
||||
public SpriteView(IEntitySystemManager sysMan)
|
||||
{
|
||||
_spriteSystem = sysMan.GetEntitySystem<SpriteSystem>();
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
public SpriteView()
|
||||
{
|
||||
_spriteSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
public SpriteView(SpriteSystem spriteSys)
|
||||
{
|
||||
_spriteSystem = spriteSys;
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
@@ -68,7 +54,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
_spriteSystem.ForceUpdate(Sprite);
|
||||
_spriteSystem ??= EntitySystem.Get<SpriteSystem>();
|
||||
_spriteSystem?.ForceUpdate(Sprite);
|
||||
|
||||
renderHandle.DrawEntity(Sprite.Owner, PixelSize / 2, Scale * UIScale, OverrideDirection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -15,6 +17,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public const string StylePseudoClassHover = "hover";
|
||||
public const string StylePseudoClassDisabled = "disabled";
|
||||
public const string StylePseudoClassPressed = "pressed";
|
||||
private string? _texturePath;
|
||||
|
||||
|
||||
public TextureButton()
|
||||
{
|
||||
@@ -32,6 +36,29 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public string TextureThemePath
|
||||
{
|
||||
set {
|
||||
TextureNormal = Theme.ResolveTexture(value);
|
||||
_texturePath = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void OnThemeUpdated()
|
||||
{
|
||||
if (_texturePath != null) TextureNormal = Theme.ResolveTexture(_texturePath);
|
||||
base.OnThemeUpdated();
|
||||
}
|
||||
public string TexturePath
|
||||
{
|
||||
set
|
||||
{
|
||||
TextureNormal = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(value);
|
||||
_texturePath = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 Scale
|
||||
{
|
||||
get => _scale;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
@@ -38,6 +40,35 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private string? _texturePath;
|
||||
|
||||
// TODO HUD REFACTOR BEFORE MERGE use or cleanup
|
||||
public string TextureThemePath
|
||||
{
|
||||
set
|
||||
{
|
||||
Texture = Theme.ResolveTexture(value);
|
||||
_texturePath = value;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO HUD REFACTOR BEFORE MERGE use or cleanup
|
||||
public string TexturePath
|
||||
{
|
||||
set
|
||||
{
|
||||
Texture = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(value);
|
||||
_texturePath = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void OnThemeUpdated()
|
||||
{
|
||||
if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath);
|
||||
base.OnThemeUpdated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the texture displayed.
|
||||
/// </summary>
|
||||
|
||||
12
Robust.Client/UserInterface/Controls/UIWidget.cs
Normal file
12
Robust.Client/UserInterface/Controls/UIWidget.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls;
|
||||
|
||||
[Virtual]
|
||||
public abstract class UIWidget : BoxContainer
|
||||
{
|
||||
protected UIWidget()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
}
|
||||
@@ -10,31 +10,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
Window = window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable the UI autoscale system, this will scale down the UI for lower resolutions
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool AutoScale { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum resolution to start clamping autoscale to 1
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Vector2i AutoScaleUpperCutoff { get; set; } = new Vector2i(1080, 720);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum resolution to start clamping autos scale to autoscale minimum
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Vector2i AutoScaleLowerCutoff { get; set; } = new Vector2i(520, 520);
|
||||
|
||||
/// <summary>
|
||||
/// The minimum ui scale value that autoscale will scale to
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float AutoScaleMinimum { get; set; } = 0.5f;
|
||||
|
||||
public override float UIScale => UIScaleSet;
|
||||
internal float UIScaleSet { get; set; }
|
||||
public override IClydeWindow Window { get; }
|
||||
|
||||
@@ -17,8 +17,6 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
private Vector2 DragOffsetTopLeft;
|
||||
private Vector2 DragOffsetBottomRight;
|
||||
|
||||
protected bool _firstTimeOpened = true;
|
||||
|
||||
public bool Resizable { get; set; } = true;
|
||||
public bool IsOpen => Parent != null;
|
||||
|
||||
@@ -27,6 +25,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
/// </summary>
|
||||
public event Action? OnClose;
|
||||
|
||||
public event Action? OnOpen;
|
||||
|
||||
public virtual void Close()
|
||||
{
|
||||
if (Parent == null)
|
||||
@@ -210,7 +210,6 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (!Visible)
|
||||
@@ -225,9 +224,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
}
|
||||
|
||||
Opened();
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
|
||||
public void OpenCentered() => OpenCenteredAt((0.5f, 0.5f));
|
||||
|
||||
public void OpenToLeft() => OpenCenteredAt((0, 0.5f));
|
||||
public void OpenCenteredLeft() => OpenCenteredAt((0.25f, 0.5f));
|
||||
public void OpenToRight() => OpenCenteredAt((1, 0.5f));
|
||||
@@ -240,17 +241,10 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
/// lower right.</param>
|
||||
public void OpenCenteredAt(Vector2 relativePosition)
|
||||
{
|
||||
if (!_firstTimeOpened)
|
||||
{
|
||||
Open();
|
||||
return;
|
||||
}
|
||||
|
||||
Measure(Vector2.Infinity);
|
||||
SetSize = DesiredSize;
|
||||
Open();
|
||||
RecenterWindow(relativePosition);
|
||||
_firstTimeOpened = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Profiling;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -19,7 +20,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
private readonly Control[] _monitors = new Control[Enum.GetNames<DebugMonitor>().Length];
|
||||
|
||||
//TODO: Think about a factory for this
|
||||
public DebugMonitors(IGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
|
||||
public DebugMonitors(IClientGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
|
||||
IInputManager inputManager, IStateManager stateManager, IClyde displayManager, IClientNetManager netManager,
|
||||
IMapManager mapManager)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -10,13 +11,13 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
public sealed class DebugTimePanel : PanelContainer
|
||||
{
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly IClientGameTiming _gameTiming;
|
||||
private readonly IClientGameStateManager _gameState;
|
||||
|
||||
private readonly char[] _textBuffer = new char[256];
|
||||
private readonly Label _contents;
|
||||
|
||||
public DebugTimePanel(IGameTiming gameTiming, IClientGameStateManager gameState)
|
||||
public DebugTimePanel(IClientGameTiming gameTiming, IClientGameStateManager gameState)
|
||||
{
|
||||
_gameTiming = gameTiming;
|
||||
_gameState = gameState;
|
||||
@@ -53,7 +54,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
// This is why there's a -1 on Pred:.
|
||||
|
||||
_contents.TextMemory = FormatHelpers.FormatIntoMem(_textBuffer,
|
||||
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}/{_gameTiming.CurTick - 1}, CurServerTick: {_gameState.CurServerTick}, Pred: {_gameTiming.CurTick.Value - _gameState.CurServerTick.Value - 1}
|
||||
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}, LastProcessed: {_gameTiming.LastProcessedTick}, LastRealTick: {_gameTiming.LastRealTick}, Pred: {_gameTiming.CurTick.Value - _gameTiming.LastRealTick.Value - 1}
|
||||
CurTime: {_gameTiming.CurTime:hh\:mm\:ss\.ff}, RealTime: {_gameTiming.RealTime:hh\:mm\:ss\.ff}, CurFrame: {_gameTiming.CurFrame}
|
||||
ServerTime: {_gameTiming.ServerTime}, TickTimingAdjustment: {_gameTiming.TickTimingAdjustment}");
|
||||
}
|
||||
|
||||
11
Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs
Normal file
11
Robust.Client/UserInterface/CustomControls/DoNotMeasure.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
internal sealed class DoNotMeasure : Control
|
||||
{
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Diagnostics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
[DebuggerDisplay("spawnbutton {" + nameof(Index) + "}")]
|
||||
public sealed class EntitySpawnButton : Control
|
||||
{
|
||||
public string PrototypeID => Prototype.ID;
|
||||
public EntityPrototype Prototype { get; set; } = default!;
|
||||
public Button ActualButton { get; private set; }
|
||||
public Label EntityLabel { get; private set; }
|
||||
public LayeredTextureRect EntityTextureRects { get; private set; }
|
||||
public int Index { get; set; }
|
||||
|
||||
public EntitySpawnButton()
|
||||
{
|
||||
AddChild(ActualButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
});
|
||||
|
||||
AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
(EntityTextureRects = new LayeredTextureRect
|
||||
{
|
||||
MinSize = (32, 32),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||
CanShrink = true
|
||||
}),
|
||||
(EntityLabel = new Label
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
Text = "Backpack",
|
||||
ClipText = true
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
public sealed class EntitySpawnWindow : DefaultWindow
|
||||
{
|
||||
private readonly IPlacementManager placementManager;
|
||||
private readonly IPrototypeManager prototypeManager;
|
||||
private readonly IResourceCache resourceCache;
|
||||
|
||||
private BoxContainer MainVBox;
|
||||
private PrototypeListContainer PrototypeList;
|
||||
private LineEdit SearchBar;
|
||||
private OptionButton OverrideMenu;
|
||||
private Button ClearButton;
|
||||
private Button EraseButton;
|
||||
private Label RotationLabel;
|
||||
|
||||
private EntitySpawnButton MeasureButton;
|
||||
|
||||
// List of prototypes that are visible based on current filter criteria.
|
||||
private readonly List<EntityPrototype> _filteredPrototypes = new();
|
||||
|
||||
// The indices of the visible prototypes last time UpdateVisiblePrototypes was ran.
|
||||
// This is inclusive, so end is the index of the last prototype, not right after it.
|
||||
private (int start, int end) _lastPrototypeIndices;
|
||||
|
||||
private static readonly string[] initOpts =
|
||||
{
|
||||
"Default",
|
||||
"PlaceFree",
|
||||
"PlaceNearby",
|
||||
"SnapgridCenter",
|
||||
"SnapgridBorder",
|
||||
"AlignSimilar",
|
||||
"AlignTileAny",
|
||||
"AlignTileEmpty",
|
||||
"AlignTileNonDense",
|
||||
"AlignTileDense",
|
||||
"AlignWall",
|
||||
"AlignWallProper",
|
||||
};
|
||||
|
||||
private EntitySpawnButton? SelectedButton;
|
||||
private EntityPrototype? SelectedPrototype;
|
||||
|
||||
public EntitySpawnWindow(IPlacementManager placementManager,
|
||||
IPrototypeManager prototypeManager,
|
||||
IResourceCache resourceCache)
|
||||
{
|
||||
this.placementManager = placementManager;
|
||||
this.prototypeManager = prototypeManager;
|
||||
this.resourceCache = resourceCache;
|
||||
|
||||
Title = Loc.GetString("entity-spawn-window-title");
|
||||
|
||||
|
||||
SetSize = (250, 300);
|
||||
MinSize = (250, 200);
|
||||
|
||||
Contents.AddChild(MainVBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Name = "AAAAAA",
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
(SearchBar = new LineEdit
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
PlaceHolder = Loc.GetString("entity-spawn-window-search-bar-placeholder")
|
||||
}),
|
||||
|
||||
(ClearButton = new Button
|
||||
{
|
||||
Disabled = true,
|
||||
Text = Loc.GetString("entity-spawn-window-clear-button"),
|
||||
})
|
||||
}
|
||||
},
|
||||
new ScrollContainer
|
||||
{
|
||||
MinSize = new Vector2(200.0f, 0.0f),
|
||||
VerticalExpand = true,
|
||||
Children =
|
||||
{
|
||||
(PrototypeList = new PrototypeListContainer())
|
||||
}
|
||||
},
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
(EraseButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
Text = Loc.GetString("entity-spawn-window-erase-button-text")
|
||||
}),
|
||||
|
||||
(OverrideMenu = new OptionButton
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
ToolTip = Loc.GetString("entity-spawn-window-override-menu-tooltip")
|
||||
})
|
||||
}
|
||||
},
|
||||
(RotationLabel = new Label()),
|
||||
new DoNotMeasure
|
||||
{
|
||||
Visible = false,
|
||||
Children =
|
||||
{
|
||||
(MeasureButton = new EntitySpawnButton())
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MeasureButton.Measure(Vector2.Infinity);
|
||||
|
||||
for (var i = 0; i < initOpts.Length; i++)
|
||||
{
|
||||
OverrideMenu.AddItem(initOpts[i], i);
|
||||
}
|
||||
|
||||
EraseButton.Pressed = placementManager.Eraser;
|
||||
EraseButton.OnToggled += OnEraseButtonToggled;
|
||||
OverrideMenu.OnItemSelected += OnOverrideMenuItemSelected;
|
||||
SearchBar.OnTextChanged += OnSearchBarTextChanged;
|
||||
ClearButton.OnPressed += OnClearButtonPressed;
|
||||
|
||||
BuildEntityList();
|
||||
|
||||
this.placementManager.PlacementChanged += OnPlacementCanceled;
|
||||
this.placementManager.DirectionChanged += OnDirectionChanged;
|
||||
UpdateDirectionLabel();
|
||||
|
||||
OnClose += OnWindowClosed;
|
||||
|
||||
SearchBar.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing) return;
|
||||
|
||||
if (EraseButton.Pressed)
|
||||
placementManager.Clear();
|
||||
|
||||
placementManager.PlacementChanged -= OnPlacementCanceled;
|
||||
placementManager.DirectionChanged -= OnDirectionChanged;
|
||||
}
|
||||
|
||||
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
placementManager.Clear();
|
||||
BuildEntityList(args.Text);
|
||||
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
|
||||
}
|
||||
|
||||
private void OnOverrideMenuItemSelected(OptionButton.ItemSelectedEventArgs args)
|
||||
{
|
||||
OverrideMenu.SelectId(args.Id);
|
||||
|
||||
if (placementManager.CurrentMode != null)
|
||||
{
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = initOpts[args.Id],
|
||||
EntityType = placementManager.CurrentPermission!.EntityType,
|
||||
Range = 2,
|
||||
IsTile = placementManager.CurrentPermission.IsTile
|
||||
};
|
||||
|
||||
placementManager.Clear();
|
||||
placementManager.BeginPlacing(newObjInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
placementManager.Clear();
|
||||
SearchBar.Clear();
|
||||
BuildEntityList("");
|
||||
}
|
||||
|
||||
private void OnEraseButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
placementManager.Clear();
|
||||
// Only toggle the eraser back if the button is pressed.
|
||||
if(args.Pressed)
|
||||
placementManager.ToggleEraser();
|
||||
// clearing will toggle the erase button off...
|
||||
args.Button.Pressed = args.Pressed;
|
||||
OverrideMenu.Disabled = args.Pressed;
|
||||
}
|
||||
|
||||
private void BuildEntityList(string? searchStr = null)
|
||||
{
|
||||
_filteredPrototypes.Clear();
|
||||
PrototypeList.RemoveAllChildren();
|
||||
// Reset last prototype indices so it automatically updates the entire list.
|
||||
_lastPrototypeIndices = (0, -1);
|
||||
PrototypeList.RemoveAllChildren();
|
||||
SelectedButton = null;
|
||||
searchStr = searchStr?.ToLowerInvariant();
|
||||
|
||||
foreach (var prototype in prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (prototype.NoSpawn || prototype.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (searchStr != null && !_doesPrototypeMatchSearch(prototype, searchStr))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_filteredPrototypes.Add(prototype);
|
||||
}
|
||||
|
||||
_filteredPrototypes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
PrototypeList.TotalItemCount = _filteredPrototypes.Count;
|
||||
}
|
||||
|
||||
private void UpdateVisiblePrototypes()
|
||||
{
|
||||
// Update visible buttons in the prototype list.
|
||||
|
||||
// Calculate index of first prototype to render based on current scroll.
|
||||
var height = MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation;
|
||||
var offset = Math.Max(-PrototypeList.Position.Y, 0);
|
||||
var startIndex = (int) Math.Floor(offset / height);
|
||||
PrototypeList.ItemOffset = startIndex;
|
||||
|
||||
var (prevStart, prevEnd) = _lastPrototypeIndices;
|
||||
|
||||
// Calculate index of final one.
|
||||
var endIndex = startIndex - 1;
|
||||
var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button.
|
||||
|
||||
while (spaceUsed < PrototypeList.Parent!.Height)
|
||||
{
|
||||
spaceUsed += height;
|
||||
endIndex += 1;
|
||||
}
|
||||
|
||||
endIndex = Math.Min(endIndex, _filteredPrototypes.Count - 1);
|
||||
|
||||
if (endIndex == prevEnd && startIndex == prevStart)
|
||||
{
|
||||
// Nothing changed so bye.
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPrototypeIndices = (startIndex, endIndex);
|
||||
|
||||
// Delete buttons at the start of the list that are no longer visible (scrolling down).
|
||||
for (var i = prevStart; i < startIndex && i <= prevEnd; i++)
|
||||
{
|
||||
var control = (EntitySpawnButton) PrototypeList.GetChild(0);
|
||||
DebugTools.Assert(control.Index == i);
|
||||
PrototypeList.RemoveChild(control);
|
||||
}
|
||||
|
||||
// Delete buttons at the end of the list that are no longer visible (scrolling up).
|
||||
for (var i = prevEnd; i > endIndex && i >= prevStart; i--)
|
||||
{
|
||||
var control = (EntitySpawnButton) PrototypeList.GetChild(PrototypeList.ChildCount - 1);
|
||||
DebugTools.Assert(control.Index == i);
|
||||
PrototypeList.RemoveChild(control);
|
||||
}
|
||||
|
||||
// Create buttons at the start of the list that are now visible (scrolling up).
|
||||
for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--)
|
||||
{
|
||||
InsertEntityButton(_filteredPrototypes[i], true, i);
|
||||
}
|
||||
|
||||
// Create buttons at the end of the list that are now visible (scrolling down).
|
||||
for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++)
|
||||
{
|
||||
InsertEntityButton(_filteredPrototypes[i], false, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a spawn button and insert it into the start or end of the list.
|
||||
private void InsertEntityButton(EntityPrototype prototype, bool insertFirst, int index)
|
||||
{
|
||||
var button = new EntitySpawnButton
|
||||
{
|
||||
Prototype = prototype,
|
||||
Index = index // We track this index purely for debugging.
|
||||
};
|
||||
button.ActualButton.OnToggled += OnItemButtonToggled;
|
||||
var entityLabelText = string.IsNullOrEmpty(prototype.Name) ? prototype.ID : prototype.Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(prototype.EditorSuffix))
|
||||
{
|
||||
entityLabelText += $" [{prototype.EditorSuffix}]";
|
||||
}
|
||||
|
||||
button.EntityLabel.Text = entityLabelText;
|
||||
|
||||
if (prototype == SelectedPrototype)
|
||||
{
|
||||
SelectedButton = button;
|
||||
SelectedButton.ActualButton.Pressed = true;
|
||||
}
|
||||
|
||||
var rect = button.EntityTextureRects;
|
||||
rect.Textures = SpriteComponent.GetPrototypeTextures(prototype, resourceCache).Select(o => o.Default).ToList();
|
||||
|
||||
PrototypeList.AddChild(button);
|
||||
if (insertFirst)
|
||||
{
|
||||
button.SetPositionInParent(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _doesPrototypeMatchSearch(EntityPrototype prototype, string searchStr)
|
||||
{
|
||||
if (prototype.ID.ToLowerInvariant().Contains(searchStr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prototype.EditorSuffix != null &&
|
||||
prototype.EditorSuffix.Contains(searchStr, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(prototype.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prototype.Name.ToLowerInvariant().Contains(searchStr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnItemButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
var item = (EntitySpawnButton) args.Button.Parent!;
|
||||
if (SelectedButton == item)
|
||||
{
|
||||
SelectedButton = null;
|
||||
SelectedPrototype = null;
|
||||
placementManager.Clear();
|
||||
return;
|
||||
}
|
||||
else if (SelectedButton != null)
|
||||
{
|
||||
SelectedButton.ActualButton.Pressed = false;
|
||||
}
|
||||
|
||||
SelectedButton = null;
|
||||
SelectedPrototype = null;
|
||||
|
||||
var overrideMode = initOpts[OverrideMenu.SelectedId];
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = overrideMode != "Default" ? overrideMode : item.Prototype.PlacementMode,
|
||||
EntityType = item.PrototypeID,
|
||||
Range = 2,
|
||||
IsTile = false
|
||||
};
|
||||
|
||||
placementManager.BeginPlacing(newObjInfo);
|
||||
|
||||
SelectedButton = item;
|
||||
SelectedPrototype = item.Prototype;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateVisiblePrototypes();
|
||||
}
|
||||
|
||||
private sealed class PrototypeListContainer : Container
|
||||
{
|
||||
// Quick and dirty container to do virtualization of the list.
|
||||
// Basically, get total item count and offset to put the current buttons at.
|
||||
// Get a constant minimum height and move the buttons in the list up to match the scrollbar.
|
||||
private int _totalItemCount;
|
||||
private int _itemOffset;
|
||||
|
||||
public int TotalItemCount
|
||||
{
|
||||
get => _totalItemCount;
|
||||
set
|
||||
{
|
||||
_totalItemCount = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
public int ItemOffset
|
||||
{
|
||||
get => _itemOffset;
|
||||
set
|
||||
{
|
||||
_itemOffset = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
public const float Separation = 2;
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
if (ChildCount == 0)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
var first = GetChild(0);
|
||||
|
||||
first.Measure(availableSize);
|
||||
var (minX, minY) = first.DesiredSize;
|
||||
|
||||
return (minX, minY * TotalItemCount + (TotalItemCount - 1) * Separation);
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
if (ChildCount == 0)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
var first = GetChild(0);
|
||||
|
||||
var height = first.DesiredSize.Y;
|
||||
var offset = ItemOffset * height + (ItemOffset - 1) * Separation;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Arrange(UIBox2.FromDimensions(0, offset, finalSize.X, height));
|
||||
offset += Separation + height;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("spawnbutton {" + nameof(Index) + "}")]
|
||||
private sealed class EntitySpawnButton : Control
|
||||
{
|
||||
public string PrototypeID => Prototype.ID;
|
||||
public EntityPrototype Prototype { get; set; } = default!;
|
||||
public Button ActualButton { get; private set; }
|
||||
public Label EntityLabel { get; private set; }
|
||||
public LayeredTextureRect EntityTextureRects { get; private set; }
|
||||
public int Index { get; set; }
|
||||
|
||||
public EntitySpawnButton()
|
||||
{
|
||||
AddChild(ActualButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
});
|
||||
|
||||
AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
(EntityTextureRects = new LayeredTextureRect
|
||||
{
|
||||
MinSize = (32, 32),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||
CanShrink = true
|
||||
}),
|
||||
(EntityLabel = new Label
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
Text = "Backpack",
|
||||
ClipText = true
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed()
|
||||
{
|
||||
if (SelectedButton != null)
|
||||
{
|
||||
SelectedButton.ActualButton.Pressed = false;
|
||||
SelectedButton = null;
|
||||
}
|
||||
placementManager.Clear();
|
||||
}
|
||||
|
||||
private void OnPlacementCanceled(object? sender, EventArgs e)
|
||||
{
|
||||
if (SelectedButton != null)
|
||||
{
|
||||
SelectedButton.ActualButton.Pressed = false;
|
||||
SelectedButton = null;
|
||||
}
|
||||
|
||||
EraseButton.Pressed = false;
|
||||
OverrideMenu.Disabled = false;
|
||||
}
|
||||
|
||||
private void OnDirectionChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateDirectionLabel();
|
||||
}
|
||||
|
||||
private void UpdateDirectionLabel()
|
||||
{
|
||||
RotationLabel.Text = placementManager.Direction.ToString();
|
||||
}
|
||||
|
||||
private sealed class DoNotMeasure : Control
|
||||
{
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<EntitySpawnWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
Title="{Loc entity-spawn-window-title}"
|
||||
SetSize="250 300"
|
||||
MinSize="250 200">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc entity-spawn-window-search-bar-placeholder}"/>
|
||||
<Button Name="ClearButton" Access="Public" Disabled="True" Text="{Loc entity-spawn-window-clear-button}" />
|
||||
</BoxContainer>
|
||||
<ScrollContainer Name="PrototypeScrollContainer" Access="Public" MinSize="200 0" VerticalExpand="True">
|
||||
<PrototypeListContainer Name="PrototypeList" Access="Public"/>
|
||||
</ScrollContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc entity-spawn-window-erase-button-text}"/>
|
||||
<OptionButton Name="OverrideMenu" Access="Public" HorizontalExpand="True" ToolTip="{Loc entity-spawn-window-override-menu-tooltip}" />
|
||||
</BoxContainer>
|
||||
<Label Name="RotationLabel" Access="Public"/>
|
||||
<DoNotMeasure Visible="False">
|
||||
<EntitySpawnButton Name="MeasureButton" Access="Public" />
|
||||
</DoNotMeasure>
|
||||
</BoxContainer>
|
||||
</EntitySpawnWindow>
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class EntitySpawnWindow : DefaultWindow
|
||||
{
|
||||
public static readonly string[] InitOpts =
|
||||
{
|
||||
"Default",
|
||||
"PlaceFree",
|
||||
"PlaceNearby",
|
||||
"SnapgridCenter",
|
||||
"SnapgridBorder",
|
||||
"AlignSimilar",
|
||||
"AlignTileAny",
|
||||
"AlignTileEmpty",
|
||||
"AlignTileNonDense",
|
||||
"AlignTileDense",
|
||||
"AlignWall",
|
||||
"AlignWallProper",
|
||||
};
|
||||
|
||||
public EntitySpawnButton? SelectedButton;
|
||||
public EntityPrototype? SelectedPrototype;
|
||||
|
||||
public EntitySpawnWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
MeasureButton.Measure(Vector2.Infinity);
|
||||
|
||||
for (var i = 0; i < InitOpts.Length; i++)
|
||||
{
|
||||
OverrideMenu.AddItem(InitOpts[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a spawn button and insert it into the start or end of the list.
|
||||
public EntitySpawnButton InsertEntityButton(EntityPrototype prototype, bool insertFirst, int index, List<Texture> textures)
|
||||
{
|
||||
var button = new EntitySpawnButton
|
||||
{
|
||||
Prototype = prototype,
|
||||
Index = index // We track this index purely for debugging.
|
||||
};
|
||||
var entityLabelText = string.IsNullOrEmpty(prototype.Name) ? prototype.ID : prototype.Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(prototype.EditorSuffix))
|
||||
{
|
||||
entityLabelText += $" [{prototype.EditorSuffix}]";
|
||||
}
|
||||
|
||||
button.EntityLabel.Text = entityLabelText;
|
||||
|
||||
if (prototype == SelectedPrototype)
|
||||
{
|
||||
SelectedButton = button;
|
||||
SelectedButton.ActualButton.Pressed = true;
|
||||
}
|
||||
|
||||
var rect = button.EntityTextureRects;
|
||||
rect.Textures = textures;
|
||||
|
||||
PrototypeList.AddChild(button);
|
||||
if (insertFirst)
|
||||
{
|
||||
button.SetPositionInParent(0);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
public sealed class PrototypeListContainer : Container
|
||||
{
|
||||
// Quick and dirty container to do virtualization of the list.
|
||||
// Basically, get total item count and offset to put the current buttons at.
|
||||
// Get a constant minimum height and move the buttons in the list up to match the scrollbar.
|
||||
private int _totalItemCount;
|
||||
private int _itemOffset;
|
||||
|
||||
public int TotalItemCount
|
||||
{
|
||||
get => _totalItemCount;
|
||||
set
|
||||
{
|
||||
_totalItemCount = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
public int ItemOffset
|
||||
{
|
||||
get => _itemOffset;
|
||||
set
|
||||
{
|
||||
_itemOffset = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
public const float Separation = 2;
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
if (ChildCount == 0)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
var first = GetChild(0);
|
||||
|
||||
first.Measure(availableSize);
|
||||
var (minX, minY) = first.DesiredSize;
|
||||
|
||||
return (minX, minY * TotalItemCount + (TotalItemCount - 1) * Separation);
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
if (ChildCount == 0)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
var first = GetChild(0);
|
||||
|
||||
var height = first.DesiredSize.Y;
|
||||
var offset = ItemOffset * height + (ItemOffset - 1) * Separation;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Arrange(UIBox2.FromDimensions(0, offset, finalSize.X, height));
|
||||
offset += Separation + height;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
public sealed class TileSpawnWindow : DefaultWindow
|
||||
{
|
||||
private readonly ITileDefinitionManager __tileDefinitionManager;
|
||||
private readonly IPlacementManager _placementManager;
|
||||
private readonly IResourceCache _resourceCache;
|
||||
|
||||
private ItemList TileList;
|
||||
private LineEdit SearchBar;
|
||||
private Button ClearButton;
|
||||
|
||||
private readonly List<ITileDefinition> _shownItems = new();
|
||||
|
||||
private bool _clearingSelections;
|
||||
|
||||
public TileSpawnWindow(ITileDefinitionManager tileDefinitionManager, IPlacementManager placementManager,
|
||||
IResourceCache resourceCache)
|
||||
{
|
||||
__tileDefinitionManager = tileDefinitionManager;
|
||||
_placementManager = placementManager;
|
||||
_resourceCache = resourceCache;
|
||||
|
||||
var vBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
Contents.AddChild(vBox);
|
||||
var hBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
vBox.AddChild(hBox);
|
||||
SearchBar = new LineEdit {PlaceHolder = "Search", HorizontalExpand = true};
|
||||
SearchBar.OnTextChanged += OnSearchBarTextChanged;
|
||||
hBox.AddChild(SearchBar);
|
||||
|
||||
ClearButton = new Button {Text = "Clear"};
|
||||
ClearButton.OnPressed += OnClearButtonPressed;
|
||||
hBox.AddChild(ClearButton);
|
||||
|
||||
TileList = new ItemList {VerticalExpand = true};
|
||||
TileList.OnItemSelected += TileListOnOnItemSelected;
|
||||
TileList.OnItemDeselected += TileListOnOnItemDeselected;
|
||||
vBox.AddChild(TileList);
|
||||
|
||||
BuildTileList();
|
||||
|
||||
_placementManager.PlacementChanged += OnPlacementCanceled;
|
||||
|
||||
OnClose += OnWindowClosed;
|
||||
|
||||
Title = "Place Tiles";
|
||||
SearchBar.GrabKeyboardFocus();
|
||||
|
||||
SetSize = (300, 300);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_placementManager.PlacementChanged -= OnPlacementCanceled;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
TileList.ClearSelected();
|
||||
_placementManager.Clear();
|
||||
SearchBar.Clear();
|
||||
BuildTileList("");
|
||||
ClearButton.Disabled = true;
|
||||
}
|
||||
|
||||
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
TileList.ClearSelected();
|
||||
_placementManager.Clear();
|
||||
BuildTileList(args.Text);
|
||||
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
|
||||
}
|
||||
|
||||
private void BuildTileList(string? searchStr = null)
|
||||
{
|
||||
TileList.Clear();
|
||||
|
||||
IEnumerable<ITileDefinition> tileDefs = __tileDefinitionManager;
|
||||
|
||||
if (!string.IsNullOrEmpty(searchStr))
|
||||
{
|
||||
tileDefs = tileDefs.Where(s =>
|
||||
s.Name.IndexOf(searchStr, StringComparison.InvariantCultureIgnoreCase) >= 0 ||
|
||||
s.ID.IndexOf(searchStr, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
}
|
||||
|
||||
tileDefs = tileDefs.OrderBy(d => d.Name);
|
||||
|
||||
_shownItems.Clear();
|
||||
_shownItems.AddRange(tileDefs);
|
||||
|
||||
foreach (var entry in _shownItems)
|
||||
{
|
||||
Texture? texture = null;
|
||||
var path = entry.Sprite?.ToString();
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
texture = _resourceCache.GetResource<TextureResource>(path);
|
||||
}
|
||||
TileList.AddItem(entry.Name, texture);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed()
|
||||
{
|
||||
TileList.ClearSelected();
|
||||
_placementManager.Clear();
|
||||
}
|
||||
|
||||
private void OnPlacementCanceled(object? sender, EventArgs e)
|
||||
{
|
||||
_clearingSelections = true;
|
||||
TileList.ClearSelected();
|
||||
_clearingSelections = false;
|
||||
}
|
||||
private void TileListOnOnItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
var definition = _shownItems[args.ItemIndex];
|
||||
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = "AlignTileAny",
|
||||
TileType = definition.TileId,
|
||||
Range = 400,
|
||||
IsTile = true
|
||||
};
|
||||
|
||||
_placementManager.BeginPlacing(newObjInfo);
|
||||
}
|
||||
|
||||
private void TileListOnOnItemDeselected(ItemList.ItemListDeselectedEventArgs args)
|
||||
{
|
||||
if (_clearingSelections)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_placementManager.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<TileSpawnWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
Title="Place Tiles"
|
||||
SetSize="300 300"
|
||||
MinSize="300 200">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="Search"/>
|
||||
<Button Name="ClearButton" Access="Public" Text="Clear"/>
|
||||
</BoxContainer>
|
||||
<ItemList Name="TileList" Access="Public" VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</TileSpawnWindow>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TileSpawnWindow : DefaultWindow
|
||||
{
|
||||
public TileSpawnWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<Button Name="RefreshButton" Text="{Loc 'dev-window-ui-refresh'}" />
|
||||
</BoxContainer>
|
||||
-->
|
||||
<Button Name="ControlPicker" ToggleMode="True" Text="Inspect" />
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="ControlTreeRoot" Orientation="Vertical" MouseFilter="Stop" />
|
||||
</ScrollContainer>
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console.Commands;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DevWindowTabUI : Control
|
||||
{
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
|
||||
public Control? SelectedControl { get; private set; }
|
||||
private Dictionary<Control, DevWindowUITreeEntry> ControlMap { get; } = new();
|
||||
private Control? LastHoveredControl { get; set; }
|
||||
|
||||
public event Action? SelectedControlChanged;
|
||||
|
||||
@@ -25,12 +32,71 @@ namespace Robust.Client.UserInterface
|
||||
private void InitializeComponent()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
ControlTreeRoot.OnKeyBindDown += ControlTreeRootOnOnKeyBindDown;
|
||||
ControlTreeRoot.OnKeyBindDown += ControlTreeRootOnKeyBindDown;
|
||||
RefreshPropertiesButton.OnPressed += _ => Refresh();
|
||||
}
|
||||
|
||||
private void ControlTreeRootOnOnKeyBindDown(GUIBoundKeyEventArgs obj)
|
||||
private Control? GetControlUnderMouse()
|
||||
{
|
||||
return UserInterfaceManager.MouseGetControl(_input.MouseScreenPosition);
|
||||
}
|
||||
|
||||
private void OnMouseMove(MouseMoveEventArgs obj)
|
||||
{
|
||||
if (!ControlPicker.Pressed)
|
||||
return;
|
||||
|
||||
var controlUnderMouse = GetControlUnderMouse();
|
||||
if (LastHoveredControl == controlUnderMouse)
|
||||
return;
|
||||
|
||||
LastHoveredControl = controlUnderMouse;
|
||||
|
||||
Stack<Control> entryStack = new();
|
||||
DevWindowUITreeEntry? entry = null;
|
||||
var control = controlUnderMouse;
|
||||
while (control != null)
|
||||
{
|
||||
if (ControlMap.TryGetValue(control, out entry))
|
||||
break;
|
||||
|
||||
entryStack.Push(control);
|
||||
control = control.Parent;
|
||||
}
|
||||
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
if (entryStack.Count > 0)
|
||||
entry.Open();
|
||||
|
||||
foreach (var subEntry in entryStack)
|
||||
{
|
||||
ControlMap[subEntry].Open();
|
||||
}
|
||||
|
||||
SelectControl(controlUnderMouse);
|
||||
}
|
||||
|
||||
private bool OnUIKeyBindStateChanged(BoundKeyEventArgs arg)
|
||||
{
|
||||
if (arg.Function != EngineKeyFunctions.UIClick)
|
||||
return false;
|
||||
|
||||
if (!ControlPicker.Pressed)
|
||||
return false;
|
||||
|
||||
var control = GetControlUnderMouse();
|
||||
if (control == ControlPicker)
|
||||
return false;
|
||||
|
||||
ControlPicker.Pressed = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ControlTreeRootOnKeyBindDown(GUIBoundKeyEventArgs obj)
|
||||
{
|
||||
if (obj.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
@@ -52,6 +118,8 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
UserInterfaceManager.OnPostDrawUIRoot += OnPostDrawUIRoot;
|
||||
_clyde.MouseMove += OnMouseMove;
|
||||
_input.UIKeyBindStateChanged += OnUIKeyBindStateChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
@@ -60,7 +128,10 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
// Clear tree children.
|
||||
ControlTreeRoot.RemoveAllChildren();
|
||||
|
||||
UserInterfaceManager.OnPostDrawUIRoot -= OnPostDrawUIRoot;
|
||||
_clyde.MouseMove -= OnMouseMove;
|
||||
_input.UIKeyBindStateChanged -= OnUIKeyBindStateChanged;
|
||||
}
|
||||
|
||||
private void OnPostDrawUIRoot(PostDrawUIRootEventArgs eventArgs)
|
||||
@@ -72,10 +143,17 @@ namespace Robust.Client.UserInterface
|
||||
eventArgs.DrawingHandle.DrawRect(rect, Color.Cyan.WithAlpha(0.35f));
|
||||
}
|
||||
|
||||
internal void EntryAdded(DevWindowUITreeEntry entry)
|
||||
{
|
||||
ControlMap[entry.VisControl] = entry;
|
||||
}
|
||||
|
||||
public void EntryRemoved(DevWindowUITreeEntry entry)
|
||||
{
|
||||
if (SelectedControl == entry.VisControl)
|
||||
SelectControl(null);
|
||||
|
||||
ControlMap.Remove(entry.VisControl);
|
||||
}
|
||||
|
||||
public void SelectControl(Control? control)
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
var typeName = visControl.GetType().Name;
|
||||
ControlName.Text = visControl.Name == null ? typeName : $"{visControl.Name} ({typeName})";
|
||||
|
||||
_tab.EntryAdded(this);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
@@ -100,16 +102,23 @@ namespace Robust.Client.UserInterface
|
||||
entry.SetPositionInParent(eventArgs.NewIndex);
|
||||
}
|
||||
|
||||
internal void Open()
|
||||
{
|
||||
DebugTools.Assert(ChildEntryContainer.ChildCount == 0);
|
||||
|
||||
ExpandButton.Pressed = true;
|
||||
|
||||
foreach (var child in VisControl.Children)
|
||||
{
|
||||
ChildEntryContainer.AddChild(new DevWindowUITreeEntry(_tab, child));
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpandButtonOnOnToggled(BaseButton.ButtonToggledEventArgs obj)
|
||||
{
|
||||
if (obj.Pressed)
|
||||
{
|
||||
DebugTools.Assert(ChildEntryContainer.ChildCount == 0);
|
||||
|
||||
foreach (var child in VisControl.Children)
|
||||
{
|
||||
ChildEntryContainer.AddChild(new DevWindowUITreeEntry(_tab, child));
|
||||
}
|
||||
Open();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
14
Robust.Client/UserInterface/IUserInterfaceManager.Screens.cs
Normal file
14
Robust.Client/UserInterface/IUserInterfaceManager.Screens.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
public partial interface IUserInterfaceManager
|
||||
{
|
||||
public UIScreen? ActiveScreen { get; }
|
||||
public void LoadScreen<T>() where T : UIScreen, new();
|
||||
internal void LoadScreenInternal(Type type);
|
||||
public void UnloadScreen();
|
||||
public T? GetActiveUIWidgetOrNull<T>() where T : UIWidget, new();
|
||||
public T GetActiveUIWidget<T>() where T : UIWidget, new();
|
||||
}
|
||||
13
Robust.Client/UserInterface/IUserInterfaceManager.Themes.cs
Normal file
13
Robust.Client/UserInterface/IUserInterfaceManager.Themes.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
public partial interface IUserInterfaceManager
|
||||
{
|
||||
public UITheme CurrentTheme { get;}
|
||||
public UITheme GetTheme(string name);
|
||||
public UITheme GetThemeOrDefault(string name);
|
||||
public void SetActiveTheme(string themeName);
|
||||
public UITheme DefaultTheme { get; }
|
||||
public void SetDefaultTheme(string themeId);
|
||||
}
|
||||
21
Robust.Client/UserInterface/IUserInterfaceManager.Windows.cs
Normal file
21
Robust.Client/UserInterface/IUserInterfaceManager.Windows.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
public partial interface IUserInterfaceManager
|
||||
{
|
||||
public T CreatePopup<T>() where T : Popup, new();
|
||||
public bool RemoveFirstPopup<T>() where T : Popup, new();
|
||||
public bool TryGetFirstPopup<T>(out T? popup) where T : Popup, new();
|
||||
public bool TryGetFirstPopup(Type type, out Popup? popup);
|
||||
|
||||
public bool RemoveFirstWindow<T>() where T : BaseWindow, new();
|
||||
public T CreateWindow<T>() where T : BaseWindow, new();
|
||||
|
||||
public void ClearWindows();
|
||||
public T GetFirstWindow<T>() where T : BaseWindow, new();
|
||||
public bool TryGetFirstWindow<T>(out T? window) where T : BaseWindow, new();
|
||||
public bool TryGetFirstWindow(Type type, out BaseWindow? window);
|
||||
}
|
||||
@@ -8,9 +8,9 @@ using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
public interface IUserInterfaceManager
|
||||
public partial interface IUserInterfaceManager
|
||||
{
|
||||
UITheme ThemeDefaults { get; }
|
||||
InterfaceTheme ThemeDefaults { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Default style sheet that applies to all controls
|
||||
@@ -33,6 +33,7 @@ namespace Robust.Client.UserInterface
|
||||
/// happens. When focus is lost on a control, it always fires Control.ControlFocusExited.
|
||||
/// </summary>
|
||||
Control? ControlFocused { get; set; }
|
||||
public void PostInitialize();
|
||||
|
||||
ViewportContainer MainViewport { get; }
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ using Robust.Client.Graphics;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
// DON'T USE THESE
|
||||
// THEY'RE A BAD IDEA THAT NEEDS TO BE BURIED.
|
||||
//THIS IS BEING DEPRECIATED BECAUSE IT'S ASS!
|
||||
//UIThemes will be eventually fully replacing this functionality without giving you turbo space ass-cancer
|
||||
|
||||
/// <summary>
|
||||
/// Fallback theme system for GUI.
|
||||
/// </summary>
|
||||
public abstract class UITheme
|
||||
public abstract class InterfaceTheme
|
||||
{
|
||||
public abstract Font DefaultFont { get; }
|
||||
public abstract Font LabelFont { get; }
|
||||
@@ -17,7 +17,7 @@ namespace Robust.Client.UserInterface
|
||||
public abstract StyleBox LineEditBox { get; }
|
||||
}
|
||||
|
||||
public sealed class UIThemeDummy : UITheme
|
||||
public sealed class InterfaceThemeDummy : InterfaceTheme
|
||||
{
|
||||
public override Font DefaultFont { get; } = new DummyFont();
|
||||
public override Font LabelFont { get; } = new DummyFont();
|
||||
59
Robust.Client/UserInterface/Themes/UiTheme.cs
Normal file
59
Robust.Client/UserInterface/Themes/UiTheme.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface.Themes;
|
||||
|
||||
[Prototype("uiTheme")]
|
||||
public sealed class UITheme : IPrototype
|
||||
{ //this is used for ease of access
|
||||
public const string DefaultPath = "/Textures/Interface";
|
||||
public const string DefaultName = "Default";
|
||||
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("path")]
|
||||
private ResourcePath? _path;
|
||||
|
||||
[DataField("colors", readOnly: true)]
|
||||
public Dictionary<string, Color>? Colors { get; }
|
||||
public ResourcePath Path => _path == null ? new ResourcePath(DefaultPath+"/"+ID) : _path;
|
||||
|
||||
private void ValidateFilePath(IResourceCache resourceCache)
|
||||
{
|
||||
var foundFolders = resourceCache.ContentFindFiles(Path.ToRootedPath());
|
||||
if (!foundFolders.Any()) throw new Exception("UITheme: "+ID+" not found in resources!");
|
||||
}
|
||||
//helper to autoresolve dependencies
|
||||
public Texture ResolveTexture(string texturePath)
|
||||
{
|
||||
return ResolveTexture(IoCManager.Resolve<IResourceCache>(), texturePath);
|
||||
}
|
||||
public Texture ResolveTexture(IResourceCache cache, string texturePath)
|
||||
{
|
||||
return cache.TryGetResource<TextureResource>( new ResourcePath($"{Path}/{texturePath}.png"), out var texture) ? texture :
|
||||
cache.GetResource<TextureResource>($"{DefaultPath}/{DefaultName}/{texturePath}.png");
|
||||
}
|
||||
|
||||
public Color? ResolveColor(string colorName)
|
||||
{
|
||||
if (Colors == null) return null;
|
||||
return Colors.TryGetValue(colorName, out var color) ? color : IoCManager.Resolve<IUserInterfaceManager>().DefaultTheme.ResolveColor(colorName);
|
||||
}
|
||||
|
||||
public Color ResolveColorOrSpecified(string colorName, Color defaultColor = default)
|
||||
{
|
||||
var color = ResolveColor(colorName) ?? defaultColor;
|
||||
return color;
|
||||
}
|
||||
}
|
||||
189
Robust.Client/UserInterface/UIScreen.cs
Normal file
189
Robust.Client/UserInterface/UIScreen.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
[PublicAPI]
|
||||
public abstract class UIScreen : LayoutContainer
|
||||
{
|
||||
private IConfigurationManager _configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
|
||||
public Vector2i AutoscaleMaxResolution
|
||||
{
|
||||
get =>
|
||||
new(_configManager.GetCVar<int>("interface.resolutionAutoScaleUpperCutoffX"),
|
||||
_configManager.GetCVar<int>("interface.resolutionAutoScaleUpperCutoffY"));
|
||||
protected set
|
||||
{
|
||||
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", value.X);
|
||||
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffY", value.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2i AutoscaleMinResolution
|
||||
{
|
||||
get
|
||||
{
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
return new Vector2i(configManager.GetCVar<int>("interface.resolutionAutoScaleLowerCutoffX"),
|
||||
configManager.GetCVar<int>("interface.resolutionAutoScaleLowerCutoffY"));
|
||||
}
|
||||
protected set
|
||||
{
|
||||
_configManager.SetCVar("interface.resolutionAutoScaleLowerCutoffX", value.X);
|
||||
_configManager.SetCVar("interface.resolutionAutoScaleLowerCutoffY", value.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public float AutoscaleFloor
|
||||
{
|
||||
get
|
||||
{
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
return configManager.GetCVar<float>("interface.resolutionAutoScaleMinimum");
|
||||
}
|
||||
protected set { _configManager.SetCVar("interface.interface.resolutionAutoScaleMinimum", value); }
|
||||
}
|
||||
|
||||
private readonly Dictionary<Type, UIWidget> _widgets = new();
|
||||
|
||||
protected UIScreen()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Stretch;
|
||||
VerticalAlignment = VAlignment.Stretch;
|
||||
}
|
||||
|
||||
public T RegisterWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
if (_widgets.ContainsKey(typeof(T))) throw new Exception("Hud Widget not found");
|
||||
var newWidget = new T();
|
||||
return newWidget;
|
||||
}
|
||||
|
||||
public void RemoveWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
if (_widgets.TryGetValue(typeof(T), out var widget))
|
||||
{
|
||||
RemoveChild(widget);
|
||||
}
|
||||
|
||||
_widgets.Remove(typeof(T));
|
||||
}
|
||||
|
||||
internal void OnRemoved()
|
||||
{
|
||||
OnUnloaded();
|
||||
}
|
||||
|
||||
internal void OnAdded()
|
||||
{
|
||||
OnLoaded();
|
||||
}
|
||||
|
||||
public UIWidget? this[Type type]
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((type.IsAbstract) || !typeof(UIWidget).IsAssignableFrom(type))
|
||||
throw new Exception("Tried to fetch a non UI widget from UI Screen");
|
||||
_widgets.TryGetValue(type, out var widget);
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddWidget(UIWidget widget)
|
||||
{
|
||||
AddChild(widget);
|
||||
}
|
||||
|
||||
public T? GetWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
return (T?) _widgets.GetValueOrDefault(typeof(T));
|
||||
}
|
||||
|
||||
public T GetOrNewWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
if (!_widgets.TryGetValue(typeof(T), out var widget))
|
||||
{
|
||||
widget = new T();
|
||||
}
|
||||
|
||||
return (T) widget;
|
||||
}
|
||||
|
||||
public bool IsWidgetShown<T>() where T : UIWidget
|
||||
{
|
||||
return _widgets.TryGetValue(typeof(T), out var widget) && widget.Visible;
|
||||
}
|
||||
|
||||
public void ShowWidget<T>(bool show) where T : UIWidget
|
||||
{
|
||||
_widgets[typeof(T)].Visible = show;
|
||||
}
|
||||
|
||||
protected override void ChildAdded(Control newChild)
|
||||
{
|
||||
base.ChildAdded(newChild);
|
||||
|
||||
RegisterChildren(newChild);
|
||||
|
||||
if (newChild is not UIWidget widget) return;
|
||||
if (!_widgets.TryAdd(widget.GetType(), widget))
|
||||
throw new Exception("Tried to add duplicate widget to screen!");
|
||||
}
|
||||
|
||||
private void RegisterChildren(Control control)
|
||||
{
|
||||
foreach (var child in control.Children)
|
||||
{
|
||||
RegisterChildren(child);
|
||||
|
||||
if (child is not UIWidget widget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_widgets.TryAdd(widget.GetType(), widget))
|
||||
{
|
||||
throw new Exception("Tried to add duplicate widget to screen!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ChildRemoved(Control child)
|
||||
{
|
||||
base.ChildRemoved(child);
|
||||
RemoveChildren(child);
|
||||
if (child is not UIWidget widget) return;
|
||||
_widgets.Remove(child.GetType());
|
||||
}
|
||||
|
||||
private void RemoveChildren(Control control)
|
||||
{
|
||||
foreach (var child in control.Children)
|
||||
{
|
||||
RemoveChildren(child);
|
||||
|
||||
if (child is not UIWidget widget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_widgets.Remove(widget.GetType());
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnLoaded()
|
||||
{
|
||||
}
|
||||
|
||||
protected void OnUnloaded()
|
||||
{
|
||||
}
|
||||
}
|
||||
12
Robust.Client/UserInterface/UISystemDependencyAttribute.cs
Normal file
12
Robust.Client/UserInterface/UISystemDependencyAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute applied to EntitySystem-typed fields inside UIControllers that should be
|
||||
/// injected when the system becomes available.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public sealed class UISystemDependencyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
526
Robust.Client/UserInterface/UserInterfaceManager.Input.cs
Normal file
526
Robust.Client/UserInterface/UserInterfaceManager.Input.cs
Normal file
@@ -0,0 +1,526 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal partial class UserInterfaceManager
|
||||
{
|
||||
private float _tooltipTimer;
|
||||
private ICursor? _worldCursor;
|
||||
private bool _needUpdateActiveCursor;
|
||||
[ViewVariables] public Control? KeyboardFocused { get; private set; }
|
||||
|
||||
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
|
||||
|
||||
private Control? _controlFocused;
|
||||
[ViewVariables]
|
||||
public Control? ControlFocused
|
||||
{
|
||||
get => _controlFocused;
|
||||
set
|
||||
{
|
||||
if (_controlFocused == value)
|
||||
return;
|
||||
_controlFocused?.ControlFocusExited();
|
||||
_controlFocused = value;
|
||||
}
|
||||
}
|
||||
|
||||
// set to null when not counting down
|
||||
private float? _tooltipDelay;
|
||||
private Tooltip _tooltip = default!;
|
||||
private bool showingTooltip;
|
||||
private Control? _suppliedTooltip;
|
||||
private const float TooltipDelay = 1;
|
||||
|
||||
private static (Control control, Vector2 rel)? MouseFindControlAtPos(Control control, Vector2 position)
|
||||
{
|
||||
for (var i = control.ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = control.GetChild(i);
|
||||
if (!child.Visible || child.RectClipContent && !child.PixelRect.Contains((Vector2i) position))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var maybeFoundOnChild = MouseFindControlAtPos(child, position - child.PixelPosition);
|
||||
if (maybeFoundOnChild != null)
|
||||
{
|
||||
return maybeFoundOnChild;
|
||||
}
|
||||
}
|
||||
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore && control.HasPoint(position / control.UIScale))
|
||||
{
|
||||
return (control, position);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void KeyBindDown(BoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.CloseModals && _modalStack.Count != 0)
|
||||
{
|
||||
bool closedAny = false;
|
||||
for (var i = _modalStack.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var top = _modalStack[i];
|
||||
|
||||
if (top is not Popup {CloseOnEscape: false})
|
||||
{
|
||||
RemoveModal(top);
|
||||
closedAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (closedAny)
|
||||
{
|
||||
args.Handle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
|
||||
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
|
||||
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindDown(ev));
|
||||
|
||||
if (guiArgs.Handled)
|
||||
{
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
public void KeyBindUp(BoundKeyEventArgs args)
|
||||
{
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
|
||||
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindUp(ev));
|
||||
|
||||
// Always mark this as handled.
|
||||
// The only case it should not be is if we do not have a control to click on,
|
||||
// in which case we never reach this.
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
public void MouseMove(MouseMoveEventArgs mouseMoveEventArgs)
|
||||
{
|
||||
_resetTooltipTimer();
|
||||
// Update which control is considered hovered.
|
||||
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
|
||||
if (newHovered != CurrentlyHovered)
|
||||
{
|
||||
_clearTooltip();
|
||||
CurrentlyHovered?.MouseExited();
|
||||
CurrentlyHovered = newHovered;
|
||||
CurrentlyHovered?.MouseEntered();
|
||||
if (CurrentlyHovered != null)
|
||||
{
|
||||
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltipDelay = null;
|
||||
}
|
||||
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
|
||||
var target = ControlFocused ?? newHovered;
|
||||
if (target != null)
|
||||
{
|
||||
var pos = mouseMoveEventArgs.Position.Position;
|
||||
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / target.UIScale,
|
||||
target,
|
||||
pos / target.UIScale, mouseMoveEventArgs.Position,
|
||||
pos / target.UIScale - target.GlobalPosition,
|
||||
pos - target.GlobalPixelPosition);
|
||||
|
||||
_doMouseGuiInput(target, guiArgs, (c, ev) => c.MouseMove(ev));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActiveCursor()
|
||||
{
|
||||
// Consider mouse input focus first so that dragging windows don't act up etc.
|
||||
var cursorTarget = ControlFocused ?? CurrentlyHovered;
|
||||
|
||||
if (cursorTarget == null)
|
||||
{
|
||||
_clyde.SetCursor(_worldCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursorTarget.CustomCursorShape != null)
|
||||
{
|
||||
_clyde.SetCursor(cursorTarget.CustomCursorShape);
|
||||
return;
|
||||
}
|
||||
|
||||
var shape = cursorTarget.DefaultCursorShape switch
|
||||
{
|
||||
Control.CursorShape.Arrow => StandardCursorShape.Arrow,
|
||||
Control.CursorShape.IBeam => StandardCursorShape.IBeam,
|
||||
Control.CursorShape.Hand => StandardCursorShape.Hand,
|
||||
Control.CursorShape.Crosshair => StandardCursorShape.Crosshair,
|
||||
Control.CursorShape.VResize => StandardCursorShape.VResize,
|
||||
Control.CursorShape.HResize => StandardCursorShape.HResize,
|
||||
_ => StandardCursorShape.Arrow
|
||||
};
|
||||
|
||||
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
|
||||
}
|
||||
|
||||
public void MouseWheel(MouseWheelEventArgs args)
|
||||
{
|
||||
var control = MouseGetControl(args.Position);
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handle();
|
||||
|
||||
var pos = args.Position.Position;
|
||||
|
||||
var guiArgs = new GUIMouseWheelEventArgs(args.Delta, control,
|
||||
pos / control.UIScale, args.Position,
|
||||
pos / control.UIScale - control.GlobalPosition, pos - control.GlobalPixelPosition);
|
||||
|
||||
_doMouseGuiInput(control, guiArgs, (c, ev) => c.MouseWheel(ev), true);
|
||||
}
|
||||
|
||||
public void TextEntered(TextEventArgs textEvent)
|
||||
{
|
||||
if (KeyboardFocused == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUITextEventArgs(KeyboardFocused, textEvent.CodePoint);
|
||||
KeyboardFocused.TextEntered(guiArgs);
|
||||
}
|
||||
|
||||
public ScreenCoordinates MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition);
|
||||
|
||||
private static void _doMouseGuiInput<T>(Control? control, T guiEvent, Action<Control, T> action,
|
||||
bool ignoreStop = false)
|
||||
where T : GUIMouseEventArgs
|
||||
{
|
||||
while (control != null)
|
||||
{
|
||||
guiEvent.SourceControl = control;
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
|
||||
{
|
||||
action(control, guiEvent);
|
||||
|
||||
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
guiEvent.RelativePosition += control.Position;
|
||||
guiEvent.RelativePixelPosition += control.PixelPosition;
|
||||
control = control.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
private static void _doGuiInput(
|
||||
Control? control,
|
||||
GUIBoundKeyEventArgs guiEvent,
|
||||
Action<Control, GUIBoundKeyEventArgs> action,
|
||||
bool ignoreStop = false)
|
||||
{
|
||||
while (control != null)
|
||||
{
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
|
||||
{
|
||||
action(control, guiEvent);
|
||||
|
||||
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
guiEvent.RelativePosition += control.Position;
|
||||
guiEvent.RelativePixelPosition += control.PixelPosition;
|
||||
control = control.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
private void _clearTooltip()
|
||||
{
|
||||
if (!showingTooltip) return;
|
||||
_tooltip.Visible = false;
|
||||
if (_suppliedTooltip != null)
|
||||
{
|
||||
PopupRoot.RemoveChild(_suppliedTooltip);
|
||||
_suppliedTooltip = null;
|
||||
}
|
||||
|
||||
CurrentlyHovered?.PerformHideTooltip();
|
||||
_resetTooltipTimer();
|
||||
showingTooltip = false;
|
||||
}
|
||||
|
||||
public void CursorChanged(Control control)
|
||||
{
|
||||
if (control == ControlFocused || control == CurrentlyHovered)
|
||||
{
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void HideTooltipFor(Control control)
|
||||
{
|
||||
if (CurrentlyHovered == control)
|
||||
{
|
||||
_clearTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HandleCanFocusDown(
|
||||
ScreenCoordinates pointerPosition,
|
||||
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData)
|
||||
{
|
||||
var hit = MouseGetControlAndRel(pointerPosition);
|
||||
var pos = pointerPosition.Position;
|
||||
|
||||
// If we have a modal open and the mouse down was outside it, close said modal.
|
||||
for (var i = _modalStack.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var top = _modalStack[i];
|
||||
var offset = pos - top.GlobalPixelPosition;
|
||||
if (!top.HasPoint(offset / top.UIScale))
|
||||
{
|
||||
if (top.MouseFilter != Control.MouseFilterMode.Stop)
|
||||
{
|
||||
if (top is not Popup {CloseOnClick: false})
|
||||
{
|
||||
RemoveModal(top);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ControlFocused = top;
|
||||
hitData = null;
|
||||
return false; // prevent anything besides the top modal control from receiving input
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hit == null)
|
||||
{
|
||||
ReleaseKeyboardFocus();
|
||||
hitData = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (control, rel) = hit.Value;
|
||||
|
||||
if (control != KeyboardFocused)
|
||||
{
|
||||
ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
ControlFocused = control;
|
||||
|
||||
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
|
||||
{
|
||||
ControlFocused.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
hitData = (control, (Vector2i) rel);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void HandleCanFocusUp()
|
||||
{
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
public ScreenCoordinates ScreenToUIPosition(ScreenCoordinates coordinates)
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
|
||||
return default;
|
||||
|
||||
return new ScreenCoordinates(coordinates.Position / root.UIScale, coordinates.Window);
|
||||
}
|
||||
|
||||
public ICursor? WorldCursor
|
||||
{
|
||||
get => _worldCursor;
|
||||
set
|
||||
{
|
||||
_worldCursor = value;
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
}
|
||||
|
||||
private (Control control, Vector2 rel)? MouseGetControlAndRel(ScreenCoordinates coordinates)
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
|
||||
return null;
|
||||
|
||||
return MouseFindControlAtPos(root, coordinates.Position);
|
||||
}
|
||||
|
||||
public Control? MouseGetControl(ScreenCoordinates coordinates)
|
||||
{
|
||||
return MouseGetControlAndRel(coordinates)?.control;
|
||||
}
|
||||
|
||||
public Control? GetSuppliedTooltipFor(Control control)
|
||||
{
|
||||
return CurrentlyHovered == control ? _suppliedTooltip : null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Converts
|
||||
/// </summary>
|
||||
/// <param name="args">Event data values for a bound key state change.</param>
|
||||
|
||||
private bool OnUIKeyBindStateChanged(BoundKeyEventArgs args)
|
||||
{
|
||||
if (args.State == BoundKeyState.Down)
|
||||
{
|
||||
KeyBindDown(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindUp(args);
|
||||
}
|
||||
|
||||
// If we are in a focused control or doing a CanFocus, return true
|
||||
// So that InputManager doesn't propagate events to simulation.
|
||||
if (!args.CanFocus && KeyboardFocused != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GrabKeyboardFocus(Control control)
|
||||
{
|
||||
if (control == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(control));
|
||||
}
|
||||
|
||||
if (!control.CanKeyboardFocus)
|
||||
{
|
||||
throw new ArgumentException("Control cannot get keyboard focus.", nameof(control));
|
||||
}
|
||||
|
||||
if (control == KeyboardFocused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseKeyboardFocus();
|
||||
|
||||
KeyboardFocused = control;
|
||||
|
||||
KeyboardFocused.KeyboardFocusEntered();
|
||||
}
|
||||
|
||||
public void ReleaseKeyboardFocus()
|
||||
{
|
||||
var oldFocused = KeyboardFocused;
|
||||
oldFocused?.KeyboardFocusExited();
|
||||
KeyboardFocused = null;
|
||||
}
|
||||
|
||||
public void ReleaseKeyboardFocus(Control ifControl)
|
||||
{
|
||||
if (ifControl == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ifControl));
|
||||
}
|
||||
|
||||
if (ifControl == KeyboardFocused)
|
||||
{
|
||||
ReleaseKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private void _resetTooltipTimer()
|
||||
{
|
||||
_tooltipTimer = 0;
|
||||
}
|
||||
|
||||
private void _showTooltip()
|
||||
{
|
||||
if (showingTooltip) return;
|
||||
showingTooltip = true;
|
||||
var hovered = CurrentlyHovered;
|
||||
if (hovered == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// show supplied tooltip if there is one
|
||||
if (hovered.TooltipSupplier != null)
|
||||
{
|
||||
_suppliedTooltip = hovered.TooltipSupplier.Invoke(hovered);
|
||||
if (_suppliedTooltip != null)
|
||||
{
|
||||
PopupRoot.AddChild(_suppliedTooltip);
|
||||
Tooltips.PositionTooltip(_suppliedTooltip);
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
|
||||
{
|
||||
// show simple tooltip if there is one
|
||||
_tooltip.Visible = true;
|
||||
_tooltip.Text = hovered.ToolTip;
|
||||
Tooltips.PositionTooltip(_tooltip);
|
||||
}
|
||||
|
||||
hovered.PerformShowTooltip();
|
||||
}
|
||||
|
||||
public Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePosScaled)
|
||||
{
|
||||
var (pos, window) = mousePosScaled;
|
||||
var root = control.Root;
|
||||
|
||||
if (root?.Window == null || root.Window.Id != window)
|
||||
return null;
|
||||
|
||||
return pos - control.GlobalPosition;
|
||||
}
|
||||
}
|
||||
199
Robust.Client/UserInterface/UserInterfaceManager.Layout.cs
Normal file
199
Robust.Client/UserInterface/UserInterfaceManager.Layout.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Profiling;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
internal sealed partial class UserInterfaceManager
|
||||
{
|
||||
private readonly List<WindowRoot> _roots = new();
|
||||
private readonly Dictionary<WindowId, WindowRoot> _windowsToRoot = new();
|
||||
public IEnumerable<UIRoot> AllRoots => _roots;
|
||||
|
||||
private readonly List<Control> _modalStack = new();
|
||||
|
||||
private void RunMeasure(Control control)
|
||||
{
|
||||
if (control.IsMeasureValid || !control.IsInsideTree)
|
||||
return;
|
||||
|
||||
if (control.Parent != null)
|
||||
{
|
||||
RunMeasure(control.Parent);
|
||||
}
|
||||
|
||||
if (control is WindowRoot root)
|
||||
{
|
||||
control.Measure(root.Window.RenderTarget.Size / root.UIScale);
|
||||
}
|
||||
else if (control.PreviousMeasure.HasValue)
|
||||
{
|
||||
control.Measure(control.PreviousMeasure.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void RunArrange(Control control)
|
||||
{
|
||||
if (control.IsArrangeValid || !control.IsInsideTree)
|
||||
return;
|
||||
|
||||
if (control.Parent != null)
|
||||
{
|
||||
RunArrange(control.Parent);
|
||||
}
|
||||
|
||||
if (control is WindowRoot root)
|
||||
{
|
||||
control.Arrange(UIBox2.FromDimensions(Vector2.Zero, root.Window.RenderTarget.Size / root.UIScale));
|
||||
}
|
||||
else if (control.PreviousArrange.HasValue)
|
||||
{
|
||||
control.Arrange(control.PreviousArrange.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Popup(string contents, string title = "Alert!")
|
||||
{
|
||||
var popup = new DefaultWindow
|
||||
{
|
||||
Title = title
|
||||
};
|
||||
|
||||
popup.Contents.AddChild(new Label {Text = contents});
|
||||
popup.OpenCentered();
|
||||
}
|
||||
|
||||
public void ControlHidden(Control control)
|
||||
{
|
||||
// Does the same thing but it could later be changed so..
|
||||
ControlRemovedFromTree(control);
|
||||
}
|
||||
|
||||
public void ControlRemovedFromTree(Control control)
|
||||
{
|
||||
ReleaseKeyboardFocus(control);
|
||||
RemoveModal(control);
|
||||
if (control == CurrentlyHovered)
|
||||
{
|
||||
control.MouseExited();
|
||||
CurrentlyHovered = null;
|
||||
_clearTooltip();
|
||||
}
|
||||
|
||||
if (control != ControlFocused) return;
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
public void PushModal(Control modal)
|
||||
{
|
||||
_modalStack.Add(modal);
|
||||
}
|
||||
|
||||
public void RemoveModal(Control modal)
|
||||
{
|
||||
if (_modalStack.Remove(modal))
|
||||
{
|
||||
modal.ModalRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(IRenderHandle renderHandle)
|
||||
{
|
||||
// Render secondary windows LAST.
|
||||
// This makes it so that (hopefully) the GPU will be done rendering secondary windows
|
||||
// by the times we try to blit to them at the end of Clyde's render cycle,
|
||||
// So that the GL driver doesn't have to block on glWaitSync.
|
||||
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
if (root.Window != _clyde.MainWindow)
|
||||
{
|
||||
using var _ = _prof.Group("Window");
|
||||
_prof.WriteValue("ID", ProfData.Int32((int) root.Window.Id));
|
||||
|
||||
renderHandle.RenderInRenderTarget(
|
||||
root.Window.RenderTarget,
|
||||
() => DoRender(root),
|
||||
root.ActualBgColor);
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("Main"))
|
||||
{
|
||||
DoRender(_windowsToRoot[_clyde.MainWindow.Id]);
|
||||
}
|
||||
|
||||
void DoRender(WindowRoot root)
|
||||
{
|
||||
var total = 0;
|
||||
_render(renderHandle, ref total, root, Vector2i.Zero, Color.White, null);
|
||||
var drawingHandle = renderHandle.DrawingHandleScreen;
|
||||
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
|
||||
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
|
||||
|
||||
_prof.WriteValue("Controls rendered", ProfData.Int32(total));
|
||||
}
|
||||
}
|
||||
|
||||
public void QueueStyleUpdate(Control control)
|
||||
{
|
||||
_styleUpdateQueue.Enqueue(control);
|
||||
}
|
||||
|
||||
public void QueueMeasureUpdate(Control control)
|
||||
{
|
||||
_measureUpdateQueue.Enqueue(control);
|
||||
_arrangeUpdateQueue.Enqueue(control);
|
||||
}
|
||||
|
||||
public void QueueArrangeUpdate(Control control)
|
||||
{
|
||||
_arrangeUpdateQueue.Enqueue(control);
|
||||
}
|
||||
|
||||
public WindowRoot CreateWindowRoot(IClydeWindow window)
|
||||
{
|
||||
if (_windowsToRoot.ContainsKey(window.Id))
|
||||
{
|
||||
throw new ArgumentException("Window already has a UI root.");
|
||||
}
|
||||
|
||||
var newRoot = new WindowRoot(window)
|
||||
{
|
||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
||||
HorizontalAlignment = Control.HAlignment.Stretch,
|
||||
VerticalAlignment = Control.VAlignment.Stretch,
|
||||
UIScaleSet = window.ContentScale.X
|
||||
};
|
||||
|
||||
_roots.Add(newRoot);
|
||||
_windowsToRoot.Add(window.Id, newRoot);
|
||||
|
||||
newRoot.StyleSheetUpdate();
|
||||
newRoot.InvalidateMeasure();
|
||||
QueueMeasureUpdate(newRoot);
|
||||
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
public void DestroyWindowRoot(IClydeWindow window)
|
||||
{
|
||||
// Destroy window root if this window had one.
|
||||
if (!_windowsToRoot.TryGetValue(window.Id, out var root))
|
||||
return;
|
||||
|
||||
_windowsToRoot.Remove(window.Id);
|
||||
_roots.Remove(root);
|
||||
|
||||
root.RemoveAllChildren();
|
||||
}
|
||||
|
||||
public WindowRoot? GetWindowRoot(IClydeWindow window)
|
||||
{
|
||||
return !_windowsToRoot.TryGetValue(window.Id, out var root) ? null : root;
|
||||
}
|
||||
}
|
||||
145
Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs
Normal file
145
Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal partial class UserInterfaceManager
|
||||
{
|
||||
[ViewVariables] public float DefaultUIScale => _clyde.DefaultWindowScale.X;
|
||||
[ViewVariables] private Vector2i _resolutionAutoScaleUpper;
|
||||
[ViewVariables] private Vector2i _resolutionAutoScaleLower;
|
||||
[ViewVariables] private bool _autoScaleEnabled;
|
||||
[ViewVariables] private float _resolutionAutoScaleMinValue;
|
||||
|
||||
private void _initScaling()
|
||||
{
|
||||
_clyde.OnWindowResized += WindowSizeChanged;
|
||||
_clyde.OnWindowScaleChanged += WindowContentScaleChanged;
|
||||
RegisterAutoscaleCVarListeners();
|
||||
_uiScaleChanged(_configurationManager.GetCVar(CVars.DisplayUIScale));
|
||||
}
|
||||
|
||||
|
||||
private void _uiScaleChanged(float newValue)
|
||||
{
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowContentScaleChanged(WindowContentScaleEventArgs args)
|
||||
{
|
||||
if (_windowsToRoot.TryGetValue(args.Window.Id, out var root))
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
_fontManager.ClearFontCache();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void RegisterAutoscaleCVarListeners()
|
||||
{
|
||||
_configurationManager.OnValueChanged(CVars.ResAutoScaleEnabled, i =>
|
||||
{
|
||||
_autoScaleEnabled = i;
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
root.UIScaleSet = 1;
|
||||
_propagateUIScaleChanged(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
|
||||
}, true);
|
||||
_configurationManager.OnValueChanged(CVars.ResAutoScaleLowX, i =>
|
||||
{
|
||||
_resolutionAutoScaleLower.X = i;
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}, true);
|
||||
_configurationManager.OnValueChanged(CVars.ResAutoScaleLowY, i =>
|
||||
{
|
||||
_resolutionAutoScaleLower.Y = i;
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}, true);
|
||||
_configurationManager.OnValueChanged(CVars.ResAutoScaleUpperX, i =>
|
||||
{
|
||||
_resolutionAutoScaleUpper.X = i;
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}, true);
|
||||
_configurationManager.OnValueChanged(CVars.ResAutoScaleUpperY, i =>
|
||||
{
|
||||
_resolutionAutoScaleUpper.Y = i;
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}, true);
|
||||
_configurationManager.OnValueChanged(CVars.ResAutoScaleMin, i =>
|
||||
{
|
||||
_resolutionAutoScaleMinValue = i;
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
private float CalculateAutoScale(WindowRoot root)
|
||||
{
|
||||
//Grab the OS UIScale or the value set through CVAR debug
|
||||
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
|
||||
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
|
||||
var windowSize = root.Window.RenderTarget.Size;
|
||||
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
|
||||
if (!_autoScaleEnabled && (windowSize.X <= 0 || windowSize.Y <= 0)) return osScale;
|
||||
var maxScaleRes = _resolutionAutoScaleUpper;
|
||||
var minScaleRes = _resolutionAutoScaleLower;
|
||||
var autoScaleMin = _resolutionAutoScaleMinValue;
|
||||
float scaleRatioX;
|
||||
float scaleRatioY;
|
||||
|
||||
//Calculate the scale ratios and clamp it between the maximums and minimums
|
||||
scaleRatioX = Math.Clamp(((float) windowSize.X - minScaleRes.X) / (maxScaleRes.X - minScaleRes.X) * osScale, autoScaleMin, osScale);
|
||||
scaleRatioY = Math.Clamp(((float) windowSize.Y - minScaleRes.Y) / (maxScaleRes.Y - minScaleRes.Y) * osScale, autoScaleMin, osScale);
|
||||
//Take the smallest UIScale value and use it for UI scaling
|
||||
return Math.Min(scaleRatioX, scaleRatioY);
|
||||
}
|
||||
|
||||
private void UpdateUIScale(WindowRoot root)
|
||||
{
|
||||
root.UIScaleSet = CalculateAutoScale(root);
|
||||
_propagateUIScaleChanged(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void _propagateUIScaleChanged(Control control)
|
||||
{
|
||||
control.UIScaleChanged();
|
||||
|
||||
foreach (var child in control.Children)
|
||||
{
|
||||
_propagateUIScaleChanged(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowSizeChanged(WindowResizedEventArgs windowResizedEventArgs)
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
|
||||
return;
|
||||
UpdateUIScale(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
88
Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
Normal file
88
Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal partial class UserInterfaceManager
|
||||
{
|
||||
private readonly Dictionary<string, UITheme> _themes = new();
|
||||
|
||||
public UITheme CurrentTheme { get; private set; } = default!;
|
||||
|
||||
private bool _defaultOverriden = false;
|
||||
public UITheme DefaultTheme { get; private set; } = default!;
|
||||
|
||||
private void _initThemes()
|
||||
{
|
||||
DefaultTheme = _protoManager.Index<UITheme>(UITheme.DefaultName);
|
||||
CurrentTheme = DefaultTheme;
|
||||
foreach (var proto in _protoManager.EnumeratePrototypes<UITheme>())
|
||||
{
|
||||
_themes.Add(proto.ID, proto);
|
||||
}
|
||||
_configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
|
||||
}
|
||||
|
||||
//Try to set the current theme, if the theme is not found do nothing
|
||||
public void SetActiveTheme(string themeName)
|
||||
{
|
||||
if (!_themes.TryGetValue(themeName, out var theme) || (theme == CurrentTheme)) return;
|
||||
CurrentTheme = theme;
|
||||
}
|
||||
|
||||
public void SetDefaultTheme(string themeId)
|
||||
{
|
||||
if (_defaultOverriden)
|
||||
{
|
||||
//this exists to stop people from misusing default theme
|
||||
Logger.Error("Tried to set default theme twice!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_protoManager.TryIndex(themeId, out UITheme? theme))
|
||||
{
|
||||
Logger.Error("Could not find UI theme prototype for ID:"+ themeId);
|
||||
return;
|
||||
}
|
||||
DefaultTheme = theme;
|
||||
UpdateTheme(theme);
|
||||
_defaultOverriden = true;
|
||||
}
|
||||
|
||||
private void UpdateTheme(UITheme newTheme)
|
||||
{
|
||||
if (newTheme == CurrentTheme) return; //do not update if the theme is unchanged
|
||||
CurrentTheme = newTheme;
|
||||
_userInterfaceManager.RootControl.ThemeUpdateRecursive();
|
||||
}
|
||||
|
||||
//Try to set the current theme, if the theme is not found leave the previous theme
|
||||
public void SetThemeOrPrevious(string name)
|
||||
{
|
||||
UpdateTheme(GetThemeOrCurrent(name));
|
||||
}
|
||||
|
||||
//Try to set the current theme, if the theme is not found set the default theme
|
||||
public void SetThemeOrDefault(string name)
|
||||
{
|
||||
UpdateTheme(GetThemeOrDefault(name));
|
||||
}
|
||||
|
||||
public UITheme GetThemeOrCurrent(string name)
|
||||
{
|
||||
return !_themes.TryGetValue(name, out var theme) ? CurrentTheme : theme;
|
||||
}
|
||||
|
||||
public UITheme GetThemeOrDefault(string name)
|
||||
{
|
||||
return !_themes.TryGetValue(name, out var theme) ? DefaultTheme : theme;
|
||||
}
|
||||
|
||||
public UITheme GetTheme(string name)
|
||||
{
|
||||
return _themes[name];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal partial class UserInterfaceManager
|
||||
{
|
||||
private UIScreen? _activeScreen;
|
||||
|
||||
public UIScreen? ActiveScreen
|
||||
{
|
||||
get => _activeScreen;
|
||||
private set
|
||||
{
|
||||
if (_activeScreen == value) return;
|
||||
_activeScreen?.OnRemoved();
|
||||
_activeScreen = value;
|
||||
_activeScreen?.OnAdded();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public Control ScreenRoot { get; private set; } = default!;
|
||||
|
||||
private readonly Dictionary<Type, UIScreen> _screens = new();
|
||||
|
||||
private void _initializeScreens()
|
||||
{
|
||||
foreach (var screenType in _reflectionManager.GetAllChildren<UIScreen>())
|
||||
{
|
||||
if (screenType.IsAbstract) continue;
|
||||
_screens.Add(screenType, (UIScreen) _typeFactory.CreateInstance(screenType));
|
||||
}
|
||||
|
||||
ScreenRoot = new Control
|
||||
{
|
||||
Name = "ScreenRoot"
|
||||
};
|
||||
RootControl.AddChild(ScreenRoot);
|
||||
//This MUST be drawn before windowroot
|
||||
ScreenRoot.SetPositionInParent(2);
|
||||
}
|
||||
|
||||
public void LoadScreen<T>() where T : UIScreen, new()
|
||||
{
|
||||
((IUserInterfaceManager) this).LoadScreenInternal(typeof(T));
|
||||
}
|
||||
|
||||
public T? GetActiveUIWidgetOrNull<T>() where T : UIWidget, new()
|
||||
{
|
||||
return (T?) _activeScreen?.GetWidget<T>();
|
||||
}
|
||||
|
||||
public T GetActiveUIWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
if (_activeScreen == null) throw new Exception("No screen is currently active");
|
||||
var widget = _activeScreen.GetWidget<T>();
|
||||
if (widget == null) throw new Exception("No widget of type found in active screen");
|
||||
return (T) widget;
|
||||
}
|
||||
|
||||
void IUserInterfaceManager.LoadScreenInternal(Type type)
|
||||
{
|
||||
var screen = _screens[type];
|
||||
ActiveScreen = screen;
|
||||
ScreenRoot.AddChild(screen);
|
||||
screen.HorizontalAlignment = Control.HAlignment.Stretch;
|
||||
screen.VerticalAlignment = Control.VAlignment.Stretch;
|
||||
}
|
||||
|
||||
public void UnloadScreen()
|
||||
{
|
||||
if (_activeScreen == null) return;
|
||||
ScreenRoot.RemoveChild(_activeScreen);
|
||||
_activeScreen = null;
|
||||
}
|
||||
}
|
||||
118
Robust.Client/UserInterface/UserInterfaceManager.Windows.cs
Normal file
118
Robust.Client/UserInterface/UserInterfaceManager.Windows.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal partial class UserInterfaceManager
|
||||
{
|
||||
private readonly Dictionary<Type, Queue<BaseWindow>> _windowsByType = new();
|
||||
private readonly Dictionary<Type, Queue<Popup>> _popupsByType = new();
|
||||
|
||||
public T CreatePopup<T>() where T : Popup, new()
|
||||
{
|
||||
var newPopup = _typeFactory.CreateInstance<T>();
|
||||
_popupsByType.GetOrNew(typeof(T)).Enqueue(newPopup);
|
||||
ModalRoot.AddChild(newPopup);
|
||||
return newPopup;
|
||||
}
|
||||
|
||||
public bool RemoveFirstPopup<T>() where T : Popup, new()
|
||||
{
|
||||
if (!_popupsByType.TryGetValue(typeof(T),out var popupQueue)) return false;
|
||||
var oldPopup = popupQueue.Dequeue();
|
||||
if (popupQueue.Count == 0)
|
||||
{
|
||||
_popupsByType.Remove(typeof(T));
|
||||
}
|
||||
oldPopup.Close();
|
||||
oldPopup.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetFirstPopup<T>(out T? popup) where T : Popup, new()
|
||||
{
|
||||
popup = null;
|
||||
var success = _popupsByType.TryGetValue(typeof(T), out var win);
|
||||
if (win is {Count: > 0})
|
||||
{
|
||||
popup = (T)win.Peek();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public bool TryGetFirstPopup(Type type, out Popup? popup)
|
||||
{
|
||||
popup = null;
|
||||
if (!typeof(Popup).IsAssignableFrom(type)) return false;
|
||||
if (!_popupsByType.TryGetValue(type, out var popupQueue) || popupQueue.Count == 0) return false;
|
||||
popup = popupQueue.Peek();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveFirstWindow<T>() where T : BaseWindow, new()
|
||||
{
|
||||
if (!_windowsByType.TryGetValue(typeof(T),out var windowQueue)) return false;
|
||||
var oldWindow = windowQueue.Dequeue();
|
||||
if (windowQueue.Count == 0)
|
||||
{
|
||||
_windowsByType.Remove(typeof(T));
|
||||
}
|
||||
_uiManager.StateRoot.RemoveChild(oldWindow);
|
||||
oldWindow.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
public T GetFirstWindow<T>() where T : BaseWindow, new()
|
||||
{
|
||||
if (!_windowsByType.TryGetValue(typeof(T), out var windowQueue) || windowQueue.Count == 0)
|
||||
throw new Exception("Window of type" + typeof(T) + " not found!");
|
||||
return (T)windowQueue.Peek();
|
||||
}
|
||||
|
||||
public T CreateWindow<T>() where T : BaseWindow, new()
|
||||
{
|
||||
//If we sandbox this we break creating engine windows. The argument is type bounded anyway so it only accepts
|
||||
//public classes that inherit from BaseWindow.
|
||||
var newWindow = _typeFactory.CreateInstanceUnchecked<T>();
|
||||
_windowsByType.GetOrNew(typeof(T)).Enqueue(newWindow);
|
||||
return newWindow;
|
||||
}
|
||||
|
||||
private void RegisterWindowOfType(BaseWindow window)
|
||||
{
|
||||
if (_windowsByType.ContainsKey(window.GetType())) return;
|
||||
_windowsByType.GetOrNew(window.GetType()).Enqueue(window);
|
||||
}
|
||||
|
||||
public bool TryGetFirstWindow<T>(out T? window) where T : BaseWindow, new()
|
||||
{
|
||||
window = null;
|
||||
var success = _windowsByType.TryGetValue(typeof(T), out var win);
|
||||
if (win is {Count: > 0})
|
||||
{
|
||||
window = (T)win.Peek();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public bool TryGetFirstWindow(Type type, out BaseWindow? window)
|
||||
{
|
||||
window = null;
|
||||
if (!typeof(BaseWindow).IsAssignableFrom(type)) return false;
|
||||
if (!_windowsByType.TryGetValue(type, out var winQueue) || winQueue.Count == 0) return false;
|
||||
window = winQueue.Peek();
|
||||
return true;
|
||||
}
|
||||
public void ClearWindows()
|
||||
{
|
||||
foreach (var data in _windowsByType)
|
||||
{
|
||||
data.Value.Dequeue().Dispose();
|
||||
}
|
||||
_windowsByType.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -16,27 +18,35 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
internal sealed class UserInterfaceManager : IUserInterfaceManagerInternal
|
||||
internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal
|
||||
{
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IFontManager _fontManager = default!;
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly ProfManager _prof = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
|
||||
[ViewVariables] public UITheme ThemeDefaults { get; private set; } = default!;
|
||||
|
||||
[ViewVariables] public InterfaceTheme ThemeDefaults { get; private set; } = default!;
|
||||
[ViewVariables]
|
||||
public Stylesheet? Stylesheet
|
||||
{
|
||||
@@ -55,27 +65,9 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public Control? KeyboardFocused { get; private set; }
|
||||
|
||||
private Control? _controlFocused;
|
||||
[ViewVariables]
|
||||
public Control? ControlFocused
|
||||
{
|
||||
get => _controlFocused;
|
||||
set
|
||||
{
|
||||
if (_controlFocused == value)
|
||||
return;
|
||||
_controlFocused?.ControlFocusExited();
|
||||
_controlFocused = value;
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!;
|
||||
[ViewVariables] public PopupContainer ModalRoot { get; private set; } = default!;
|
||||
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
|
||||
[ViewVariables] public float DefaultUIScale => _clyde.DefaultWindowScale.X;
|
||||
[ViewVariables] public WindowRoot RootControl { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer WindowRoot { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer PopupRoot { get; private set; } = default!;
|
||||
@@ -83,35 +75,19 @@ namespace Robust.Client.UserInterface
|
||||
[ViewVariables] public IDebugMonitors DebugMonitors => _debugMonitors;
|
||||
private DebugMonitors _debugMonitors = default!;
|
||||
|
||||
private readonly List<Control> _modalStack = new();
|
||||
|
||||
private bool _rendering = true;
|
||||
|
||||
private float _tooltipTimer;
|
||||
|
||||
// set to null when not counting down
|
||||
private float? _tooltipDelay;
|
||||
private Tooltip _tooltip = default!;
|
||||
private bool showingTooltip;
|
||||
private Control? _suppliedTooltip;
|
||||
private const float TooltipDelay = 1;
|
||||
|
||||
private readonly Queue<Control> _styleUpdateQueue = new();
|
||||
private readonly Queue<Control> _measureUpdateQueue = new();
|
||||
private readonly Queue<Control> _arrangeUpdateQueue = new();
|
||||
private Stylesheet? _stylesheet;
|
||||
private ICursor? _worldCursor;
|
||||
private bool _needUpdateActiveCursor;
|
||||
|
||||
private readonly List<WindowRoot> _roots = new();
|
||||
private readonly Dictionary<WindowId, WindowRoot> _windowsToRoot = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_configurationManager.OnValueChanged(CVars.DisplayUIScale, _uiScaleChanged, true);
|
||||
|
||||
ThemeDefaults = new UIThemeDummy();
|
||||
|
||||
ThemeDefaults = new InterfaceThemeDummy();
|
||||
_initScaling();
|
||||
_setupControllers();
|
||||
_initializeCommon();
|
||||
|
||||
DebugConsole = new DropDownDebugConsole();
|
||||
@@ -135,17 +111,17 @@ namespace Robust.Client.UserInterface
|
||||
disabled: session => _rendering = true));
|
||||
|
||||
_inputManager.UIKeyBindStateChanged += OnUIKeyBindStateChanged;
|
||||
|
||||
_uiScaleChanged(_configurationManager.GetCVar(CVars.DisplayUIScale));
|
||||
_initThemes();
|
||||
}
|
||||
public void PostInitialize()
|
||||
{
|
||||
_initializeScreens();
|
||||
_initializeControllers();
|
||||
}
|
||||
|
||||
private void _initializeCommon()
|
||||
{
|
||||
RootControl = CreateWindowRoot(_clyde.MainWindow);
|
||||
RootControl.Name = "MainWindowRoot";
|
||||
|
||||
_clyde.OnWindowResized += WindowSizeChanged;
|
||||
_clyde.OnWindowScaleChanged += WindowContentScaleChanged;
|
||||
_clyde.DestroyWindow += WindowDestroyed;
|
||||
|
||||
MainViewport = new MainViewportContainer(_eyeManager)
|
||||
@@ -189,54 +165,11 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public void InitializeTesting()
|
||||
{
|
||||
ThemeDefaults = new UIThemeDummy();
|
||||
ThemeDefaults = new InterfaceThemeDummy();
|
||||
|
||||
_initializeCommon();
|
||||
}
|
||||
|
||||
public WindowRoot CreateWindowRoot(IClydeWindow window)
|
||||
{
|
||||
if (_windowsToRoot.ContainsKey(window.Id))
|
||||
{
|
||||
throw new ArgumentException("Window already has a UI root.");
|
||||
}
|
||||
|
||||
var newRoot = new WindowRoot(window)
|
||||
{
|
||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
||||
HorizontalAlignment = Control.HAlignment.Stretch,
|
||||
VerticalAlignment = Control.VAlignment.Stretch,
|
||||
UIScaleSet = window.ContentScale.X
|
||||
};
|
||||
|
||||
_roots.Add(newRoot);
|
||||
_windowsToRoot.Add(window.Id, newRoot);
|
||||
|
||||
newRoot.StyleSheetUpdate();
|
||||
newRoot.InvalidateMeasure();
|
||||
QueueMeasureUpdate(newRoot);
|
||||
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
public void DestroyWindowRoot(IClydeWindow window)
|
||||
{
|
||||
// Destroy window root if this window had one.
|
||||
if (!_windowsToRoot.TryGetValue(window.Id, out var root))
|
||||
return;
|
||||
|
||||
_windowsToRoot.Remove(window.Id);
|
||||
_roots.Remove(root);
|
||||
|
||||
root.RemoveAllChildren();
|
||||
}
|
||||
|
||||
public WindowRoot? GetWindowRoot(IClydeWindow window)
|
||||
{
|
||||
return !_windowsToRoot.TryGetValue(window.Id, out var root) ? null : root;
|
||||
}
|
||||
|
||||
public IEnumerable<UIRoot> AllRoots => _roots;
|
||||
public event Action<PostDrawUIRootEventArgs>? OnPostDrawUIRoot;
|
||||
|
||||
private void WindowDestroyed(WindowDestroyedEventArgs args)
|
||||
@@ -311,6 +244,8 @@ namespace Robust.Client.UserInterface
|
||||
_prof.WriteValue("Total", ProfData.Int32(total));
|
||||
}
|
||||
|
||||
UpdateControllers(args);
|
||||
|
||||
// count down tooltip delay if we're not showing one yet and
|
||||
// are hovering the mouse over a control without moving it
|
||||
if (_tooltipDelay != null && !showingTooltip)
|
||||
@@ -329,442 +264,6 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private void RunMeasure(Control control)
|
||||
{
|
||||
if (control.IsMeasureValid || !control.IsInsideTree)
|
||||
return;
|
||||
|
||||
if (control.Parent != null)
|
||||
{
|
||||
RunMeasure(control.Parent);
|
||||
}
|
||||
|
||||
if (control is WindowRoot root)
|
||||
{
|
||||
control.Measure(root.Window.RenderTarget.Size / root.UIScale);
|
||||
}
|
||||
else if (control.PreviousMeasure.HasValue)
|
||||
{
|
||||
control.Measure(control.PreviousMeasure.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void RunArrange(Control control)
|
||||
{
|
||||
if (control.IsArrangeValid || !control.IsInsideTree)
|
||||
return;
|
||||
|
||||
if (control.Parent != null)
|
||||
{
|
||||
RunArrange(control.Parent);
|
||||
}
|
||||
|
||||
if (control is WindowRoot root)
|
||||
{
|
||||
control.Arrange(UIBox2.FromDimensions(Vector2.Zero, root.Window.RenderTarget.Size / root.UIScale));
|
||||
}
|
||||
else if (control.PreviousArrange.HasValue)
|
||||
{
|
||||
control.Arrange(control.PreviousArrange.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool HandleCanFocusDown(
|
||||
ScreenCoordinates pointerPosition,
|
||||
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData)
|
||||
{
|
||||
var hit = MouseGetControlAndRel(pointerPosition);
|
||||
var pos = pointerPosition.Position;
|
||||
|
||||
// If we have a modal open and the mouse down was outside it, close said modal.
|
||||
while (_modalStack.Count != 0)
|
||||
{
|
||||
var top = _modalStack[^1];
|
||||
var offset = pos - top.GlobalPixelPosition;
|
||||
if (!top.HasPoint(offset / top.UIScale))
|
||||
{
|
||||
if (top.MouseFilter != Control.MouseFilterMode.Stop)
|
||||
RemoveModal(top);
|
||||
else
|
||||
{
|
||||
ControlFocused = top;
|
||||
hitData = null;
|
||||
return false; // prevent anything besides the top modal control from receiving input
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hit == null)
|
||||
{
|
||||
ReleaseKeyboardFocus();
|
||||
hitData = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (control, rel) = hit.Value;
|
||||
|
||||
if (control != KeyboardFocused)
|
||||
{
|
||||
ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
ControlFocused = control;
|
||||
|
||||
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
|
||||
{
|
||||
ControlFocused.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
hitData = (control, (Vector2i) rel);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void HandleCanFocusUp()
|
||||
{
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
public void KeyBindDown(BoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.CloseModals && _modalStack.Count != 0)
|
||||
{
|
||||
while (_modalStack.Count > 0)
|
||||
{
|
||||
var top = _modalStack[^1];
|
||||
RemoveModal(top);
|
||||
}
|
||||
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
|
||||
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
|
||||
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindDown(ev));
|
||||
|
||||
if (guiArgs.Handled)
|
||||
{
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
public void KeyBindUp(BoundKeyEventArgs args)
|
||||
{
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
|
||||
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindUp(ev));
|
||||
|
||||
// Always mark this as handled.
|
||||
// The only case it should not be is if we do not have a control to click on,
|
||||
// in which case we never reach this.
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
public void MouseMove(MouseMoveEventArgs mouseMoveEventArgs)
|
||||
{
|
||||
_resetTooltipTimer();
|
||||
// Update which control is considered hovered.
|
||||
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
|
||||
if (newHovered != CurrentlyHovered)
|
||||
{
|
||||
_clearTooltip();
|
||||
CurrentlyHovered?.MouseExited();
|
||||
CurrentlyHovered = newHovered;
|
||||
CurrentlyHovered?.MouseEntered();
|
||||
if (CurrentlyHovered != null)
|
||||
{
|
||||
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltipDelay = null;
|
||||
}
|
||||
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
|
||||
var target = ControlFocused ?? newHovered;
|
||||
if (target != null)
|
||||
{
|
||||
var pos = mouseMoveEventArgs.Position.Position;
|
||||
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / target.UIScale,
|
||||
target,
|
||||
pos / target.UIScale, mouseMoveEventArgs.Position,
|
||||
pos / target.UIScale - target.GlobalPosition,
|
||||
pos - target.GlobalPixelPosition);
|
||||
|
||||
_doMouseGuiInput(target, guiArgs, (c, ev) => c.MouseMove(ev));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActiveCursor()
|
||||
{
|
||||
// Consider mouse input focus first so that dragging windows don't act up etc.
|
||||
var cursorTarget = ControlFocused ?? CurrentlyHovered;
|
||||
|
||||
if (cursorTarget == null)
|
||||
{
|
||||
_clyde.SetCursor(_worldCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursorTarget.CustomCursorShape != null)
|
||||
{
|
||||
_clyde.SetCursor(cursorTarget.CustomCursorShape);
|
||||
return;
|
||||
}
|
||||
|
||||
var shape = cursorTarget.DefaultCursorShape switch
|
||||
{
|
||||
Control.CursorShape.Arrow => StandardCursorShape.Arrow,
|
||||
Control.CursorShape.IBeam => StandardCursorShape.IBeam,
|
||||
Control.CursorShape.Hand => StandardCursorShape.Hand,
|
||||
Control.CursorShape.Crosshair => StandardCursorShape.Crosshair,
|
||||
Control.CursorShape.VResize => StandardCursorShape.VResize,
|
||||
Control.CursorShape.HResize => StandardCursorShape.HResize,
|
||||
_ => StandardCursorShape.Arrow
|
||||
};
|
||||
|
||||
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
|
||||
}
|
||||
|
||||
public void MouseWheel(MouseWheelEventArgs args)
|
||||
{
|
||||
var control = MouseGetControl(args.Position);
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handle();
|
||||
|
||||
var pos = args.Position.Position;
|
||||
|
||||
var guiArgs = new GUIMouseWheelEventArgs(args.Delta, control,
|
||||
pos / control.UIScale, args.Position,
|
||||
pos / control.UIScale - control.GlobalPosition, pos - control.GlobalPixelPosition);
|
||||
|
||||
_doMouseGuiInput(control, guiArgs, (c, ev) => c.MouseWheel(ev), true);
|
||||
}
|
||||
|
||||
public void TextEntered(TextEventArgs textEvent)
|
||||
{
|
||||
if (KeyboardFocused == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUITextEventArgs(KeyboardFocused, textEvent.CodePoint);
|
||||
KeyboardFocused.TextEntered(guiArgs);
|
||||
}
|
||||
|
||||
public void Popup(string contents, string title = "Alert!")
|
||||
{
|
||||
var popup = new DefaultWindow
|
||||
{
|
||||
Title = title
|
||||
};
|
||||
|
||||
popup.Contents.AddChild(new Label {Text = contents});
|
||||
popup.OpenCentered();
|
||||
}
|
||||
|
||||
public Control? MouseGetControl(ScreenCoordinates coordinates)
|
||||
{
|
||||
return MouseGetControlAndRel(coordinates)?.control;
|
||||
}
|
||||
|
||||
private (Control control, Vector2 rel)? MouseGetControlAndRel(ScreenCoordinates coordinates)
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
|
||||
return null;
|
||||
|
||||
return MouseFindControlAtPos(root, coordinates.Position);
|
||||
}
|
||||
|
||||
public ScreenCoordinates MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition);
|
||||
|
||||
public ScreenCoordinates ScreenToUIPosition(ScreenCoordinates coordinates)
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
|
||||
return default;
|
||||
|
||||
return new ScreenCoordinates(coordinates.Position / root.UIScale, coordinates.Window);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GrabKeyboardFocus(Control control)
|
||||
{
|
||||
if (control == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(control));
|
||||
}
|
||||
|
||||
if (!control.CanKeyboardFocus)
|
||||
{
|
||||
throw new ArgumentException("Control cannot get keyboard focus.", nameof(control));
|
||||
}
|
||||
|
||||
if (control == KeyboardFocused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseKeyboardFocus();
|
||||
|
||||
KeyboardFocused = control;
|
||||
|
||||
KeyboardFocused.KeyboardFocusEntered();
|
||||
}
|
||||
|
||||
public void ReleaseKeyboardFocus()
|
||||
{
|
||||
var oldFocused = KeyboardFocused;
|
||||
oldFocused?.KeyboardFocusExited();
|
||||
KeyboardFocused = null;
|
||||
}
|
||||
|
||||
public void ReleaseKeyboardFocus(Control ifControl)
|
||||
{
|
||||
if (ifControl == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ifControl));
|
||||
}
|
||||
|
||||
if (ifControl == KeyboardFocused)
|
||||
{
|
||||
ReleaseKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
public ICursor? WorldCursor
|
||||
{
|
||||
get => _worldCursor;
|
||||
set
|
||||
{
|
||||
_worldCursor = value;
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void ControlHidden(Control control)
|
||||
{
|
||||
// Does the same thing but it could later be changed so..
|
||||
ControlRemovedFromTree(control);
|
||||
}
|
||||
|
||||
public void ControlRemovedFromTree(Control control)
|
||||
{
|
||||
ReleaseKeyboardFocus(control);
|
||||
RemoveModal(control);
|
||||
if (control == CurrentlyHovered)
|
||||
{
|
||||
control.MouseExited();
|
||||
CurrentlyHovered = null;
|
||||
_clearTooltip();
|
||||
}
|
||||
|
||||
if (control != ControlFocused) return;
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
public void PushModal(Control modal)
|
||||
{
|
||||
_modalStack.Add(modal);
|
||||
}
|
||||
|
||||
public void RemoveModal(Control modal)
|
||||
{
|
||||
if (_modalStack.Remove(modal))
|
||||
{
|
||||
modal.ModalRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(IRenderHandle renderHandle)
|
||||
{
|
||||
// Render secondary windows LAST.
|
||||
// This makes it so that (hopefully) the GPU will be done rendering secondary windows
|
||||
// by the times we try to blit to them at the end of Clyde's render cycle,
|
||||
// So that the GL driver doesn't have to block on glWaitSync.
|
||||
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
if (root.Window != _clyde.MainWindow)
|
||||
{
|
||||
using var _ = _prof.Group("Window");
|
||||
_prof.WriteValue("ID", ProfData.Int32((int) root.Window.Id));
|
||||
|
||||
renderHandle.RenderInRenderTarget(
|
||||
root.Window.RenderTarget,
|
||||
() => DoRender(root),
|
||||
root.ActualBgColor);
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("Main"))
|
||||
{
|
||||
DoRender(_windowsToRoot[_clyde.MainWindow.Id]);
|
||||
}
|
||||
|
||||
void DoRender(WindowRoot root)
|
||||
{
|
||||
var total = 0;
|
||||
_render(renderHandle, ref total, root, Vector2i.Zero, Color.White, null);
|
||||
var drawingHandle = renderHandle.DrawingHandleScreen;
|
||||
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
|
||||
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
|
||||
|
||||
_prof.WriteValue("Controls rendered", ProfData.Int32(total));
|
||||
}
|
||||
}
|
||||
|
||||
public void QueueStyleUpdate(Control control)
|
||||
{
|
||||
_styleUpdateQueue.Enqueue(control);
|
||||
}
|
||||
|
||||
public void QueueMeasureUpdate(Control control)
|
||||
{
|
||||
_measureUpdateQueue.Enqueue(control);
|
||||
_arrangeUpdateQueue.Enqueue(control);
|
||||
}
|
||||
|
||||
public void QueueArrangeUpdate(Control control)
|
||||
{
|
||||
_arrangeUpdateQueue.Enqueue(control);
|
||||
}
|
||||
|
||||
public void CursorChanged(Control control)
|
||||
{
|
||||
if (control == ControlFocused || control == CurrentlyHovered)
|
||||
{
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void _render(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
|
||||
UIBox2i? scissorBox)
|
||||
{
|
||||
@@ -837,243 +336,11 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private static (Control control, Vector2 rel)? MouseFindControlAtPos(Control control, Vector2 position)
|
||||
{
|
||||
for (var i = control.ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = control.GetChild(i);
|
||||
if (!child.Visible || child.RectClipContent && !child.PixelRect.Contains((Vector2i) position))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var maybeFoundOnChild = MouseFindControlAtPos(child, position - child.PixelPosition);
|
||||
if (maybeFoundOnChild != null)
|
||||
{
|
||||
return maybeFoundOnChild;
|
||||
}
|
||||
}
|
||||
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore && control.HasPoint(position / control.UIScale))
|
||||
{
|
||||
return (control, position);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void _doMouseGuiInput<T>(Control? control, T guiEvent, Action<Control, T> action,
|
||||
bool ignoreStop = false)
|
||||
where T : GUIMouseEventArgs
|
||||
{
|
||||
while (control != null)
|
||||
{
|
||||
guiEvent.SourceControl = control;
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
|
||||
{
|
||||
action(control, guiEvent);
|
||||
|
||||
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
guiEvent.RelativePosition += control.Position;
|
||||
guiEvent.RelativePixelPosition += control.PixelPosition;
|
||||
control = control.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
private static void _doGuiInput(
|
||||
Control? control,
|
||||
GUIBoundKeyEventArgs guiEvent,
|
||||
Action<Control, GUIBoundKeyEventArgs> action,
|
||||
bool ignoreStop = false)
|
||||
{
|
||||
while (control != null)
|
||||
{
|
||||
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
|
||||
{
|
||||
action(control, guiEvent);
|
||||
|
||||
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
guiEvent.RelativePosition += control.Position;
|
||||
guiEvent.RelativePixelPosition += control.PixelPosition;
|
||||
control = control.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
private void _clearTooltip()
|
||||
{
|
||||
if (!showingTooltip) return;
|
||||
_tooltip.Visible = false;
|
||||
if (_suppliedTooltip != null)
|
||||
{
|
||||
PopupRoot.RemoveChild(_suppliedTooltip);
|
||||
_suppliedTooltip = null;
|
||||
}
|
||||
|
||||
CurrentlyHovered?.PerformHideTooltip();
|
||||
_resetTooltipTimer();
|
||||
showingTooltip = false;
|
||||
}
|
||||
|
||||
|
||||
public void HideTooltipFor(Control control)
|
||||
{
|
||||
if (CurrentlyHovered == control)
|
||||
{
|
||||
_clearTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
public Control? GetSuppliedTooltipFor(Control control)
|
||||
{
|
||||
return CurrentlyHovered == control ? _suppliedTooltip : null;
|
||||
}
|
||||
|
||||
public Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePosScaled)
|
||||
{
|
||||
var (pos, window) = mousePosScaled;
|
||||
var root = control.Root;
|
||||
|
||||
if (root?.Window == null || root.Window.Id != window)
|
||||
return null;
|
||||
|
||||
return pos - control.GlobalPosition;
|
||||
}
|
||||
|
||||
public Color GetMainClearColor() => RootControl.ActualBgColor;
|
||||
|
||||
private void _resetTooltipTimer()
|
||||
~UserInterfaceManager()
|
||||
{
|
||||
_tooltipTimer = 0;
|
||||
}
|
||||
|
||||
private void _showTooltip()
|
||||
{
|
||||
if (showingTooltip) return;
|
||||
showingTooltip = true;
|
||||
var hovered = CurrentlyHovered;
|
||||
if (hovered == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// show supplied tooltip if there is one
|
||||
if (hovered.TooltipSupplier != null)
|
||||
{
|
||||
_suppliedTooltip = hovered.TooltipSupplier.Invoke(hovered);
|
||||
if (_suppliedTooltip != null)
|
||||
{
|
||||
PopupRoot.AddChild(_suppliedTooltip);
|
||||
Tooltips.PositionTooltip(_suppliedTooltip);
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
|
||||
{
|
||||
// show simple tooltip if there is one
|
||||
_tooltip.Visible = true;
|
||||
_tooltip.Text = hovered.ToolTip;
|
||||
Tooltips.PositionTooltip(_tooltip);
|
||||
}
|
||||
|
||||
hovered.PerformShowTooltip();
|
||||
}
|
||||
|
||||
private void _uiScaleChanged(float newValue)
|
||||
{
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowContentScaleChanged(WindowContentScaleEventArgs args)
|
||||
{
|
||||
if (_windowsToRoot.TryGetValue(args.Window.Id, out var root))
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
_fontManager.ClearFontCache();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private float CalculateAutoScale(WindowRoot root)
|
||||
{
|
||||
//Grab the OS UIScale or the value set through CVAR debug
|
||||
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
|
||||
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
|
||||
var windowSize = root.Window.RenderTarget.Size;
|
||||
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
|
||||
if (!root.AutoScale && (windowSize.X <= 0 || windowSize.Y <= 0)) return osScale;
|
||||
var maxScaleRes = root.AutoScaleUpperCutoff;
|
||||
var minScaleRes = root.AutoScaleLowerCutoff;
|
||||
var autoScaleMin = root.AutoScaleMinimum;
|
||||
float scaleRatioX;
|
||||
float scaleRatioY;
|
||||
|
||||
//Calculate the scale ratios and clamp it between the maximums and minimums
|
||||
scaleRatioX = Math.Clamp(((float) windowSize.X - minScaleRes.X) / (maxScaleRes.X - minScaleRes.X) * osScale, autoScaleMin, osScale);
|
||||
scaleRatioY = Math.Clamp(((float) windowSize.Y - minScaleRes.Y) / (maxScaleRes.Y - minScaleRes.Y) * osScale, autoScaleMin, osScale);
|
||||
//Take the smallest UIScale value and use it for UI scaling
|
||||
return Math.Min(scaleRatioX, scaleRatioY);
|
||||
}
|
||||
|
||||
private void UpdateUIScale(WindowRoot root)
|
||||
{
|
||||
root.UIScaleSet = CalculateAutoScale(root);
|
||||
_propagateUIScaleChanged(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void _propagateUIScaleChanged(Control control)
|
||||
{
|
||||
control.UIScaleChanged();
|
||||
|
||||
foreach (var child in control.Children)
|
||||
{
|
||||
_propagateUIScaleChanged(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowSizeChanged(WindowResizedEventArgs windowResizedEventArgs)
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
|
||||
return;
|
||||
UpdateUIScale(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts
|
||||
/// </summary>
|
||||
/// <param name="args">Event data values for a bound key state change.</param>
|
||||
private bool OnUIKeyBindStateChanged(BoundKeyEventArgs args)
|
||||
{
|
||||
if (args.State == BoundKeyState.Down)
|
||||
{
|
||||
KeyBindDown(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindUp(args);
|
||||
}
|
||||
|
||||
// If we are in a focused control or doing a CanFocus, return true
|
||||
// So that InputManager doesn't propagate events to simulation.
|
||||
if (!args.CanFocus && KeyboardFocused != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
ClearWindows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
Robust.Client/UserInterface/XAML/UiTextureExtension.cs
Normal file
28
Robust.Client/UserInterface/XAML/UiTextureExtension.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML;
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class UiTexExtension
|
||||
{
|
||||
public string Path { get; }
|
||||
public UITheme Theme { get; }
|
||||
public UiTexExtension(string path)
|
||||
{
|
||||
Path = path;
|
||||
Theme = IoCManager.Resolve<IUserInterfaceManager>().CurrentTheme;
|
||||
}
|
||||
//Support for forcing a theme
|
||||
public UiTexExtension(UITheme theme, string path)
|
||||
{
|
||||
Path = path;
|
||||
Theme = theme;
|
||||
}
|
||||
|
||||
public object ProvideValue()
|
||||
{
|
||||
return Theme.ResolveTexture(Path);
|
||||
}
|
||||
}
|
||||
@@ -646,9 +646,6 @@ namespace Robust.Server
|
||||
ServerCurTick.Set(_time.CurTick.Value);
|
||||
ServerCurTime.Set(_time.CurTime.TotalSeconds);
|
||||
|
||||
// These are always the same on the server, there is no prediction.
|
||||
_time.LastRealTick = _time.CurTick;
|
||||
|
||||
_systemConsole.UpdateTick();
|
||||
|
||||
using (TickUsage.WithLabels("PreEngine").NewTimer())
|
||||
|
||||
@@ -75,11 +75,11 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SaveBp : IConsoleCommand
|
||||
public sealed class SaveGridCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "savebp";
|
||||
public string Command => "savegrid";
|
||||
public string Description => "Serializes a grid to disk.";
|
||||
public string Help => "savebp <gridID> <Path>";
|
||||
public string Help => "savegrid <gridID> <Path>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
@@ -104,16 +104,30 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
IoCManager.Resolve<IMapLoader>().SaveBlueprint(gridId, args[1]);
|
||||
IoCManager.Resolve<IMapLoader>().SaveGrid(gridId, args[1]);
|
||||
shell.WriteLine("Save successful. Look in the user data directory.");
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savebp-id"));
|
||||
case 2:
|
||||
var res = IoCManager.Resolve<IResourceManager>();
|
||||
var opts = CompletionHelper.UserFilePath(args[1], res.UserData);
|
||||
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
|
||||
}
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LoadBp : IConsoleCommand
|
||||
public sealed class LoadGridCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "loadbp";
|
||||
public string Description => "Loads a blueprint from disk into the game.";
|
||||
public string Help => "loadbp <MapID> <Path> [x y] [rotation] [storeUids]";
|
||||
public string Command => "loadgrid";
|
||||
public string Description => "Loads a grid from a file into an existing map.";
|
||||
public string Help => "loadgrid <MapID> <Path> [x y] [rotation] [storeUids]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
@@ -148,15 +162,15 @@ namespace Robust.Server.Console.Commands
|
||||
var loadOptions = new MapLoadOptions();
|
||||
if (args.Length >= 4)
|
||||
{
|
||||
if (!int.TryParse(args[2], out var x))
|
||||
if (!float.TryParse(args[2], out var x))
|
||||
{
|
||||
shell.WriteError($"{args[2]} is not a valid integer.");
|
||||
shell.WriteError($"{args[2]} is not a valid float.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[3], out var y))
|
||||
if (!float.TryParse(args[3], out var y))
|
||||
{
|
||||
shell.WriteError($"{args[3]} is not a valid integer.");
|
||||
shell.WriteError($"{args[3]} is not a valid float.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,7 +181,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
if (!float.TryParse(args[4], out var rotation))
|
||||
{
|
||||
shell.WriteError($"{args[4]} is not a valid integer.");
|
||||
shell.WriteError($"{args[4]} is not a valid float.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +192,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
if (!bool.TryParse(args[5], out var storeUids))
|
||||
{
|
||||
shell.WriteError($"{args[5]} is not a valid boolean..");
|
||||
shell.WriteError($"{args[5]} is not a valid boolean.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,7 +200,12 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
|
||||
var mapLoader = IoCManager.Resolve<IMapLoader>();
|
||||
mapLoader.LoadBlueprint(mapId, args[1], loadOptions);
|
||||
mapLoader.LoadGrid(mapId, args[1], loadOptions);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
return LoadMap.GetCompletionResult(shell, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +277,7 @@ namespace Robust.Server.Console.Commands
|
||||
public string Description => Loc.GetString("cmd-loadmap-desc");
|
||||
public string Help => Loc.GetString("cmd-loadmap-help");
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
public static CompletionResult GetCompletionResult(IConsoleShell shell, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
@@ -282,6 +301,11 @@ namespace Robust.Server.Console.Commands
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
return GetCompletionResult(shell, args);
|
||||
}
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 2 || args.Length > 6)
|
||||
@@ -361,7 +385,7 @@ namespace Robust.Server.Console.Commands
|
||||
IoCManager.Resolve<IMapLoader>().LoadMap(mapId, args[1], loadOptions);
|
||||
|
||||
if (mapManager.MapExists(mapId))
|
||||
shell.WriteLine(Loc.GetString("cmd-loadmap-successt", ("mapId", mapId), ("path", args[1])));
|
||||
shell.WriteLine(Loc.GetString("cmd-loadmap-success", ("mapId", mapId), ("path", args[1])));
|
||||
else
|
||||
shell.WriteLine(Loc.GetString("cmd-loadmap-error", ("path", args[1])));
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
|
||||
playerTransform.Coordinates = targetCoords;
|
||||
playerTransform.AttachToGridOrMap();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -119,6 +120,7 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
|
||||
victimTransform.Coordinates = targetCoords;
|
||||
victimTransform.AttachToGridOrMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class SpinCommand : IConsoleCommand
|
||||
EntityUid target;
|
||||
if (args.Length == 3)
|
||||
{
|
||||
if (!EntityUid.TryParse(args[1], out target))
|
||||
if (!EntityUid.TryParse(args[2], out target))
|
||||
{
|
||||
shell.WriteError($"Unable to find entity {args[1]}");
|
||||
return;
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation, VisibilityMask);
|
||||
return new EyeComponentState(DrawFov, Zoom, Offset, VisibilityMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user