Compare commits

...

87 Commits

Author SHA1 Message Date
wrexbe
85c8dc05fe Version: 0.45.3.0 2022-09-04 16:13:16 -07:00
Jezithyr
710371d7d1 UI refactor and UITheme implementations (#2712)
* UIControllerManager


Implemented UI Controller Manager

* added fetch function

* added note

* Hiding some internal stuff

* Implemented event on gamestate switch for ui

* Fix serialization field assigner emit

* fixing issues with ILEmitter stuff

* AHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH

Blame Smug

* fixing nullref

* Add checking for no backing field / property for ui system dependencies

* fixes Gamestate detection

* Implemented event on UIControllers on system load

* Updated systemload/unload listeners

* Had this backwards lol

* Fix nulling systems before calling OnSystemUnloaded, broke InventoryUIController.Hands.cs

* Created UI Window management system

- A manager that allows for easy creation and access of popup or gamestate windows

* Changing to use basewindow instead of default window

* Implemented UI Theming that isn't ass

* Updated default theme loading and validation

* Added path validation for themes

* Implemented UI Themes

* Implemented UI Theme prototypes

* Implementing theming for texture buttons and Texturerects

* fixing server error

* Remove IUILink

* Implemented default theme overriding and theme colors

* Fixing sandbox lul

* Added error for not finding UITheme

* fixing setting default theme in content

* Move entity and tile spawn window logic to controllers

* Add 2 TODOs

* Merge fixes

* Add IOnStateChanged for UI controllers

* Fix inventory window being slow to open
Caches resources when the UI theme is changed

* Remove caching on theme change
The real fix was fixing the path for resources

* Remove test method

* Fix crash when controllers implement non generic interfaces

* Add controllers frame update

* Split UserInterfaceManager into partials

- Created UI screen

* Converted more UI managers into partials

* Setup UIScreen manager system

* Added some widget utility funcs


updated adding widgets

* Started removing HUDManager

* Moved UiController Manager to Partials


Finished moving all UIController code to UIManager

* Fixed screen loading

* Fixed Screen scaling

* Fixed Screen scaling


cleanup

* wat

* IwantToDie

* Fixed resolving ResourceCache instead of IResourceCache

* Split IOnStateChanged into IOnStateEntered and IOnStateExited

* Implemented helpers for adjusting UIAutoscale for screens

* Fixed autoscale, removed archiving from autoscale

* Implemented popups and adjusted some stuff

* Fixing some popup related shinanegans

* Fixing some draw order issues

* fixing dumb shit

* Fix indentation in UserInterfaceManager.Input.cs

* Moved screen setup to post init (run after content)

* Fix updating theme

* Merge fixes

* Fix resolving sprite system on control creation

* Fix min size of tile spawn window

* Add UIController.Initialize method

* https://tenor.com/view/minor-spelling-mistake-gif-21179057

* Add doc comment to UIController

* Split UIController.cs and UISystemDependency.cs into their own files

* Add more documentation to ui controllers

* Add AttributeUsage to UISystemDependencyAttribute

* Fix method naming

* Add documentation for assigners

* Return casted widgets where relevant

* Fix entity spawner scroll (#1)

* Add CloseOnClick and CloseOnEscape for popups

* Remove named windows and popups

* Cleanup controller code

* Add IOnStateChanged, IOnSystemChanged, IOnSystemLoaded, IOnSystemUnloaded

* Add more docs to state and system change interfaces

* Fixing Window issues

* Fixing some window fuckery

* Added OnOpen event to windows, updated sandbox window

Sandbox windows now persist values and positions

* Recurse through controls to register widgets (#2)

* Allow path to be folder

* Fix local player shutdown

* Fixing escape menu

* Fix backing field in DataDefinition.Emitters

* Ent+Tile spawn no crash

* Skip no-spawn in entity spawn menu

Co-authored-by: Jezithyr <jmaster9999@gmail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Jezithyr <Jezithyr@gmail.com>
Co-authored-by: wrexbe <81056464+wrexbe@users.noreply.github.com>
Co-authored-by: Flipp Syder <76629141+vulppine@users.noreply.github.com>
Co-authored-by: wrexbe <wrexbe@protonmail.com>
2022-09-04 16:10:54 -07:00
ElectroJr
9f651646d7 Version: 0.45.2.0 2022-09-04 13:13:08 -04:00
Leon Friedrich
d2860c80a9 Predict component adding and removal (#2876) 2022-09-05 03:09:42 +10:00
Alex Evgrashin
e1b9ae22b6 Box2 from two points (#3211) 2022-09-05 02:49:16 +10:00
Leon Friedrich
5d64f35c96 Sprite BB fixes (#3217) 2022-09-04 16:20:15 +10:00
Flipp Syder
5cfea0cd97 Includes paused for user interface updates (#3215) 2022-09-03 17:20:20 -07:00
Jacob Tong
a165556bf5 Make AudioSystem.PlayingStream public (#3208) 2022-09-03 09:32:23 +10:00
Kara
dee2881203 Fix loadmap command typo (#3209) 2022-09-01 06:27:54 -07:00
DrSmugleaf
da0891a5f4 Add element picker to dev window (#3180) 2022-09-01 06:18:30 +02:00
DrSmugleaf
1ed9796700 Add DataRecordAttribute to turn a whole record into a data definition (#3207)
Co-authored-by: Paul <ritter.paul1@googlemail.com>
2022-09-01 01:13:43 +02:00
Leon Friedrich
578a967a31 Misc PVS error logging / debugging changes (#3197) 2022-08-30 10:17:11 +10:00
Paul Ritter
14c0c58e87 adds includedatafields to serv3 (#3101)
* adds includedatafields to serv3

* fixes some stuff and adds tests

* checks for circular include

* fix writing

* this one should fix it frfrf

* ok now this one is gonna be it i swear

* we can save a few operations here

* goofy string

Co-authored-by: Paul <ritter.paul1@gmail.com>
2022-08-29 17:16:04 +02:00
Vera Aguilera Puerto
766d909a08 Fixes MIDI bank selection not working. (#3182) 2022-08-29 11:52:29 +02:00
metalgearsloth
9c50a217da Version: 0.45.1.0 2022-08-29 15:13:31 +10:00
Leon Friedrich
4f9db0cdb7 Fix bug in spin command (#3198) 2022-08-29 14:37:59 +10:00
Leon Friedrich
3c19937750 Update sprite bounds for SnapCardinals (#3195) 2022-08-29 13:52:23 +10:00
metalgearsloth
c509764014 Add camera reset rotation key (#3185) 2022-08-29 11:09:32 +10:00
wrexbe
75e06e4060 Version: 0.45.0.3 2022-08-28 15:34:15 -07:00
wrexbe
1b909f71a1 Fix some test issues (#3196) 2022-08-28 14:04:14 -07:00
metalgearsloth
1a8764f54b Add support for snapping sprites to south (#3186) 2022-08-29 05:02:50 +10:00
metalgearsloth
9152c97de7 Make mapload errors more obvious (#3193) 2022-08-29 04:08:14 +10:00
Paul Ritter
45cb04f928 makes all serv3 null checks use the proper logic (#3128) 2022-08-27 21:37:38 +02:00
Pieter-Jan Briers
a700750d9e Make ALC context not current before destroying it.
Let's hope this fixes the hangs.
2022-08-27 00:32:05 +02:00
wrexbe
07d327eb8b Version: 0.45.0.2 2022-08-25 21:31:36 -07:00
wrexbe
2275ec9573 Update YamlDotNet (#3082) 2022-08-25 21:28:12 -07:00
wrexbe
5848b449f6 Expose way to test sandbox (#3166) 2022-08-25 21:27:42 -07:00
metalgearsloth
67aa32e694 Version: 0.45.0.1 2022-08-26 12:21:45 +10:00
Leon Friedrich
dbd2961b9f Fix client state handling bug (#3192) 2022-08-26 12:00:40 +10:00
Pieter-Jan Briers
b77b49c667 Mapped string serializer hit/miss stats.
Fixes #3190
2022-08-26 01:58:17 +02:00
Leon Friedrich
0eabe62bdb Fix prototype reloading debug assert failure (#3188) 2022-08-26 09:57:58 +10:00
Leon Friedrich
31e2ea2770 Fix AudioParams deserialization (#3191) 2022-08-25 16:46:44 -07:00
metalgearsloth
7a636b3b87 Version: 0.45.0.0 2022-08-25 23:33:10 +10:00
Leon Friedrich
98ce017b4a Physics get-colliding tweaks (#3183) 2022-08-25 23:29:46 +10:00
Leon Friedrich
26b04f0d66 Fix container init IDs (#3184) 2022-08-25 23:28:48 +10:00
Leon Friedrich
f4f2dea688 Fix load grid errors (#3181) 2022-08-25 23:28:29 +10:00
metalgearsloth
e9a0f9a4c1 Bandaid maploader for loading onto an existing one (#3160) 2022-08-24 15:49:46 +10:00
Leon Friedrich
de438ae94c Prevent players from attaching to terminating entities (#3175) 2022-08-24 01:42:48 +10:00
metalgearsloth
9ec77f20ee Version: 0.44.0.0 2022-08-23 16:35:59 +10:00
Leon Friedrich
da01040b52 Hopefully fix container issues? (#3174) 2022-08-23 16:35:02 +10:00
metalgearsloth
dce2a5ddb2 Fix physics grid reparent crash (#3122) 2022-08-23 11:33:36 +10:00
Acruid
5c99fbabf2 Map serialization postmapinit bugfix (#3165) 2022-08-23 10:52:23 +10:00
Leon Friedrich
9d0846c0e9 Add PVS/entity-state debug commands (#3169) 2022-08-23 10:50:23 +10:00
metalgearsloth
035ecfb098 Fix joint collision (#3161)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-08-23 10:48:19 +10:00
metalgearsloth
3693f5aee7 Physics component cleanup (#3158) 2022-08-23 10:45:28 +10:00
ElectroJr
b859815b07 Version: 0.43.1.1 2022-08-22 17:03:25 -04:00
Leon Friedrich
3701ca83e4 Only request full state on missing metadata. (#3170) 2022-08-23 07:01:41 +10:00
Leon Friedrich
1473f1d34c Fix entity state exception tolerance (#3171) 2022-08-23 07:01:31 +10:00
ElectroJr
889c140fb9 Version: 0.43.1.0 2022-08-22 13:32:57 -04:00
metalgearsloth
c8259915f8 LoadBP completion results (#3159) 2022-08-23 02:58:56 +10:00
Leon Friedrich
a2a25fb296 Add session info for client-side ToPrettyString() (#3162) 2022-08-23 02:56:09 +10:00
Leon Friedrich
938a9929ea Fix deferred component remove error (#3163) 2022-08-23 02:55:59 +10:00
wrexbe
7726075b9b Fix gamestate debug display (#3167) 2022-08-23 02:55:41 +10:00
Leon Friedrich
03b3d1bbe7 Maybe fix PVS bug (#3164) 2022-08-23 02:54:50 +10:00
wrexbe
17ec51b74c Add reusing username+id in tests (#3143) 2022-08-22 05:57:07 +10:00
Leon Friedrich
e92998d1ec Remove a PVS warning on full release (#3154) 2022-08-22 05:50:52 +10:00
metalgearsloth
6691512136 Version: 0.43.0.2 2022-08-21 17:10:05 +10:00
metalgearsloth
a80f4ad76c Add broadphase AABB back in (#3157) 2022-08-21 16:56:01 +10:00
Leon Friedrich
2c6f4cd80c Fix PVS/container bug (#3155) 2022-08-21 16:06:44 +10:00
ElectroJr
51a4c6dcf2 Version: 0.43.0.1 2022-08-20 19:49:03 -04:00
Leon Friedrich
1f402e581a Fix state interpolation (#3153) 2022-08-21 09:47:15 +10:00
ElectroJr
17ea92bfda Version: 0.43.0.0 2022-08-20 18:31:48 -04:00
Leon Friedrich
6a8266af7e Revert "Revert "PVS & client state handling changes"" (#3152) 2022-08-21 08:30:28 +10:00
ElectroJr
f6b7606648 Version: 0.42.0.0 2022-08-20 15:28:34 -04:00
Leon Friedrich
9cd8adae93 Revert "PVS & client state handling changes" (#3151) 2022-08-21 05:27:16 +10:00
ElectroJr
c5ba8b75c8 Version: 0.41.0.0 2022-08-20 13:42:20 -04:00
Leon Friedrich
3d73cc7289 Make AudioSystem accept nullable SoundSpecifier (#3133) 2022-08-21 03:40:49 +10:00
Leon Friedrich
11aa062ee0 Un-revert BUI PRs (#3139) 2022-08-21 03:40:29 +10:00
Leon Friedrich
b4358a9e33 PVS & client state handling changes (#3000) 2022-08-21 03:40:18 +10:00
Leon Friedrich
0cce4714a1 Make a specific EntityUid error more descriptive (#3132) 2022-08-20 16:30:11 +10:00
metalgearsloth
9c4e6a6595 Hotfix tile placement brrrrt (#3140) 2022-08-20 16:29:38 +10:00
DrSmugleaf
1ddd541fe9 Add IEntityManager.Systems proxy methods (#3150) 2022-08-20 16:28:36 +10:00
Leon Friedrich
e45aa3f2fe Assert that components are not added to terminating entities (#3144) 2022-08-20 16:28:09 +10:00
Leon Friedrich
99efdb6061 Include stack trace in deletion errors (#3145) 2022-08-20 16:27:54 +10:00
DrSmugleaf
32f0ffdc79 Move ContainerHelpers methods to SharedContainerSystem (#3149) 2022-08-20 07:57:56 +02:00
DrSmugleaf
cf166483c9 Change state manager methods to return the new state (#3137) 2022-08-15 22:05:48 +02:00
Pieter-Jan Briers
49631867f4 Fix sRGB conversion in WindowRoot background color. 2022-08-15 16:51:23 +02:00
metalgearsloth
9f56eaec9a Obsolote EntitySystem.Get<T> (#3142) 2022-08-15 16:22:06 +02:00
DrSmugleaf
04f2b732a5 Add TryFirstOrNull and TryFirstOrDefault without predicates (#3141) 2022-08-15 16:21:40 +02:00
metalgearsloth
b8cfabc339 Version: 0.40.3.3 2022-08-15 14:06:22 +10:00
metalgearsloth
1cdd39202f Make tile grid placement somewhat bearable (#3131)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-08-15 14:05:27 +10:00
Leon Friedrich
3290720b4c Don't error on missing entity suffixes (#3124) 2022-08-15 14:04:50 +10:00
Leon Friedrich
49badb06cb Fix fixture init/state bug. (#3125) 2022-08-15 14:03:57 +10:00
Visne
2c6941e73b Version: 0.40.3.2 2022-08-15 02:49:34 +02:00
wrexbe
5e5883cb88 Fix map schema (#3138) 2022-08-15 02:40:56 +02:00
ElectroJr
02c504445e Version: 0.40.3.1 2022-08-14 19:21:42 -04:00
wrexbe
4d5075a792 Update mapfile_validators.py (#3136) 2022-08-15 09:20:12 +10:00
192 changed files with 6976 additions and 3746 deletions

View File

@@ -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.3.0</Version></PropertyGroup>
</Project>

View File

@@ -0,0 +1,3 @@
- type: uiTheme
id: Default
path: /textures/interface/Default

View 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.

View File

@@ -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.

View File

@@ -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());

View File

@@ -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:

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -48,7 +48,7 @@ namespace Robust.Client.GameObjects
if (_visible == value) return;
_visible = value;
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
}
}
@@ -78,6 +78,7 @@ namespace Robust.Client.GameObjects
get => scale;
set
{
_bounds = _bounds.Scale(value / scale);
scale = value;
UpdateLocalMatrix();
}
@@ -222,7 +223,7 @@ namespace Robust.Client.GameObjects
{
if (_containerOccluded == value) return;
_containerOccluded = value;
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
}
}
@@ -247,7 +248,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)]
@@ -582,7 +583,6 @@ namespace Robust.Client.GameObjects
}
RebuildBounds();
QueueUpdateIsInert();
return index;
}
@@ -610,7 +610,6 @@ namespace Robust.Client.GameObjects
}
RebuildBounds();
QueueUpdateIsInert();
}
public void RemoveLayer(object layerKey)
@@ -634,6 +633,8 @@ namespace Robust.Client.GameObjects
_bounds = _bounds.Union(layer.CalculateBoundingBox());
}
_bounds = _bounds.Scale(Scale);
entities?.EventBus?.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
}
/// <summary>
@@ -1340,6 +1341,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;
@@ -1373,35 +1394,29 @@ 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();
worldRotation = worldRotation.Reduced().FlipPositive();
if (worldRotation.Theta < 0)
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
var cardinal = Angle.Zero;
// 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 +1485,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;
@@ -1509,7 +1522,7 @@ namespace Robust.Client.GameObjects
// 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();
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
if (_inertUpdateQueued)
return;
@@ -1600,18 +1613,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 +1626,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 +1710,7 @@ namespace Robust.Client.GameObjects
_scale = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
_parent.RebuildBounds();
}
}
internal Vector2 _scale = Vector2.One;
@@ -1725,7 +1725,7 @@ namespace Robust.Client.GameObjects
_rotation = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
_parent.RebuildBounds();
}
}
internal Angle _rotation = Angle.Zero;
@@ -1752,7 +1752,7 @@ namespace Robust.Client.GameObjects
_offset = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
_parent.RebuildBounds();
}
}
@@ -1930,7 +1930,6 @@ namespace Robust.Client.GameObjects
{
Visible = value;
_parent.QueueUpdateIsInert();
_parent.RebuildBounds();
}
@@ -2029,35 +2028,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>

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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!;

View File

@@ -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;
@@ -61,7 +62,7 @@ namespace Robust.Client.GameObjects
goto skip;
}
container.EmptyContainer(true, entMan: EntityManager);
EmptyContainer(container, true);
container.Shutdown();
toDelete.Add(id);
@@ -114,13 +115,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 +134,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 +162,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 +172,6 @@ namespace Robust.Client.GameObjects
return;
}
RemoveExpectedEntity(message.Entity);
if (container.Deleted)
return;
@@ -189,20 +191,33 @@ 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))
{
DebugTools.Assert(ExpectedEntities[uid] == container,
$"Expecting entity {ToPrettyString(uid)} to be present in two containers. New: {container.ID} in {ToPrettyString(container.Owner)}. Old: {ExpectedEntities[uid].ID} in {ToPrettyString(ExpectedEntities[uid].Owner)}");
DebugTools.Assert(ExpectedEntities[uid].ExpectedEntities.Contains(uid),
$"Entity {ToPrettyString(uid)} is expected, but not expected in the given container? Container: {ExpectedEntities[uid].ID} in {ToPrettyString(ExpectedEntities[uid].Owner)}");
return;
}
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)

View File

@@ -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)
{

View File

@@ -1,4 +1,4 @@
using JetBrains.Annotations;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -142,6 +142,8 @@ namespace Robust.Client.Graphics.Audio
if (_openALContext != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(_openALContext);
}

View File

@@ -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);

View File

@@ -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];

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>
@@ -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()

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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()
{
}
}

View File

@@ -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);
}
}

View File

@@ -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) { }
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}

View 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
{
}

View 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);
}

View 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);
}

View 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
{
}

View 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);
}

View 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);
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View 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)
{
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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>

View 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);
}
}

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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}");
}

View 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;
}
}

View File

@@ -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
})
}
});
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -0,0 +1,23 @@
<EntitySpawnWindow
xmlns="https://spacestation14.io"
Title="{Loc entity-spawn-window-title}"
Size="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>

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,13 @@
<TileSpawnWindow
xmlns="https://spacestation14.io"
Title="Place Tiles"
Size="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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
{

View 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();
}

View 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);
}

View 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);
}

View File

@@ -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; }

View File

@@ -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();

View 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;
}
}

View 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()
{
}
}

View 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
{
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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];
}
}

View File

@@ -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;
}
}

View 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();
}
}

View File

@@ -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();
}
}
}

View 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);
}
}

View File

@@ -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())

View File

@@ -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])));
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using static Robust.Shared.GameObjects.SharedUserInterfaceComponent;
namespace Robust.Server.GameObjects
@@ -22,16 +18,10 @@ namespace Robust.Server.GameObjects
[ComponentReference(typeof(SharedUserInterfaceComponent))]
public sealed class ServerUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks
{
private readonly Dictionary<object, BoundUserInterface> _interfaces =
internal readonly Dictionary<Enum, BoundUserInterface> _interfaces =
new();
[DataField("interfaces", readOnly: true)]
private List<PrototypeData> _interfaceData = new();
/// <summary>
/// Enumeration of all the interfaces this component provides.
/// </summary>
public IEnumerable<BoundUserInterface> Interfaces => _interfaces.Values;
public IReadOnlyDictionary<Enum, BoundUserInterface> Interfaces => _interfaces;
void ISerializationHooks.AfterDeserialization()
{
@@ -42,35 +32,12 @@ namespace Robust.Server.GameObjects
_interfaces[prototypeData.UiKey] = new BoundUserInterface(prototypeData, this);
}
}
}
public BoundUserInterface GetBoundUserInterface(object uiKey)
{
return _interfaces[uiKey];
}
public bool TryGetBoundUserInterface(object uiKey,
[NotNullWhen(true)] out BoundUserInterface? boundUserInterface)
{
return _interfaces.TryGetValue(uiKey, out boundUserInterface);
}
public BoundUserInterface? GetBoundUserInterfaceOrNull(object uiKey)
{
return TryGetBoundUserInterface(uiKey, out var boundUserInterface)
? boundUserInterface
: null;
}
public bool HasBoundUserInterface(object uiKey)
{
return _interfaces.ContainsKey(uiKey);
}
internal void SendToSession(IPlayerSession session, BoundUserInterfaceMessage message, object uiKey)
{
EntitySystem.Get<UserInterfaceSystem>()
.SendTo(session, new BoundUIWrapMessage(Owner, message, uiKey));
}
[RegisterComponent]
public sealed class ActiveUserInterfaceComponent : Component
{
public HashSet<BoundUserInterface> Interfaces = new();
}
/// <summary>
@@ -79,275 +46,92 @@ namespace Robust.Server.GameObjects
[PublicAPI]
public sealed class BoundUserInterface
{
private bool _isActive;
public float InteractionRangeSqrd;
public object UiKey { get; }
public ServerUserInterfaceComponent Owner { get; }
private readonly HashSet<IPlayerSession> _subscribedSessions = new();
private BoundUserInterfaceState? _lastState;
public Enum UiKey { get; }
public ServerUserInterfaceComponent Component { get; }
public EntityUid Owner => Component.Owner;
internal readonly HashSet<IPlayerSession> _subscribedSessions = new();
internal BoundUIWrapMessage? LastStateMsg;
public bool RequireInputValidation;
private bool _stateDirty;
internal bool StateDirty;
private readonly Dictionary<IPlayerSession, BoundUserInterfaceState> _playerStateOverrides =
internal readonly Dictionary<IPlayerSession, BoundUIWrapMessage> PlayerStateOverrides =
new();
/// <summary>
/// All of the sessions currently subscribed to this UserInterface.
/// </summary>
public IReadOnlyCollection<IPlayerSession> SubscribedSessions => _subscribedSessions;
public IReadOnlySet<IPlayerSession> SubscribedSessions => _subscribedSessions;
[Obsolete("Use system events")]
public event Action<ServerBoundUserInterfaceMessage>? OnReceiveMessage;
public event Action<IPlayerSession>? OnClosed;
public BoundUserInterface(PrototypeData data, ServerUserInterfaceComponent owner)
{
RequireInputValidation = data.RequireInputValidation;
UiKey = data.UiKey;
Owner = owner;
Component = owner;
// One Abs(), because negative values imply no limit
InteractionRangeSqrd = data.InteractionRange * MathF.Abs(data.InteractionRange);
}
/// <summary>
/// Sets a state. This can be used for stateful UI updating, which can be easier to implement,
/// but is more costly on bandwidth.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public void SetState(BoundUserInterfaceState state, IPlayerSession? session = null)
[Obsolete("Use UserInterfaceSystem")]
public void SetState(BoundUserInterfaceState state, IPlayerSession? session = null, bool clearOverrides = true)
{
if (session == null)
{
_lastState = state;
_playerStateOverrides.Clear();
}
else
{
_playerStateOverrides[session] = state;
}
_stateDirty = true;
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().SetUiState(this, state, session, clearOverrides);
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
/// <param name="session">The player session to toggle the UI on.</param>
/// <exception cref="ArgumentException">
/// Thrown if the session's status is <c>Connecting</c> or <c>Disconnected</c>
/// </exception>
/// <exception cref="ArgumentNullException">Thrown if <see cref="session"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public void Toggle(IPlayerSession session)
{
if (_subscribedSessions.Contains(session))
{
Close(session);
}
else
{
Open(session);
}
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().ToggleUi(this, session);
}
/// <summary>
/// Opens this interface for a specific client.
/// </summary>
/// <param name="session">The player session to open the UI on.</param>
/// <exception cref="ArgumentException">
/// Thrown if the session's status is <c>Connecting</c> or <c>Disconnected</c>
/// </exception>
/// <exception cref="ArgumentNullException">Thrown if <see cref="session"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public bool Open(IPlayerSession session)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
{
throw new ArgumentException("Invalid session status.", nameof(session));
}
if (_subscribedSessions.Contains(session))
{
return false;
}
_subscribedSessions.Add(session);
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner.Owner, new BoundUIOpenedEvent(UiKey, Owner.Owner, session), true);
SendMessage(new OpenBoundInterfaceMessage(), session);
if (_lastState != null)
{
SendMessage(new UpdateBoundStateMessage(_lastState));
}
if (!_isActive)
{
_isActive = true;
EntitySystem.Get<UserInterfaceSystem>()
.ActivateInterface(this);
}
session.PlayerStatusChanged += OnSessionOnPlayerStatusChanged;
return true;
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().OpenUi(this, session);
}
private void OnSessionOnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus == SessionStatus.Disconnected)
{
CloseShared(args.Session);
}
}
/// <summary>
/// Close this interface for a specific client.
/// </summary>
/// <param name="session">The session to close the UI on.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="session"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public bool Close(IPlayerSession session)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (!_subscribedSessions.Contains(session))
{
return false;
}
var msg = new CloseBoundInterfaceMessage();
SendMessage(msg, session);
CloseShared(session);
return true;
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().CloseUi(this, session);
}
public void CloseShared(IPlayerSession session)
{
var owner = Owner.Owner;
OnClosed?.Invoke(session);
_subscribedSessions.Remove(session);
_playerStateOverrides.Remove(session);
session.PlayerStatusChanged -= OnSessionOnPlayerStatusChanged;
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(owner, new BoundUIClosedEvent(UiKey, owner, session), true);
if (_subscribedSessions.Count == 0)
{
EntitySystem.Get<UserInterfaceSystem>()
.DeactivateInterface(this);
_isActive = false;
}
}
/// <summary>
/// Closes this interface for any clients that have it open.
/// </summary>
[Obsolete("Use UserInterfaceSystem")]
public void CloseAll()
{
foreach (var session in _subscribedSessions.ToArray())
Close(session);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().CloseAll(this);
}
/// <summary>
/// Returns whether or not a session has this UI open.
/// </summary>
/// <param name="session">The session to check.</param>
/// <returns>True if the player has this UI open, false otherwise.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="session"/> is null.</exception>
[Obsolete("Just check SubscribedSessions.Contains")]
public bool SessionHasOpen(IPlayerSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
return _subscribedSessions.Contains(session);
}
/// <summary>
/// Sends a message to ALL sessions that currently have the UI open.
/// </summary>
/// <param name="message">The message to send.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="message"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public void SendMessage(BoundUserInterfaceMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
foreach (var session in _subscribedSessions)
{
Owner.SendToSession(session, message, UiKey);
}
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().SendUiMessage(this, message);
}
/// <summary>
/// Sends a message to a specific session.
/// </summary>
/// <param name="message">The message to send.</param>
/// <param name="session">The session to send the message to.</param>
/// <exception cref="ArgumentNullException">Thrown if either argument is null.</exception>
/// <exception cref="ArgumentException">Thrown if the session does not have this UI open.</exception>
[Obsolete("Use UserInterfaceSystem")]
public void SendMessage(BoundUserInterfaceMessage message, IPlayerSession session)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
AssertContains(session);
Owner.SendToSession(session, message, UiKey);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().TrySendUiMessage(this, message, session);
}
internal void ReceiveMessage(ServerBoundUserInterfaceMessage message)
internal void InvokeOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
OnReceiveMessage?.Invoke(message);
}
private void AssertContains(IPlayerSession session)
{
if (!SessionHasOpen(session))
{
throw new ArgumentException("Player session does not have this UI open.");
}
}
public void DispatchPendingState()
{
if (!_stateDirty)
{
return;
}
foreach (var playerSession in _subscribedSessions)
{
if (!_playerStateOverrides.ContainsKey(playerSession) && _lastState != null)
{
SendMessage(new UpdateBoundStateMessage(_lastState), playerSession);
}
}
foreach (var (player, state) in _playerStateOverrides)
{
SendMessage(new UpdateBoundStateMessage(state), player);
}
_stateDirty = false;
}
}
[PublicAPI]

View File

@@ -50,8 +50,8 @@ namespace Robust.Server.GameObjects
// Null by default.
forceKicked = null;
// Cannot attach to a deleted/nonexisting entity.
if (EntityManager.Deleted(uid))
// Cannot attach to a deleted, nonexisting or terminating entity.
if (!TryComp(uid, out MetaDataComponent? meta) || meta.EntityLifeStage > EntityLifeStage.MapInitialized)
{
return false;
}

View File

@@ -119,8 +119,11 @@ namespace Robust.Server.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 (sound == null)
return null;
var filter = Filter.Pvs(source, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user);
return Play(sound, filter, source, audioParams);
}

View File

@@ -4,10 +4,11 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
{
@@ -18,9 +19,9 @@ namespace Robust.Server.GameObjects
private readonly List<IPlayerSession> _sessionCache = new();
// List of all bound user interfaces that have at least one player looking at them.
[ViewVariables]
private readonly List<BoundUserInterface> _activeInterfaces = new();
private Dictionary<IPlayerSession, List<BoundUserInterface>> _openInterfaces = new();
[Dependency] private readonly IPlayerManager _playerMan = default!;
/// <inheritdoc />
public override void Initialize()
@@ -29,19 +30,39 @@ namespace Robust.Server.GameObjects
SubscribeNetworkEvent<BoundUIWrapMessage>(OnMessageReceived);
SubscribeLocalEvent<ServerUserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
_playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
}
public override void Shutdown()
{
base.Shutdown();
_playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus != SessionStatus.Disconnected)
return;
if (!_openInterfaces.TryGetValue(args.Session, out var buis))
return;
foreach (var bui in buis.ToArray())
{
CloseShared(bui, args.Session);
}
}
private void OnUserInterfaceShutdown(EntityUid uid, ServerUserInterfaceComponent component, ComponentShutdown args)
{
foreach (var bui in component.Interfaces)
{
DeactivateInterface(bui);
}
}
if (!TryComp(uid, out ActiveUserInterfaceComponent? activeUis))
return;
internal void SendTo(IPlayerSession session, BoundUIWrapMessage msg)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
foreach (var bui in activeUis.Interfaces)
{
DeactivateInterface(bui, activeUis);
}
}
/// <summary>
@@ -53,13 +74,13 @@ namespace Robust.Server.GameObjects
if (!TryComp(uid, out ServerUserInterfaceComponent? uiComp) || args.SenderSession is not IPlayerSession session)
return;
if (!uiComp.TryGetBoundUserInterface(msg.UiKey, out var ui))
if (!uiComp._interfaces.TryGetValue(msg.UiKey, out var ui))
{
Logger.DebugS("go.comp.ui", "Got BoundInterfaceMessageWrapMessage for unknown UI key: {0}", msg.UiKey);
return;
}
if (!ui.SessionHasOpen(session))
if (!ui.SubscribedSessions.Contains(session))
{
Logger.DebugS("go.comp.ui", $"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {session}", msg.UiKey);
return;
@@ -68,7 +89,7 @@ namespace Robust.Server.GameObjects
// if they want to close the UI, we can go home early.
if (msg.Message is CloseBoundInterfaceMessage)
{
ui.CloseShared(session);
CloseShared(ui, session);
return;
}
@@ -93,24 +114,45 @@ namespace Robust.Server.GameObjects
// Once we have populated our message's wrapped message, we will wrap it up into a message that can be sent
// to old component-code.
var WrappedUnwrappedMessageMessageMessage = new ServerBoundUserInterfaceMessage(message, session);
ui.ReceiveMessage(WrappedUnwrappedMessageMessageMessage);
ui.InvokeOnReceiveMessage(WrappedUnwrappedMessageMessageMessage);
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var query = GetEntityQuery<TransformComponent>();
foreach (var userInterface in _activeInterfaces.ToList())
foreach (var (activeUis, xform) in EntityQuery<ActiveUserInterfaceComponent, TransformComponent>(true))
{
CheckRange(userInterface, query);
userInterface.DispatchPendingState();
foreach (var ui in activeUis.Interfaces)
{
CheckRange(activeUis, ui, xform, query);
if (!ui.StateDirty)
continue;
ui.StateDirty = false;
foreach (var (player, state) in ui.PlayerStateOverrides)
{
RaiseNetworkEvent(state, player.ConnectedClient);
}
if (ui.LastStateMsg == null)
continue;
foreach (var session in ui.SubscribedSessions)
{
if (!ui.PlayerStateOverrides.ContainsKey(session))
RaiseNetworkEvent(ui.LastStateMsg, session.ConnectedClient);
}
}
}
}
/// <summary>
/// Verify that the subscribed clients are still in range of the interface.
/// </summary>
private void CheckRange(BoundUserInterface ui, EntityQuery<TransformComponent> query)
private void CheckRange(ActiveUserInterfaceComponent activeUis, BoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
{
if (ui.InteractionRangeSqrd <= 0)
return;
@@ -119,7 +161,6 @@ namespace Robust.Server.GameObjects
_sessionCache.Clear();
_sessionCache.AddRange(ui.SubscribedSessions);
var transform = query.GetComponent(ui.Owner.Owner);
var uiPos = _xformSys.GetWorldPosition(transform, query);
var uiMap = transform.MapID;
@@ -127,175 +168,321 @@ namespace Robust.Server.GameObjects
{
// The component manages the set of sessions, so this invalid session should be removed soon.
if (!query.TryGetComponent(session.AttachedEntity, out var xform))
{
continue;
}
if (uiMap != xform.MapID)
{
ui.Close(session);
CloseUi(ui, session, activeUis);
continue;
}
var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared;
if (distanceSquared > ui.InteractionRangeSqrd)
{
ui.Close(session);
}
CloseUi(ui, session, activeUis);
}
}
internal void DeactivateInterface(BoundUserInterface userInterface)
private void DeactivateInterface(BoundUserInterface ui, ActiveUserInterfaceComponent? activeUis = null)
{
_activeInterfaces.Remove(userInterface);
if (!Resolve(ui.Component.Owner, ref activeUis, false))
return;
activeUis.Interfaces.Remove(ui);
if (activeUis.Interfaces.Count == 0)
RemCompDeferred(activeUis.Owner, activeUis);
}
internal void ActivateInterface(BoundUserInterface userInterface)
private void ActivateInterface(BoundUserInterface ui)
{
_activeInterfaces.Add(userInterface);
EnsureComp<ActiveUserInterfaceComponent>(ui.Component.Owner).Interfaces.Add(ui);
}
#region Proxy Methods
public bool HasUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
#region Get BUI
public bool HasUi(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
return ui.HasBoundUserInterface(uiKey);
return ui._interfaces.ContainsKey(uiKey);
}
public BoundUserInterface GetUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
public BoundUserInterface GetUi(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
throw new InvalidOperationException($"Cannot get {typeof(BoundUserInterface)} from an entity without {typeof(ServerUserInterfaceComponent)}!");
return ui.GetBoundUserInterface(uiKey);
return ui._interfaces[uiKey];
}
public BoundUserInterface? GetUiOrNull(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
public BoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
return TryGetUi(uid, uiKey, out var bui, ui)
? bui
: null;
}
public bool TryGetUi(EntityUid uid, object uiKey, [NotNullWhen(true)] out BoundUserInterface? bui, ServerUserInterfaceComponent? ui = null)
public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out BoundUserInterface? bui, ServerUserInterfaceComponent? ui = null)
{
bui = null;
return Resolve(uid, ref ui, false) && ui.TryGetBoundUserInterface(uiKey, out bui);
return Resolve(uid, ref ui, false) && ui._interfaces.TryGetValue(uiKey, out bui);
}
#endregion
public bool IsUiOpen(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
public bool IsUiOpen(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui, false))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Count > 0;
}
public bool TrySetUiState(EntityUid uid, object uiKey, BoundUserInterfaceState state, IPlayerSession? session = null, ServerUserInterfaceComponent? ui = null)
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui, false))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
bui.SetState(state, session);
return true;
return bui.SubscribedSessions.Contains(session);
}
public bool TryToggleUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public bool TrySetUiState(EntityUid uid,
Enum uiKey,
BoundUserInterfaceState state,
IPlayerSession? session = null,
ServerUserInterfaceComponent? ui = null,
bool clearOverrides = true)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
bui.Toggle(session);
return true;
}
public bool TryOpen(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
return bui.Open(session);
}
public bool TryClose(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.Close(session);
}
public bool TryCloseAll(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
bui.CloseAll();
SetUiState(bui, state, session, clearOverrides);
return true;
}
public bool SessionHasOpenUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public void SetUiState(BoundUserInterface bui, BoundUserInterfaceState state, IPlayerSession? session = null, bool clearOverrides = true)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
return bui.SessionHasOpen(session);
}
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
bui.SendMessage(message);
return true;
}
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
try
var msg = new BoundUIWrapMessage(bui.Component.Owner, new UpdateBoundStateMessage(state), bui.UiKey);
if (session == null)
{
bui.SendMessage(message, session);
bui.LastStateMsg = msg;
if (clearOverrides)
bui.PlayerStateOverrides.Clear();
}
catch (ArgumentException)
else
{
return false;
bui.PlayerStateOverrides[session] = msg;
}
bui.StateDirty = true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
public bool TryToggleUi(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
ToggleUi(bui, session);
return true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
public void ToggleUi(BoundUserInterface bui, IPlayerSession session)
{
if (bui._subscribedSessions.Contains(session))
CloseUi(bui, session);
else
OpenUi(bui, session);
}
#region Open
public bool TryOpen(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return OpenUi(bui, session);
}
/// <summary>
/// Opens this interface for a specific client.
/// </summary>
public bool OpenUi(BoundUserInterface bui, IPlayerSession session)
{
if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
return false;
if (!bui._subscribedSessions.Add(session))
return false;
_openInterfaces.GetOrNew(session).Add(bui);
RaiseLocalEvent(bui.Component.Owner, new BoundUIOpenedEvent(bui.UiKey, bui.Component.Owner, session));
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Component.Owner, new OpenBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
// Fun fact, clients needs to have BUIs open before they can receive the state.....
if (bui.LastStateMsg != null)
RaiseNetworkEvent(bui.LastStateMsg, session.ConnectedClient);
ActivateInterface(bui);
return true;
}
#endregion
#region Close
public bool TryClose(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return CloseUi(bui, session);
}
/// <summary>
/// Close this interface for a specific client.
/// </summary>
public bool CloseUi(BoundUserInterface bui, IPlayerSession session, ActiveUserInterfaceComponent? activeUis = null)
{
if (!bui._subscribedSessions.Remove(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Component.Owner, new CloseBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
CloseShared(bui, session, activeUis);
return true;
}
private void CloseShared(BoundUserInterface bui, IPlayerSession session, ActiveUserInterfaceComponent? activeUis = null)
{
var owner = bui.Component.Owner;
bui._subscribedSessions.Remove(session);
bui.PlayerStateOverrides.Remove(session);
if (_openInterfaces.TryGetValue(session, out var buis))
buis.Remove(bui);
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
if (bui._subscribedSessions.Count == 0)
DeactivateInterface(bui, activeUis);
}
/// <summary>
/// Closes this all interface for any clients that have any open.
/// </summary>
public bool TryCloseAll(EntityUid uid, ActiveUserInterfaceComponent? aui = null)
{
if (!Resolve(uid, ref aui, false))
return false;
foreach (var ui in aui.Interfaces)
{
CloseAll(ui);
}
return true;
}
/// <summary>
/// Closes this specific interface for any clients that have it open.
/// </summary>
public bool TryCloseAll(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
CloseAll(bui);
return true;
}
/// <summary>
/// Closes this interface for any clients that have it open.
/// </summary>
public void CloseAll(BoundUserInterface bui)
{
foreach (var session in bui.SubscribedSessions.ToArray())
{
CloseUi(bui, session);
}
}
#endregion
#region SendMessage
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
SendUiMessage(bui, message);
return true;
}
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public void SendUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage message)
{
var msg = new BoundUIWrapMessage(bui.Component.Owner, message, bui.UiKey);
foreach (var session in bui.SubscribedSessions)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
}
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return TrySendUiMessage(bui, message, session);
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage message, IPlayerSession session)
{
if (!bui.SubscribedSessions.Contains(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Component.Owner, message, bui.UiKey), session.ConnectedClient);
return true;
}

View File

@@ -1,3 +1,8 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates
{
/// <summary>
@@ -16,5 +21,9 @@ namespace Robust.Server.GameStates
void SendGameStateUpdate();
ushort TransformNetId { get; set; }
Action<ICommonSession, GameTick, GameTick>? ClientAck { get; set; }
Action<ICommonSession, GameTick, GameTick, EntityUid?>? ClientRequestFull { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
namespace Robust.Server.GameStates;
namespace Robust.Server.GameStates;
public enum PVSEntityVisiblity : byte
{

Some files were not shown because too many files have changed in this diff Show More