mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e16732eb7b | ||
|
|
91f61bb9de | ||
|
|
ddc91d05ec | ||
|
|
ef22842b90 | ||
|
|
303e2152d2 | ||
|
|
37fc0d0d2a | ||
|
|
53987e1e5d | ||
|
|
3216d7770b | ||
|
|
3203ca2ff4 | ||
|
|
e22254cd51 | ||
|
|
7ed722f669 | ||
|
|
4864096b2a | ||
|
|
5161385de4 | ||
|
|
98e009b38f | ||
|
|
3863ab8f62 | ||
|
|
f576eb5125 | ||
|
|
314742ccd8 | ||
|
|
f9074811f9 | ||
|
|
5f3e1eb378 | ||
|
|
3c1ee20ca1 | ||
|
|
3768f5e68e | ||
|
|
765a560380 | ||
|
|
39ae3ac653 | ||
|
|
e48f4027e5 | ||
|
|
2fa1e98faf | ||
|
|
cedfa0ee2f | ||
|
|
92f44b390e | ||
|
|
65a42f9209 | ||
|
|
ebf53248cf | ||
|
|
289f637e8a | ||
|
|
d7c13f30c8 | ||
|
|
0dac17ae5e | ||
|
|
9a19a774fa | ||
|
|
81f49d5eb2 | ||
|
|
4f3b4ac2d2 | ||
|
|
e428056b52 | ||
|
|
8dc9d2989a | ||
|
|
fd8c90dcbb | ||
|
|
ffe4e5a8ab | ||
|
|
6e5026d270 | ||
|
|
946c4166dc | ||
|
|
7d2fb85a04 | ||
|
|
d6ec078519 | ||
|
|
32256fc4d9 | ||
|
|
37bbdfe7ff | ||
|
|
c906675cdf | ||
|
|
90bb5574c1 | ||
|
|
7b50dcd969 | ||
|
|
8d82f48a8f | ||
|
|
469f9fd219 | ||
|
|
1a5783ab4e | ||
|
|
3d25886d79 | ||
|
|
516b2cd372 | ||
|
|
3cfcfa0be2 | ||
|
|
69328087bd | ||
|
|
1bf8b2a52b | ||
|
|
fc6dc6f4e1 | ||
|
|
31c1feca4e | ||
|
|
3ed1eef2ab | ||
|
|
1394a017bb | ||
|
|
6b0670d5f1 | ||
|
|
f573331541 | ||
|
|
a7218cd3b8 | ||
|
|
f7e8178736 | ||
|
|
31f921e4aa | ||
|
|
aa1c25637c | ||
|
|
71f2c48463 | ||
|
|
d65f4ca898 | ||
|
|
b35568ffe5 | ||
|
|
a0d241e551 | ||
|
|
33a6934582 | ||
|
|
f237a8bbbc | ||
|
|
4bc775c01c | ||
|
|
93b4d81505 | ||
|
|
0afb85a09e | ||
|
|
7b9315cea4 | ||
|
|
dc3af45096 | ||
|
|
00ce0179ae | ||
|
|
81947ba3d8 | ||
|
|
49327279d0 |
Submodule Lidgren.Network/Lidgren.Network updated: 73554e6061...5fc11c2b2b
@@ -28,5 +28,10 @@ void fragment()
|
||||
|
||||
highp float occlusion = ChebyshevUpperBound(occlDist, ourDist);
|
||||
|
||||
if (occlusion >= 1.0)
|
||||
{
|
||||
discard;
|
||||
}
|
||||
|
||||
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - occlusion);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Broadphase;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -321,6 +320,11 @@ namespace Robust.Client.Audio.Midi
|
||||
continue;
|
||||
}
|
||||
|
||||
if (renderer.TrackingEntity != null)
|
||||
{
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
|
||||
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
|
||||
{
|
||||
// just duck out instead of move to NaN
|
||||
|
||||
@@ -27,8 +27,6 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
@@ -66,8 +64,6 @@ namespace Robust.Client
|
||||
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
|
||||
IoCManager.Register<IClientConsoleHost, ClientConsoleHost>();
|
||||
IoCManager.Register<IConsoleHost, ClientConsoleHost>();
|
||||
IoCManager.Register<IFontManager, FontManager>();
|
||||
IoCManager.Register<IFontManagerInternal, FontManager>();
|
||||
IoCManager.Register<IMidiManager, MidiManager>();
|
||||
IoCManager.Register<IAuthManager, AuthManager>();
|
||||
switch (mode)
|
||||
@@ -94,8 +90,9 @@ namespace Robust.Client
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
IoCManager.Register<IFontManager, FontManager>();
|
||||
IoCManager.Register<IFontManagerInternal, FontManager>();
|
||||
IoCManager.Register<IEyeManager, EyeManager>();
|
||||
|
||||
IoCManager.Register<IPlacementManager, PlacementManager>();
|
||||
IoCManager.Register<IOverlayManager, OverlayManager>();
|
||||
IoCManager.Register<IOverlayManagerInternal, OverlayManager>();
|
||||
|
||||
44
Robust.Client/Console/Commands/MonitorCommands.cs
Normal file
44
Robust.Client/Console/Commands/MonitorCommands.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class LsMonitorCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "lsmonitor";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
|
||||
foreach (var monitor in clyde.EnumerateMonitors())
|
||||
{
|
||||
shell.WriteLine(
|
||||
$"[{monitor.Id}] {monitor.Name}: {monitor.Size.X}x{monitor.Size.Y}@{monitor.RefreshRate}Hz");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SetMonitorCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "setmonitor";
|
||||
public string Description => "";
|
||||
public string Help => "Usage: setmonitor <id>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
|
||||
var id = int.Parse(args[0]);
|
||||
var monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
|
||||
clyde.SetWindowMonitor(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -41,14 +42,14 @@ namespace Robust.Client.Debugging
|
||||
|
||||
_debugColliders = value;
|
||||
|
||||
if (value)
|
||||
if (value && !_overlayManager.HasOverlay<PhysicsOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new PhysicsOverlay(_componentManager, _eyeManager,
|
||||
_prototypeManager, _inputManager, _physicsManager));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay(nameof(PhysicsOverlay));
|
||||
_overlayManager.RemoveOverlay<PhysicsOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,13 +67,13 @@ namespace Robust.Client.Debugging
|
||||
|
||||
_debugPositions = value;
|
||||
|
||||
if (value)
|
||||
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new EntityPositionOverlay(_entityManager, _eyeManager));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay(nameof(EntityPositionOverlay));
|
||||
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,8 +92,8 @@ namespace Robust.Client.Debugging
|
||||
private Vector2 _hoverStartScreen = Vector2.Zero;
|
||||
private List<IPhysBody> _hoverBodies = new();
|
||||
|
||||
|
||||
public PhysicsOverlay(IComponentManager compMan, IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IPhysicsManager physicsManager)
|
||||
: base(nameof(PhysicsOverlay))
|
||||
{
|
||||
_componentManager = compMan;
|
||||
_eyeManager = eyeMan;
|
||||
@@ -160,6 +161,8 @@ namespace Robust.Client.Debugging
|
||||
if (viewport.IsEmpty()) return;
|
||||
|
||||
var mapId = _eyeManager.CurrentMap;
|
||||
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
|
||||
var colorEdge = Color.Red.WithAlpha(0.33f);
|
||||
|
||||
foreach (var physBody in EntitySystem.Get<SharedBroadPhaseSystem>().GetCollidingEntities(mapId, viewport))
|
||||
{
|
||||
@@ -169,9 +172,6 @@ namespace Robust.Client.Debugging
|
||||
var worldBox = physBody.GetWorldAABB();
|
||||
if (worldBox.IsEmpty()) continue;
|
||||
|
||||
var colorEdge = Color.Red.WithAlpha(0.33f);
|
||||
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
|
||||
|
||||
foreach (var fixture in physBody.Fixtures)
|
||||
{
|
||||
var shape = fixture.Shape;
|
||||
@@ -193,9 +193,9 @@ namespace Robust.Client.Debugging
|
||||
{
|
||||
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
|
||||
|
||||
foreach (var chr in str)
|
||||
foreach (var rune in str.EnumerateRunes())
|
||||
{
|
||||
var advance = font.DrawChar(handle, chr, baseLine, 1, Color.White);
|
||||
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,7 @@ namespace Robust.Client.Debugging
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public EntityPositionOverlay(IEntityManager entityManager, IEyeManager eyeManager) : base(nameof(EntityPositionOverlay))
|
||||
public EntityPositionOverlay(IEntityManager entityManager, IEyeManager eyeManager)
|
||||
{
|
||||
_entityManager = entityManager;
|
||||
_eyeManager = eyeManager;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.Client.Debugging
|
||||
@@ -38,13 +39,13 @@ namespace Robust.Client.Debugging
|
||||
|
||||
_debugDrawRays = value;
|
||||
|
||||
if (value)
|
||||
if (value && !_overlayManager.HasOverlay<DebugDrawRayOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new DebugDrawRayOverlay(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay(nameof(DebugDrawRayOverlay));
|
||||
_overlayManager.RemoveOverlay<DebugDrawRayOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +82,7 @@ namespace Robust.Client.Debugging
|
||||
private readonly DebugDrawingManager _owner;
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public DebugDrawRayOverlay(DebugDrawingManager owner) : base(nameof(DebugDrawRayOverlay))
|
||||
public DebugDrawRayOverlay(DebugDrawingManager owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -55,7 +56,7 @@ namespace Robust.Client.Debugging
|
||||
IoCManager.Resolve<IOverlayManager>().AddOverlay(new PhysicsDebugOverlay(this));
|
||||
|
||||
if (value == PhysicsDebugFlags.None)
|
||||
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(nameof(PhysicsDebugOverlay));
|
||||
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(typeof(PhysicsDebugOverlay));
|
||||
|
||||
_flags = value;
|
||||
}
|
||||
@@ -119,7 +120,7 @@ namespace Robust.Client.Debugging
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public PhysicsDebugOverlay(DebugPhysicsSystem system) : base(nameof(PhysicsDebugOverlay))
|
||||
public PhysicsDebugOverlay(DebugPhysicsSystem system)
|
||||
{
|
||||
_physics = system;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Management;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Audio.Midi;
|
||||
@@ -80,6 +81,74 @@ namespace Robust.Client
|
||||
}
|
||||
|
||||
public bool Startup(Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
if (!StartupSystemSplash(logHandlerFactory))
|
||||
return false;
|
||||
|
||||
// Disable load context usage on content start.
|
||||
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
|
||||
_modLoader.SetUseLoadContext(!_disableAssemblyLoadContext);
|
||||
_modLoader.SetEnableSandboxing(true);
|
||||
|
||||
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content."))
|
||||
{
|
||||
Logger.Fatal("Errors while loading content assemblies.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var loadedModule in _modLoader.LoadedModules)
|
||||
{
|
||||
_configurationManager.LoadCVarsFromAssembly(loadedModule);
|
||||
}
|
||||
|
||||
IoCManager.Resolve<ISerializationManager>().Initialize();
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
|
||||
|
||||
_resourceCache.PreloadTextures();
|
||||
_userInterfaceManager.Initialize();
|
||||
_networkManager.Initialize(false);
|
||||
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
|
||||
_prototypeManager.Resync();
|
||||
_mapManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
_gameStateManager.Initialize();
|
||||
_placementManager.Initialize();
|
||||
_viewVariablesManager.Initialize();
|
||||
_scriptClient.Initialize();
|
||||
|
||||
_client.Initialize();
|
||||
_discord.Initialize();
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
|
||||
|
||||
if (_commandLineArgs?.Username != null)
|
||||
{
|
||||
_client.PlayerNameOverride = _commandLineArgs.Username;
|
||||
}
|
||||
|
||||
_authManager.LoadFromEnv();
|
||||
|
||||
GC.Collect();
|
||||
|
||||
_clyde.Ready();
|
||||
|
||||
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
|
||||
&& LaunchState.ConnectEndpoint != null)
|
||||
{
|
||||
_client.ConnectToServer(LaunchState.ConnectEndpoint);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
|
||||
{
|
||||
ReadInitialLaunchState();
|
||||
|
||||
@@ -119,6 +188,8 @@ namespace Robust.Client
|
||||
_configurationManager.OverrideConVars(_commandLineArgs.CVars);
|
||||
}
|
||||
|
||||
ProfileOptSetup.Setup(_configurationManager);
|
||||
|
||||
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
|
||||
|
||||
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
|
||||
@@ -129,6 +200,13 @@ namespace Robust.Client
|
||||
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
|
||||
}
|
||||
|
||||
_clyde.TextEntered += TextEntered;
|
||||
_clyde.MouseMove += MouseMove;
|
||||
_clyde.KeyUp += KeyUp;
|
||||
_clyde.KeyDown += KeyDown;
|
||||
_clyde.MouseWheel += MouseWheel;
|
||||
_clyde.CloseWindow += Shutdown;
|
||||
|
||||
// Bring display up as soon as resources are mounted.
|
||||
if (!_clyde.Initialize())
|
||||
{
|
||||
@@ -137,65 +215,7 @@ namespace Robust.Client
|
||||
|
||||
_clyde.SetWindowTitle("Space Station 14");
|
||||
|
||||
_fontManager.Initialize();
|
||||
|
||||
// Disable load context usage on content start.
|
||||
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
|
||||
_modLoader.SetUseLoadContext(!_disableAssemblyLoadContext);
|
||||
_modLoader.SetEnableSandboxing(true);
|
||||
|
||||
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content."))
|
||||
{
|
||||
Logger.Fatal("Errors while loading content assemblies.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var loadedModule in _modLoader.LoadedModules)
|
||||
{
|
||||
_configurationManager.LoadCVarsFromAssembly(loadedModule);
|
||||
}
|
||||
|
||||
IoCManager.Resolve<ISerializationManager>().Initialize();
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
|
||||
|
||||
_userInterfaceManager.Initialize();
|
||||
_networkManager.Initialize(false);
|
||||
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
|
||||
_prototypeManager.Resync();
|
||||
_mapManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
_gameStateManager.Initialize();
|
||||
_placementManager.Initialize();
|
||||
_viewVariablesManager.Initialize();
|
||||
_scriptClient.Initialize();
|
||||
|
||||
_client.Initialize();
|
||||
_discord.Initialize();
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
|
||||
|
||||
if (_commandLineArgs?.Username != null)
|
||||
{
|
||||
_client.PlayerNameOverride = _commandLineArgs.Username;
|
||||
}
|
||||
|
||||
_authManager.LoadFromEnv();
|
||||
|
||||
_clyde.Ready();
|
||||
|
||||
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
|
||||
&& LaunchState.ConnectEndpoint != null)
|
||||
{
|
||||
_client.ConnectToServer(LaunchState.ConnectEndpoint);
|
||||
}
|
||||
|
||||
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -272,17 +292,13 @@ namespace Robust.Client
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
|
||||
_timerManager.UpdateTimers(frameEventArgs);
|
||||
_taskManager.ProcessPendingTasks();
|
||||
_userInterfaceManager.Update(frameEventArgs);
|
||||
|
||||
// GameStateManager is in full control of the simulation update.
|
||||
if (_client.RunLevel >= ClientRunLevel.Connected)
|
||||
{
|
||||
_componentManager.CullRemovedComponents();
|
||||
_gameStateManager.ApplyGameState();
|
||||
_entityManager.Update(frameEventArgs.DeltaSeconds);
|
||||
_playerManager.Update(frameEventArgs.DeltaSeconds);
|
||||
}
|
||||
|
||||
_stateManager.Update(frameEventArgs);
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
|
||||
@@ -212,12 +212,6 @@ namespace Robust.Client.GameObjects
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity SpawnEntityNoMapInit(string? protoName, EntityCoordinates coordinates)
|
||||
{
|
||||
return SpawnEntity(protoName, coordinates);
|
||||
}
|
||||
|
||||
protected override EntityUid GenerateEntityUid()
|
||||
{
|
||||
return new(_nextClientEntityUid++);
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Robust.Client.GameObjects
|
||||
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
public void TickUpdate()
|
||||
{
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
|
||||
{
|
||||
@@ -45,12 +45,12 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntitySystemMessage message)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message)
|
||||
{
|
||||
SendSystemNetworkMessage(message, default(uint));
|
||||
}
|
||||
|
||||
public void SendSystemNetworkMessage(EntitySystemMessage message, uint sequence)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
|
||||
{
|
||||
var msg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
msg.Type = EntityMessageType.SystemMessage;
|
||||
@@ -62,7 +62,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntitySystemMessage message, INetChannel channel)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
@@ -152,6 +152,7 @@ namespace Robust.Client.GameObjects
|
||||
Zoom = state.Zoom;
|
||||
Offset = state.Offset;
|
||||
Rotation = state.Rotation;
|
||||
VisibilityMask = state.VisibilityMask;
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
@@ -19,6 +19,7 @@ using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -36,13 +37,13 @@ namespace Robust.Client.GameObjects
|
||||
private bool _visible = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Visible
|
||||
public override bool Visible
|
||||
{
|
||||
get => _visible;
|
||||
set => _visible = value;
|
||||
}
|
||||
|
||||
[DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))]
|
||||
[DataField("drawdepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
|
||||
private int drawDepth = DrawDepthTag.Default;
|
||||
|
||||
/// <summary>
|
||||
@@ -2084,6 +2085,7 @@ namespace Robust.Client.GameObjects
|
||||
public IEntityManager EntityManager { get; } = null!;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public EntityUid Uid { get; } = EntityUid.Invalid;
|
||||
EntityLifeStage IEntity.LifeStage { get => _lifeStage; set => _lifeStage = value; }
|
||||
public bool Initialized { get; } = false;
|
||||
public bool Initializing { get; } = false;
|
||||
public bool Deleted { get; } = true;
|
||||
@@ -2100,6 +2102,7 @@ namespace Robust.Client.GameObjects
|
||||
public IMetaDataComponent MetaData { get; } = null!;
|
||||
|
||||
private Dictionary<Type, IComponent> _components = new();
|
||||
private EntityLifeStage _lifeStage;
|
||||
|
||||
public T AddComponent<T>() where T : Component, new()
|
||||
{
|
||||
@@ -2147,11 +2150,6 @@ namespace Robust.Client.GameObjects
|
||||
return null!;
|
||||
}
|
||||
|
||||
public IComponent GetComponent(uint netID)
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component) where T : class
|
||||
{
|
||||
component = null;
|
||||
@@ -2178,21 +2176,6 @@ namespace Robust.Client.GameObjects
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryGetComponent(uint netId, [NotNullWhen(true)] out IComponent? component)
|
||||
{
|
||||
component = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IComponent? GetComponentOrNull(uint netId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Robust.Client.GameObjects
|
||||
public static class EntityManagerExt
|
||||
{
|
||||
public static void RaisePredictiveEvent<T>(this IEntityManager entityManager, T msg)
|
||||
where T : EntitySystemMessage
|
||||
where T : EntityEventArgs
|
||||
{
|
||||
var localPlayer = IoCManager.Resolve<IPlayerManager>().LocalPlayer;
|
||||
DebugTools.AssertNotNull(localPlayer);
|
||||
|
||||
@@ -8,7 +8,6 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Broadphase;
|
||||
using Robust.Shared.Player;
|
||||
@@ -165,6 +164,12 @@ namespace Robust.Client.GameObjects
|
||||
Logger.Warning("Interrupting positional audio, can't set position.");
|
||||
stream.Source.StopPlaying();
|
||||
}
|
||||
|
||||
if (stream.TrackingEntity != null)
|
||||
{
|
||||
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +193,7 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null)
|
||||
private IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
|
||||
{
|
||||
@@ -204,7 +209,7 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public IPlayingAudioStream Play(AudioStream stream, AudioParams? audioParams = null)
|
||||
private IPlayingAudioStream Play(AudioStream stream, AudioParams? audioParams = null)
|
||||
{
|
||||
var source = _clyde.CreateAudioSource(stream);
|
||||
ApplyAudioParams(audioParams, source);
|
||||
@@ -226,7 +231,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
|
||||
private IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
|
||||
{
|
||||
@@ -243,7 +248,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
|
||||
private IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
|
||||
{
|
||||
var source = _clyde.CreateAudioSource(stream);
|
||||
if (!source.SetPosition(entity.Transform.WorldPosition))
|
||||
@@ -272,7 +277,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
|
||||
{
|
||||
@@ -289,7 +294,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
|
||||
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
var source = _clyde.CreateAudioSource(stream);
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Event raised by a <see cref="ClientOccluderComponent"/> when it needs to be recalculated.
|
||||
/// </summary>
|
||||
internal sealed class OccluderDirtyEvent : EntitySystemMessage
|
||||
internal sealed class OccluderDirtyEvent : EntityEventArgs
|
||||
{
|
||||
public OccluderDirtyEvent(IEntity sender, (GridId grid, Vector2i pos)? lastPosition, SnapGridOffset offset)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -42,7 +43,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
overlayManager.RemoveOverlay("EffectSystem");
|
||||
overlayManager.RemoveOverlay(typeof(EffectOverlay));
|
||||
}
|
||||
|
||||
public void CreateEffect(EffectSystemMessage message)
|
||||
@@ -329,7 +330,6 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
private readonly IPlayerManager _playerManager;
|
||||
|
||||
public override bool AlwaysDirty => true;
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly ShaderInstance _unshadedShader;
|
||||
@@ -337,8 +337,7 @@ namespace Robust.Client.GameObjects
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
|
||||
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager) : base(
|
||||
"EffectSystem")
|
||||
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager)
|
||||
{
|
||||
_owner = owner;
|
||||
_unshadedShader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Entity system message that is raised when the player changes attached entities.
|
||||
/// </summary>
|
||||
public class PlayerAttachSysMessage : EntitySystemMessage
|
||||
public class PlayerAttachSysMessage : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// New entity the player is attached to.
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Robust.Client.GameStates
|
||||
private uint _nextInputCmdSeq = 1;
|
||||
private readonly Queue<FullInputCmdMessage> _pendingInputs = new();
|
||||
|
||||
private readonly Queue<(uint sequence, GameTick sourceTick, EntitySystemMessage msg, object sessionMsg)>
|
||||
private readonly Queue<(uint sequence, GameTick sourceTick, EntityEventArgs msg, object sessionMsg)>
|
||||
_pendingSystemMessages
|
||||
= new();
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace Robust.Client.GameStates
|
||||
_nextInputCmdSeq++;
|
||||
}
|
||||
|
||||
public uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage
|
||||
public uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs
|
||||
{
|
||||
if (!Predicting)
|
||||
{
|
||||
@@ -242,65 +242,67 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (!Predicting) return;
|
||||
|
||||
using var _ = _timing.StartPastPredictionArea();
|
||||
|
||||
|
||||
if (_pendingInputs.Count > 0)
|
||||
using(var _ = _timing.StartPastPredictionArea())
|
||||
{
|
||||
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
|
||||
if (_pendingInputs.Count > 0)
|
||||
{
|
||||
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
|
||||
}
|
||||
|
||||
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
|
||||
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
|
||||
var hasPendingInput = pendingInputEnumerator.MoveNext();
|
||||
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
|
||||
|
||||
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
|
||||
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
|
||||
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
|
||||
|
||||
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
|
||||
|
||||
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
|
||||
{
|
||||
var tick = new GameTick(t);
|
||||
_timing.CurTick = tick;
|
||||
|
||||
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
|
||||
{
|
||||
var inputCmd = pendingInputEnumerator.Current;
|
||||
|
||||
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
|
||||
|
||||
Logger.DebugS(CVars.NetPredict.Name,
|
||||
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
|
||||
$"state={inputCmd.State}");
|
||||
|
||||
|
||||
input.PredictInputCommand(inputCmd);
|
||||
|
||||
hasPendingInput = pendingInputEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
|
||||
{
|
||||
var msg = pendingMessagesEnumerator.Current.msg;
|
||||
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
|
||||
|
||||
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
if (t != targetTick)
|
||||
{
|
||||
// Don't run EntitySystemManager.TickUpdate if this is the target tick,
|
||||
// because the rest of the main loop will call into it with the target tick later,
|
||||
// and it won't be a past prediction.
|
||||
_entitySystemManager.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
|
||||
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
|
||||
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
|
||||
var hasPendingInput = pendingInputEnumerator.MoveNext();
|
||||
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
|
||||
|
||||
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
|
||||
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
|
||||
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
|
||||
|
||||
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
|
||||
|
||||
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
|
||||
{
|
||||
var tick = new GameTick(t);
|
||||
_timing.CurTick = tick;
|
||||
|
||||
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
|
||||
{
|
||||
var inputCmd = pendingInputEnumerator.Current;
|
||||
|
||||
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
|
||||
|
||||
Logger.DebugS(CVars.NetPredict.Name,
|
||||
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
|
||||
$"state={inputCmd.State}");
|
||||
|
||||
|
||||
input.PredictInputCommand(inputCmd);
|
||||
|
||||
hasPendingInput = pendingInputEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
|
||||
{
|
||||
var msg = pendingMessagesEnumerator.Current.msg;
|
||||
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
|
||||
|
||||
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
if (t != targetTick)
|
||||
{
|
||||
// Don't run EntitySystemManager.Update if this is the target tick,
|
||||
// because the rest of the main loop will call into it with the target tick later,
|
||||
// and it won't be a past prediction.
|
||||
_entitySystemManager.Update((float) _timing.TickPeriod.TotalSeconds);
|
||||
((IEntityEventBus) _entities.EventBus).ProcessEventQueue();
|
||||
}
|
||||
}
|
||||
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
|
||||
}
|
||||
|
||||
private void ResetPredictedEntities(GameTick curTick)
|
||||
|
||||
@@ -70,6 +70,6 @@ namespace Robust.Client.GameStates
|
||||
/// <param name="message">Message being dispatched.</param>
|
||||
void InputCommandDispatched(FullInputCmdMessage message);
|
||||
|
||||
uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage;
|
||||
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Configuration;
|
||||
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.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
@@ -27,13 +30,13 @@ namespace Robust.Client.GameStates
|
||||
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly Font _font;
|
||||
private readonly int _lineHeight;
|
||||
private readonly List<NetEntity> _netEnts = new();
|
||||
|
||||
public NetEntityOverlay() : base(nameof(NetEntityOverlay))
|
||||
public NetEntityOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
@@ -42,12 +45,12 @@ namespace Robust.Client.GameStates
|
||||
|
||||
_gameStateManager.GameStateApplied += HandleGameStateApplied;
|
||||
}
|
||||
|
||||
|
||||
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++)
|
||||
{
|
||||
@@ -74,7 +77,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (netEnt.Id != entityState.Uid)
|
||||
continue;
|
||||
|
||||
|
||||
//TODO: calculate size of state and record it here.
|
||||
netEnt.Traffic[^1] = 1;
|
||||
netEnt.LastUpdate = gameState.ToSequence;
|
||||
@@ -94,15 +97,15 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
|
||||
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
var pvsCenter = _eyeManager.CurrentEye.Position;
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize*2, pvsSize*2));
|
||||
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))
|
||||
{
|
||||
//TODO: Whoever is working on PVS remake, change the InPVS detection.
|
||||
@@ -123,22 +126,58 @@ namespace Robust.Client.GameStates
|
||||
_netEnts[i] = netEnt; // copy struct back
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
return;
|
||||
|
||||
switch (currentSpace)
|
||||
{
|
||||
case OverlaySpace.ScreenSpace:
|
||||
DrawScreen(handle);
|
||||
break;
|
||||
case OverlaySpace.WorldSpace:
|
||||
DrawWorld(handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWorld(DrawingHandleBase handle)
|
||||
{
|
||||
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
|
||||
|
||||
if(!pvsEnabled)
|
||||
return;
|
||||
|
||||
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
var pvsCenter = _eyeManager.CurrentEye.Position;
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
|
||||
|
||||
var worldHandle = (DrawingHandleWorld)handle;
|
||||
|
||||
worldHandle.DrawRect(pvsBox, Color.Red, false);
|
||||
}
|
||||
|
||||
private void DrawScreen(DrawingHandleBase handle)
|
||||
{
|
||||
// remember, 0,0 is top left of ui with +X right and +Y down
|
||||
var screenHandle = (DrawingHandleScreen)handle;
|
||||
|
||||
var screenHandle = (DrawingHandleScreen) handle;
|
||||
|
||||
for (int i = 0; i < _netEnts.Count; i++)
|
||||
{
|
||||
var netEnt = _netEnts[i];
|
||||
|
||||
if (!_entityManager.TryGetEntity(netEnt.Id, out var ent))
|
||||
{
|
||||
_netEnts.RemoveSwap(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
var xPos = 100;
|
||||
var yPos = 10 + _lineHeight * i;
|
||||
var name = $"({netEnt.Id}) {_entityManager.GetEntity(netEnt.Id).Prototype?.ID}";
|
||||
var name = $"({netEnt.Id}) {ent.Prototype?.ID}";
|
||||
var color = CalcTextColor(ref netEnt);
|
||||
DrawString(screenHandle, _font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
|
||||
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
|
||||
@@ -179,20 +218,19 @@ namespace Robust.Client.GameStates
|
||||
return Color.Green; // Entity in PVS, but not updated recently.
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
|
||||
|
||||
base.Dispose(disposing);
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private static void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str, Color textColor)
|
||||
{
|
||||
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
|
||||
|
||||
foreach (var chr in str)
|
||||
foreach (var rune in str.EnumerateRunes())
|
||||
{
|
||||
var advance = font.DrawChar(handle, chr, baseLine, 1, textColor);
|
||||
var advance = font.DrawChar(handle, rune, baseLine, 1, textColor);
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +263,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
|
||||
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,14 +276,14 @@ namespace Robust.Client.GameStates
|
||||
var bValue = iValue > 0;
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if(bValue && !overlayMan.HasOverlay(nameof(NetEntityOverlay)))
|
||||
if(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
{
|
||||
overlayMan.AddOverlay(new NetEntityOverlay());
|
||||
shell.WriteLine("Enabled network entity report overlay.");
|
||||
}
|
||||
else if(!bValue && overlayMan.HasOverlay(nameof(NetEntityOverlay)))
|
||||
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
{
|
||||
overlayMan.RemoveOverlay(nameof(NetEntityOverlay));
|
||||
overlayMan.RemoveOverlay(typeof(NetEntityOverlay));
|
||||
shell.WriteLine("Disabled network entity report overlay.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
@@ -34,7 +38,11 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private readonly List<(GameTick Tick, int Payload, int lag, int interp)> _history = new(HistorySize+10);
|
||||
|
||||
public NetGraphOverlay() : base(nameof(NetGraphOverlay))
|
||||
private int _totalHistoryPayload; // sum of all data point sizes in bytes
|
||||
|
||||
public EntityUid WatchEntId { get; set; }
|
||||
|
||||
public NetGraphOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
@@ -58,7 +66,73 @@ namespace Robust.Client.GameStates
|
||||
// calc interp info
|
||||
var interpBuff = _gameStateManager.CurrentBufferSize - _gameStateManager.MinBufferSize;
|
||||
|
||||
_totalHistoryPayload += sz;
|
||||
_history.Add((toSeq, sz, lag, interpBuff));
|
||||
|
||||
// not watching an ent
|
||||
if(!WatchEntId.IsValid() || WatchEntId.IsClientSide())
|
||||
return;
|
||||
|
||||
string? entStateString = null;
|
||||
string? entDelString = null;
|
||||
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
|
||||
|
||||
var entStates = args.AppliedState.EntityStates;
|
||||
if (entStates is not null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var entState in entStates)
|
||||
{
|
||||
if (entState.Uid == WatchEntId)
|
||||
{
|
||||
if(entState.ComponentChanges is not null)
|
||||
{
|
||||
sb.Append($"\n Changes:");
|
||||
foreach (var compChange in entState.ComponentChanges)
|
||||
{
|
||||
var del = compChange.Deleted ? 'D' : 'C';
|
||||
sb.Append($"\n [{del}]{compChange.NetID}:{compChange.ComponentName}");
|
||||
}
|
||||
}
|
||||
|
||||
if (entState.ComponentStates is not null)
|
||||
{
|
||||
sb.Append($"\n States:");
|
||||
foreach (var compState in entState.ComponentStates)
|
||||
{
|
||||
sb.Append($"\n {compState.NetID}:{compState.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entStateString = sb.ToString();
|
||||
}
|
||||
|
||||
var entDeletes = args.AppliedState.EntityDeletions;
|
||||
if (entDeletes is not null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var entDelete in entDeletes)
|
||||
{
|
||||
if (entDelete == WatchEntId)
|
||||
{
|
||||
entDelString = "\n Deleted";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
|
||||
{
|
||||
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
|
||||
if (!string.IsNullOrWhiteSpace(entStateString))
|
||||
fullString += entStateString;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entDelString))
|
||||
fullString += entDelString;
|
||||
|
||||
conShell.WriteLine(fullString + "\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -67,10 +141,16 @@ namespace Robust.Client.GameStates
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var over = _history.Count - HistorySize;
|
||||
if (over > 0)
|
||||
if (over <= 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < over; i++)
|
||||
{
|
||||
_history.RemoveRange(0, over);
|
||||
var point = _history[i];
|
||||
_totalHistoryPayload -= point.Payload;
|
||||
}
|
||||
|
||||
_history.RemoveRange(0, over);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
@@ -80,6 +160,7 @@ namespace Robust.Client.GameStates
|
||||
var leftMargin = 300;
|
||||
var width = HistorySize;
|
||||
var height = 500;
|
||||
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
|
||||
|
||||
// bottom payload line
|
||||
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
|
||||
@@ -99,6 +180,12 @@ namespace Robust.Client.GameStates
|
||||
var yoff = height - state.Payload / BytesPerPixel;
|
||||
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
|
||||
|
||||
// Draw size if above average
|
||||
if (drawSizeThreshold * 1.5 < state.Payload)
|
||||
{
|
||||
DrawString((DrawingHandleScreen) handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
|
||||
}
|
||||
|
||||
// second tick marks
|
||||
if (state.Tick.Value % _gameTiming.TickRate == 0)
|
||||
{
|
||||
@@ -123,6 +210,10 @@ namespace Robust.Client.GameStates
|
||||
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + state.interp * 6), interpColor.WithAlpha(0.8f));
|
||||
}
|
||||
|
||||
// average payload line
|
||||
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
|
||||
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));
|
||||
@@ -142,20 +233,20 @@ namespace Robust.Client.GameStates
|
||||
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
|
||||
|
||||
base.Dispose(disposing);
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str)
|
||||
{
|
||||
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
|
||||
|
||||
foreach (var chr in str)
|
||||
foreach (var rune in str.EnumerateRunes())
|
||||
{
|
||||
var advance = font.DrawChar(handle, chr, baseLine, 1, Color.White);
|
||||
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
}
|
||||
@@ -183,17 +274,48 @@ namespace Robust.Client.GameStates
|
||||
var bValue = iValue > 0;
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if(bValue && !overlayMan.HasOverlay(nameof(NetGraphOverlay)))
|
||||
if(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
{
|
||||
overlayMan.AddOverlay(new NetGraphOverlay());
|
||||
shell.WriteLine("Enabled network overlay.");
|
||||
}
|
||||
else if(overlayMan.HasOverlay(nameof(NetGraphOverlay)))
|
||||
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
{
|
||||
overlayMan.RemoveOverlay(nameof(NetGraphOverlay));
|
||||
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
|
||||
shell.WriteLine("Disabled network overlay.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NetWatchEntCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "net_watchent";
|
||||
public string Help => "net_watchent <0|EntityUid>";
|
||||
public string Description => "Dumps all network updates for an EntityId to the console.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 1 argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var eValue))
|
||||
{
|
||||
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
|
||||
return;
|
||||
}
|
||||
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
{
|
||||
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
|
||||
|
||||
netOverlay.WatchEntId = eValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -5,6 +6,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
@@ -18,7 +20,7 @@ namespace Robust.Client.GameStates
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
public NetInterpOverlay() : base(nameof(NetInterpOverlay))
|
||||
public NetInterpOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
|
||||
@@ -85,14 +87,14 @@ namespace Robust.Client.GameStates
|
||||
var bValue = iValue > 0;
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (bValue && !overlayMan.HasOverlay(nameof(NetInterpOverlay)))
|
||||
if (bValue && !overlayMan.HasOverlay<NetInterpOverlay>())
|
||||
{
|
||||
overlayMan.AddOverlay(new NetInterpOverlay());
|
||||
shell.WriteLine("Enabled network interp overlay.");
|
||||
}
|
||||
else if (overlayMan.HasOverlay(nameof(NetInterpOverlay)))
|
||||
else if (overlayMan.HasOverlay<NetInterpOverlay>())
|
||||
{
|
||||
overlayMan.RemoveOverlay(nameof(NetInterpOverlay));
|
||||
overlayMan.RemoveOverlay<NetInterpOverlay>();
|
||||
shell.WriteLine("Disabled network interp overlay.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JetBrains.Annotations;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_configurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
}
|
||||
|
||||
private void _audioCreateContext()
|
||||
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void _audioOpenDevice()
|
||||
{
|
||||
var preferredDevice = _configurationManager.GetCVar(CVars.AudioDevice);
|
||||
var preferredDevice = ConfigurationManager.GetCVar(CVars.AudioDevice);
|
||||
|
||||
// Open device.
|
||||
if (!string.IsNullOrEmpty(preferredDevice))
|
||||
@@ -482,7 +482,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var (x, y) = position;
|
||||
|
||||
if (!ValidatePosition(x, y))
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -503,7 +503,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ValidatePosition(float x, float y)
|
||||
private static bool AreFinite(float x, float y)
|
||||
{
|
||||
if (float.IsFinite(x) && float.IsFinite(y))
|
||||
{
|
||||
@@ -513,6 +513,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = velocity;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
|
||||
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
_checkDisposed();
|
||||
@@ -667,7 +683,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
|
||||
private void SetOcclusionEfx(float gain, float cutoff)
|
||||
{
|
||||
if (FilterHandle == 0)
|
||||
@@ -694,7 +709,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var (x, y) = position;
|
||||
|
||||
if (!ValidatePosition(x, y))
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -706,7 +721,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ValidatePosition(float x, float y)
|
||||
private static bool AreFinite(float x, float y)
|
||||
{
|
||||
if (float.IsFinite(x) && float.IsFinite(y))
|
||||
{
|
||||
@@ -716,6 +731,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = velocity;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
|
||||
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
static Clyde()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
|
||||
Environment.GetEnvironmentVariable("ROBUST_INTEGRATED_GPU") != "1")
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Shared.Log;
|
||||
@@ -115,7 +115,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var prev = cap;
|
||||
var cVarName = $"display.ogl_block_{capName}";
|
||||
var block = _configurationManager.GetCVar<bool>(cVarName);
|
||||
var block = ConfigurationManager.GetCVar<bool>(cVarName);
|
||||
|
||||
if (block)
|
||||
{
|
||||
@@ -146,7 +146,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
foreach (var cvar in cvars)
|
||||
{
|
||||
_configurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
|
||||
ConfigurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
@@ -68,8 +70,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
RenderOverlays(OverlaySpace.ScreenSpaceBelowWorld);
|
||||
|
||||
_mainViewport.Eye = _eyeManager.CurrentEye;
|
||||
RenderViewport(_mainViewport);
|
||||
|
||||
RenderViewport(_mainViewport); //Worldspace overlays are rendered here.
|
||||
{
|
||||
var handle = _renderHandle.DrawingHandleScreen;
|
||||
var tex = _mainViewport.RenderTarget.Texture;
|
||||
@@ -107,25 +108,67 @@ namespace Robust.Client.Graphics.Clyde
|
||||
list.Add(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
list.Sort(OverlayComparer.Instance);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
overlay.ClydeRender(_renderHandle, space);
|
||||
}
|
||||
|
||||
FlushRenderQueue();
|
||||
list.Sort(OverlayComparer.Instance);
|
||||
foreach (var overlay in list) {
|
||||
if (overlay.RequestScreenTexture) {
|
||||
FlushRenderQueue();
|
||||
UpdateOverlayScreenTexture(space, _mainViewport.RenderTarget);
|
||||
}
|
||||
if (overlay.OverwriteTargetFrameBuffer()) {
|
||||
ClearFramebuffer(default);
|
||||
}
|
||||
overlay.ClydeRender(_renderHandle, space);
|
||||
FlushRenderQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEntitiesAndWorldOverlay(Viewport viewport, Box2 worldBounds)
|
||||
private ClydeTexture? ScreenBufferTexture;
|
||||
private GLHandle screenBufferHandle;
|
||||
private Vector2 lastFrameSize;
|
||||
/// <summary>
|
||||
/// Sends SCREEN_TEXTURE to all overlays in the given OverlaySpace that request it.
|
||||
/// </summary>
|
||||
private bool UpdateOverlayScreenTexture(OverlaySpace space, RenderTexture texture) {
|
||||
//This currently does NOT consider viewports and just grabs the current screen framebuffer. This will need to be improved upon in the future.
|
||||
List<Overlay> oTargets = new List<Overlay>();
|
||||
foreach (var overlay in _overlayManager.AllOverlays) {
|
||||
if (overlay.RequestScreenTexture && overlay.Space == space) {
|
||||
oTargets.Add(overlay);
|
||||
}
|
||||
}
|
||||
if (oTargets.Count > 0 && ScreenBufferTexture != null) {
|
||||
if (lastFrameSize != _framebufferSize) {
|
||||
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, _hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, _framebufferSize.X, _framebufferSize.Y, 0,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
}
|
||||
lastFrameSize = _framebufferSize;
|
||||
CopyRenderTextureToTexture(texture, ScreenBufferTexture);
|
||||
foreach (Overlay overlay in oTargets) {
|
||||
overlay.ScreenTexture = ScreenBufferTexture;
|
||||
}
|
||||
oTargets.Clear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void DrawEntities(Viewport viewport, Box2 worldBounds)
|
||||
{
|
||||
if (_eyeManager.CurrentMap == MapId.Nullspace || !_mapManager.HasMapEntity(_eyeManager.CurrentMap))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RenderOverlays(OverlaySpace.WorldSpaceBelowEntities);
|
||||
|
||||
var screenSize = viewport.Size;
|
||||
|
||||
// So we could calculate the correct size of the entities based on the contents of their sprite...
|
||||
@@ -267,14 +310,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
_drawingSpriteList.Clear();
|
||||
FlushRenderQueue();
|
||||
|
||||
// Cleanup remainders
|
||||
foreach (var overlay in worldOverlays)
|
||||
{
|
||||
overlay.ClydeRender(_renderHandle, OverlaySpace.WorldSpace);
|
||||
}
|
||||
|
||||
FlushRenderQueue();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
@@ -368,12 +403,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// We will also render worldspace overlays here so we can do them under / above entities as necessary
|
||||
using (DebugGroup("Entities"))
|
||||
{
|
||||
DrawEntitiesAndWorldOverlay(viewport, worldBounds);
|
||||
DrawEntities(viewport, worldBounds);
|
||||
}
|
||||
|
||||
RenderOverlays(OverlaySpace.WorldSpaceBelowFOV);
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
|
||||
{
|
||||
GL.Clear(ClearBufferMask.StencilBufferBit);
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
GL.StencilOp(OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Replace);
|
||||
GL.StencilFunc(StencilFunction.Always, 1, 0xFF);
|
||||
GL.StencilMask(0xFF);
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
GL.StencilMask(0x00);
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +445,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
viewport.WallBleedIntermediateRenderTarget2.Texture,
|
||||
UIBox2.FromDimensions(Vector2.Zero, ScreenSize), new Color(1, 1, 1, 0.5f));
|
||||
}
|
||||
|
||||
|
||||
RenderOverlays(OverlaySpace.WorldSpace);
|
||||
|
||||
GL.StencilFunc(StencilFunction.Notequal, 1, 0xFF);
|
||||
GL.Disable(EnableCap.DepthTest);
|
||||
RenderOverlays(OverlaySpace.WorldSpaceFOVStencil);
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
|
||||
PopRenderStateFull(state);
|
||||
|
||||
@@ -5,6 +5,7 @@ using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using ES20 = OpenToolkit.Graphics.ES20;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -31,6 +32,27 @@ namespace Robust.Client.Graphics.Clyde
|
||||
CheckGlError();
|
||||
GL.BindTexture(TextureTarget.Texture2D, glHandle.Handle);
|
||||
CheckGlError();
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
}
|
||||
|
||||
private void CopyRenderTextureToTexture(RenderTexture source, ClydeTexture target) {
|
||||
LoadedRenderTarget sourceLoaded = RtToLoaded(source);
|
||||
bool pause = sourceLoaded != _currentBoundRenderTarget;
|
||||
FullStoredRendererState? store = null;
|
||||
if (pause) {
|
||||
store = PushRenderStateFull();
|
||||
BindRenderTargetFull(source);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
GL.BindTexture(TextureTarget.Texture2D, _loadedTextures[target.TextureId].OpenGLObject.Handle);
|
||||
CheckGlError();
|
||||
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, _framebufferSize.X, _framebufferSize.Y);
|
||||
CheckGlError();
|
||||
|
||||
if (pause && store != null) {
|
||||
PopRenderStateFull((FullStoredRendererState)store);
|
||||
}
|
||||
}
|
||||
|
||||
private static long EstPixelSize(PixelInternalFormat format)
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.GameObjects.ClientOccluderComponent;
|
||||
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
|
||||
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
@@ -195,7 +196,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_lightSoftShaderHandle = LoadShaderHandle("/Shaders/Internal/light-soft.swsl");
|
||||
_lightHardShaderHandle = LoadShaderHandle("/Shaders/Internal/light-hard.swsl");
|
||||
_fovShaderHandle = LoadShaderHandle("/Shaders/Internal/fov.swsl");
|
||||
@@ -361,16 +361,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FinalizeDepthDraw();
|
||||
}
|
||||
|
||||
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
|
||||
CheckGlError();
|
||||
GLClearColor(_lightManager.AmbientLightColor);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit);
|
||||
CheckGlError();
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
_isStencilling = true;
|
||||
|
||||
var (lightW, lightH) = GetLightMapSize(viewport.Size);
|
||||
GL.Viewport(0, 0, lightW, lightH);
|
||||
CheckGlError();
|
||||
|
||||
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
|
||||
CheckGlError();
|
||||
GLClearColor(_lightManager.AmbientLightColor);
|
||||
GL.ClearStencil(0xFF);
|
||||
GL.StencilMask(0xFF);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
|
||||
CheckGlError();
|
||||
|
||||
ApplyLightingFovToBuffer(viewport, eye);
|
||||
|
||||
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle].Program;
|
||||
lightShader.Use();
|
||||
|
||||
@@ -382,6 +389,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
CheckGlError();
|
||||
|
||||
GL.StencilFunc(StencilFunction.Equal, 0xFF, 0xFF);
|
||||
CheckGlError();
|
||||
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Keep);
|
||||
CheckGlError();
|
||||
|
||||
var lastRange = float.NaN;
|
||||
var lastPower = float.NaN;
|
||||
var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN);
|
||||
@@ -463,11 +475,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
ResetBlendFunc();
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
_isStencilling = false;
|
||||
|
||||
CheckGlError();
|
||||
|
||||
ApplyLightingFovToBuffer(viewport, eye);
|
||||
|
||||
BlurOntoWalls(viewport, eye);
|
||||
|
||||
MergeWallLayer(viewport);
|
||||
@@ -690,6 +702,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
fovShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
|
||||
fovShader.SetUniformMaybe("center", eye.Position.Position);
|
||||
|
||||
GL.StencilMask(0xFF);
|
||||
CheckGlError();
|
||||
GL.StencilFunc(StencilFunction.Always, 0, 0);
|
||||
CheckGlError();
|
||||
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Replace);
|
||||
CheckGlError();
|
||||
|
||||
DrawBlit(viewport, fovShader);
|
||||
|
||||
if (_hasGLSamplerObjects)
|
||||
@@ -939,7 +958,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8,
|
||||
name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}");
|
||||
|
||||
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize, lightMapColorFormat,
|
||||
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize,
|
||||
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true),
|
||||
lightMapSampleParameters,
|
||||
$"{viewport.Name}-{nameof(viewport.LightRenderTarget)}");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
@@ -280,7 +280,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush the render handle, processing and re-pooling all the command lists.
|
||||
/// Flushes the render handle, processing and re-pooling all the command lists.
|
||||
/// </summary>
|
||||
private void FlushRenderQueue()
|
||||
{
|
||||
@@ -371,6 +371,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
program.Use();
|
||||
|
||||
int textureUnitVal = 0;
|
||||
// Assign shader parameters to uniform since they may be dirty.
|
||||
foreach (var (name, value) in instance.Parameters)
|
||||
{
|
||||
@@ -413,6 +414,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case Matrix4 matrix4:
|
||||
program.SetUniform(name, matrix4);
|
||||
break;
|
||||
case ClydeTexture clydeTexture:
|
||||
//It's important to start at Texture6 here since DrawCommandBatch uses Texture0 and Texture1 immediately after calling this
|
||||
//function! If passing in textures as uniforms ever stops working it might be since someone made it use all the way up to Texture6 too.
|
||||
//Might change this in the future?
|
||||
TextureUnit cTarget = TextureUnit.Texture6+textureUnitVal;
|
||||
SetTexture(cTarget, ((ClydeTexture)clydeTexture).TextureId);
|
||||
program.SetUniformTexture(name, cTarget);
|
||||
textureUnitVal++;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unable to handle shader parameter {name}: {value}");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -428,7 +428,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private protected override void SetParameterImpl(string name, Texture value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetStencilOpImpl(StencilOp op)
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private readonly ConcurrentQueue<ClydeHandle> _textureDisposeQueue = new();
|
||||
|
||||
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
TextureLoadParameters? loadParams = null)
|
||||
{
|
||||
DebugTools.Assert(_mainThread == Thread.CurrentThread);
|
||||
@@ -37,7 +37,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return LoadTextureFromImage(image, name, loadParams);
|
||||
}
|
||||
|
||||
public Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
DebugTools.Assert(_mainThread == Thread.CurrentThread);
|
||||
@@ -56,19 +56,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Disable sRGB so stuff doesn't get interpreter wrong.
|
||||
actualParams.Srgb = false;
|
||||
var img = ApplyA8Swizzle((Image<A8>) (object) image);
|
||||
using var img = ApplyA8Swizzle((Image<A8>) (object) image);
|
||||
return LoadTextureFromImage(img, name, loadParams);
|
||||
}
|
||||
|
||||
if (pixelType == typeof(L8) && !actualParams.Srgb)
|
||||
{
|
||||
var img = ApplyL8Swizzle((Image<L8>) (object) image);
|
||||
using var img = ApplyL8Swizzle((Image<L8>) (object) image);
|
||||
return LoadTextureFromImage(img, name, loadParams);
|
||||
}
|
||||
}
|
||||
|
||||
// Flip image because OpenGL reads images upside down.
|
||||
var copy = FlipClone(image);
|
||||
using var copy = FlipClone(image);
|
||||
|
||||
var texture = CreateBaseTextureInternal<T>(image.Width, image.Height, actualParams, name);
|
||||
|
||||
@@ -324,11 +324,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (typeof(T) == typeof(A8))
|
||||
{
|
||||
SetSubImage(texture, dstTl, ApplyA8Swizzle((Image<A8>) (object) srcImage), srcBox);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(L8))
|
||||
{
|
||||
SetSubImage(texture, dstTl, ApplyL8Swizzle((Image<L8>) (object) srcImage), srcBox);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
@@ -14,6 +15,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using static Robust.Client.Utility.LiterallyJustMessageBox;
|
||||
@@ -46,6 +48,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Keep delegates around to prevent GC issues.
|
||||
private GLFWCallbacks.ErrorCallback _errorCallback = default!;
|
||||
private GLFWCallbacks.MonitorCallback _monitorCallback = default!;
|
||||
private GLFWCallbacks.CharCallback _charCallback = default!;
|
||||
private GLFWCallbacks.CursorPosCallback _cursorPosCallback = default!;
|
||||
private GLFWCallbacks.KeyCallback _keyCallback = default!;
|
||||
@@ -73,6 +76,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private Vector2 _lastMousePos;
|
||||
|
||||
// Can't use ClydeHandle because it's 64 bit.
|
||||
private int _nextWindowId = 1;
|
||||
private readonly Dictionary<int, MonitorReg> _monitors = new();
|
||||
|
||||
public event Action<TextEventArgs>? TextEntered;
|
||||
public event Action<MouseMoveEventArgs>? MouseMove;
|
||||
public event Action<KeyEventArgs>? KeyUp;
|
||||
public event Action<KeyEventArgs>? KeyDown;
|
||||
public event Action<MouseWheelEventArgs>? MouseWheel;
|
||||
public event Action<string>? CloseWindow;
|
||||
public event Action? OnWindowScaleChanged;
|
||||
|
||||
// NOTE: in engine we pretend the framebuffer size is the screen size..
|
||||
// For practical reasons like UI rendering.
|
||||
public override Vector2i ScreenSize => _framebufferSize;
|
||||
@@ -148,24 +163,71 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return false;
|
||||
}
|
||||
|
||||
InitMonitors();
|
||||
InitCursors();
|
||||
|
||||
return InitWindow();
|
||||
}
|
||||
|
||||
private void InitMonitors()
|
||||
{
|
||||
var monitors = GLFW.GetMonitorsRaw(out var count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
SetupMonitor(monitors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupMonitor(Monitor* monitor)
|
||||
{
|
||||
var handle = _nextWindowId++;
|
||||
|
||||
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null, "GLFW window already has user pointer??");
|
||||
|
||||
var name = GLFW.GetMonitorName(monitor);
|
||||
var videoMode = GLFW.GetVideoMode(monitor);
|
||||
var impl = new ClydeMonitorImpl(handle, name, (videoMode->Width, videoMode->Height), videoMode->RefreshRate);
|
||||
|
||||
GLFW.SetMonitorUserPointer(monitor, (void*) handle);
|
||||
_monitors[handle] = new MonitorReg
|
||||
{
|
||||
Id = handle,
|
||||
Impl = impl,
|
||||
Monitor = monitor
|
||||
};
|
||||
}
|
||||
|
||||
private void DestroyMonitor(Monitor* monitor)
|
||||
{
|
||||
var ptr = GLFW.GetMonitorUserPointer(monitor);
|
||||
|
||||
if (ptr == null)
|
||||
{
|
||||
var name = GLFW.GetMonitorName(monitor);
|
||||
Logger.WarningS("clyde.win", $"Monitor '{name}' had no user pointer set??");
|
||||
return;
|
||||
}
|
||||
|
||||
_monitors.Remove((int) ptr);
|
||||
GLFW.SetMonitorUserPointer(monitor, null);
|
||||
}
|
||||
|
||||
private bool InitWindow()
|
||||
{
|
||||
var width = _configurationManager.GetCVar(CVars.DisplayWidth);
|
||||
var height = _configurationManager.GetCVar(CVars.DisplayHeight);
|
||||
var width = ConfigurationManager.GetCVar(CVars.DisplayWidth);
|
||||
var height = ConfigurationManager.GetCVar(CVars.DisplayHeight);
|
||||
|
||||
Monitor* monitor = null;
|
||||
|
||||
if (WindowMode == WindowMode.Fullscreen)
|
||||
{
|
||||
monitor = GLFW.GetPrimaryMonitor();
|
||||
monitor = GLFW.GetMonitors()[1];
|
||||
var mode = GLFW.GetVideoMode(monitor);
|
||||
width = mode->Width;
|
||||
height = mode->Height;
|
||||
|
||||
GLFW.WindowHint(WindowHintInt.RefreshRate, mode->RefreshRate);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@@ -174,13 +236,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.WindowHint(WindowHintString.X11ClassName, "SS14");
|
||||
GLFW.WindowHint(WindowHintString.X11InstanceName, "SS14");
|
||||
|
||||
var renderer = (Renderer) _configurationManager.GetCVar<int>(CVars.DisplayRenderer);
|
||||
var renderer = (Renderer) ConfigurationManager.GetCVar<int>(CVars.DisplayRenderer);
|
||||
|
||||
Span<Renderer> renderers = (renderer == Renderer.Default) ? stackalloc Renderer[] {
|
||||
Renderer.OpenGL33,
|
||||
Renderer.OpenGL31,
|
||||
Renderer.OpenGLES2
|
||||
} : stackalloc Renderer[] {renderer};
|
||||
Span<Renderer> renderers = (renderer == Renderer.Default)
|
||||
? stackalloc Renderer[]
|
||||
{
|
||||
Renderer.OpenGL33,
|
||||
Renderer.OpenGL31,
|
||||
Renderer.OpenGLES2
|
||||
}
|
||||
: stackalloc Renderer[] {renderer};
|
||||
|
||||
foreach (Renderer r in renderers)
|
||||
{
|
||||
@@ -193,6 +258,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_isCore = renderer == Renderer.OpenGL33;
|
||||
break;
|
||||
}
|
||||
|
||||
// Window failed to init due to error.
|
||||
// Try not to treat the error code seriously.
|
||||
var code = GLFW.GetError(out string desc);
|
||||
@@ -206,9 +272,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var code = GLFW.GetError(out string desc);
|
||||
|
||||
var errorContent = "Failed to create the game window. " +
|
||||
"This probably means your GPU is too old to play the game. " +
|
||||
"That or update your graphic drivers\n" +
|
||||
$"The exact error is: [{code}]\n {desc}";
|
||||
"This probably means your GPU is too old to play the game. " +
|
||||
"That or update your graphic drivers\n" +
|
||||
$"The exact error is: [{code}]\n {desc}";
|
||||
|
||||
MessageBoxW(null,
|
||||
errorContent,
|
||||
@@ -301,6 +367,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
|
||||
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
|
||||
}
|
||||
|
||||
_glfwWindow = GLFW.CreateWindow(width, height, string.Empty, monitor, null);
|
||||
}
|
||||
}
|
||||
@@ -393,11 +460,30 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Logger.ErrorS("clyde.win.glfw", "GLFW Error: [{0}] {1}", code, description);
|
||||
}
|
||||
|
||||
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (state == ConnectedState.Connected)
|
||||
{
|
||||
SetupMonitor(monitor);
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyMonitor(monitor);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CatchCallbackException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGlfwChar(Window* window, uint codepoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
_gameController.TextEntered(new TextEventArgs(codepoint));
|
||||
TextEntered?.Invoke(new TextEventArgs(codepoint));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -413,8 +499,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var delta = newPos - _lastMousePos;
|
||||
_lastMousePos = newPos;
|
||||
|
||||
var ev = new MouseMoveEventArgs(delta, newPos);
|
||||
_gameController.MouseMove(ev);
|
||||
MouseMove?.Invoke(new MouseMoveEventArgs(delta, newPos));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -461,11 +546,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
switch (action)
|
||||
{
|
||||
case InputAction.Release:
|
||||
_gameController.KeyUp(ev);
|
||||
KeyUp?.Invoke(ev);
|
||||
break;
|
||||
case InputAction.Press:
|
||||
case InputAction.Repeat:
|
||||
_gameController.KeyDown(ev);
|
||||
KeyDown?.Invoke(ev);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(action), action, null);
|
||||
@@ -477,7 +562,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
try
|
||||
{
|
||||
var ev = new MouseWheelEventArgs(((float) offsetX, (float) offsetY), _lastMousePos);
|
||||
_gameController.MouseWheel(ev);
|
||||
MouseWheel?.Invoke(ev);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -489,7 +574,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
try
|
||||
{
|
||||
_gameController.Shutdown("Window closed");
|
||||
CloseWindow?.Invoke("Window closed");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -533,6 +618,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
try
|
||||
{
|
||||
_windowScale = (xScale, yScale);
|
||||
OnWindowScaleChanged?.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -568,6 +654,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void StoreCallbacks()
|
||||
{
|
||||
_errorCallback = OnGlfwError;
|
||||
_monitorCallback = OnGlfwMonitor;
|
||||
_charCallback = OnGlfwChar;
|
||||
_cursorPosCallback = OnGlfwCursorPos;
|
||||
_keyCallback = OnGlfwKey;
|
||||
@@ -590,6 +677,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.SetWindowTitle(_glfwWindow, title);
|
||||
}
|
||||
|
||||
public void SetWindowMonitor(IClydeMonitor monitor)
|
||||
{
|
||||
var monitorImpl = (ClydeMonitorImpl) monitor;
|
||||
var reg = _monitors[monitorImpl.Id];
|
||||
|
||||
GLFW.SetWindowMonitor(
|
||||
_glfwWindow,
|
||||
reg.Monitor,
|
||||
0, 0,
|
||||
monitorImpl.Size.X, monitorImpl.Size.Y,
|
||||
monitorImpl.RefreshRate);
|
||||
}
|
||||
|
||||
public void RequestWindowAttention()
|
||||
{
|
||||
GLFW.RequestWindowAttention(_glfwWindow);
|
||||
@@ -654,18 +754,40 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
GLFW.GetWindowPos(_glfwWindow, out var x, out var y);
|
||||
_prevWindowPos = (x, y);
|
||||
var monitor = GLFW.GetPrimaryMonitor();
|
||||
var monitor = MonitorForWindow(_glfwWindow);
|
||||
var mode = GLFW.GetVideoMode(monitor);
|
||||
|
||||
GLFW.SetWindowMonitor(_glfwWindow, GLFW.GetPrimaryMonitor(), 0, 0, mode->Width, mode->Height,
|
||||
GLFW.SetWindowMonitor(_glfwWindow, monitor, 0, 0, mode->Width, mode->Height,
|
||||
mode->RefreshRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X, _prevWindowSize.Y, 0);
|
||||
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X,
|
||||
_prevWindowSize.Y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// glfwGetWindowMonitor only works for fullscreen windows.
|
||||
// Picks the monitor with the top-left corner of the window.
|
||||
private Monitor* MonitorForWindow(Window* window)
|
||||
{
|
||||
GLFW.GetWindowPos(window, out var winPosX, out var winPosY);
|
||||
var monitors = GLFW.GetMonitorsRaw(out var count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var monitor = monitors[i];
|
||||
GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY);
|
||||
var videoMode = GLFW.GetVideoMode(monitor);
|
||||
|
||||
var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height);
|
||||
if (box.Contains(winPosX, winPosY))
|
||||
return monitor;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return GLFW.GetPrimaryMonitor();
|
||||
}
|
||||
|
||||
string IClipboardManager.GetText()
|
||||
{
|
||||
return GLFW.GetClipboardString(_glfwWindow);
|
||||
@@ -676,6 +798,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.SetClipboardString(_glfwWindow, text);
|
||||
}
|
||||
|
||||
public IEnumerable<IClydeMonitor> EnumerateMonitors()
|
||||
{
|
||||
return _monitors.Values.Select(c => c.Impl);
|
||||
}
|
||||
|
||||
// We can't let exceptions unwind into GLFW, as that can cause the CLR to crash.
|
||||
// And it probably messes up GLFW too.
|
||||
// So all the callbacks are passed to this method.
|
||||
@@ -689,5 +816,28 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
_glfwExceptionList.Add(e);
|
||||
}
|
||||
|
||||
private sealed class MonitorReg
|
||||
{
|
||||
public int Id;
|
||||
public Monitor* Monitor;
|
||||
public ClydeMonitorImpl Impl = default!;
|
||||
}
|
||||
|
||||
private sealed class ClydeMonitorImpl : IClydeMonitor
|
||||
{
|
||||
public ClydeMonitorImpl(int id, string name, Vector2i size, int refreshRate)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Size = size;
|
||||
RefreshRate = refreshRate;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public string Name { get; }
|
||||
public Vector2i Size { get; }
|
||||
public int RefreshRate { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_configurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
|
||||
if (!InitWindowing())
|
||||
{
|
||||
@@ -124,9 +124,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
protected override void ReadConfig()
|
||||
{
|
||||
base.ReadConfig();
|
||||
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
|
||||
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
|
||||
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
|
||||
_lightmapDivider = ConfigurationManager.GetCVar(CVars.DisplayLightMapDivider);
|
||||
_maxLightsPerScene = ConfigurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
|
||||
_enableSoftShadows = ConfigurationManager.GetCVar(CVars.DisplaySoftShadows);
|
||||
}
|
||||
|
||||
protected override void ReloadConfig()
|
||||
@@ -238,7 +238,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private (int major, int minor)? ParseGLOverrideVersion()
|
||||
{
|
||||
var overrideGLVersion = _configurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
|
||||
var overrideGLVersion = ConfigurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
|
||||
if (string.IsNullOrEmpty(overrideGLVersion))
|
||||
{
|
||||
return null;
|
||||
@@ -315,6 +315,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
|
||||
|
||||
CreateMainViewport();
|
||||
|
||||
screenBufferHandle = new GLHandle(GL.GenTexture());
|
||||
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
|
||||
ApplySampleParameters(TextureSampleParameters.Default);
|
||||
ScreenBufferTexture = GenTexture(screenBufferHandle, _framebufferSize, true, null, TexturePixelType.Rgba32);
|
||||
}
|
||||
|
||||
private void CreateMainViewport()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
@@ -37,6 +38,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public IClydeDebugInfo DebugInfo { get; } = new DummyDebugInfo();
|
||||
public IClydeDebugStats DebugStats { get; } = new DummyDebugStats();
|
||||
|
||||
public event Action<TextEventArgs>? TextEntered;
|
||||
public event Action<MouseMoveEventArgs>? MouseMove;
|
||||
public event Action<KeyEventArgs>? KeyUp;
|
||||
public event Action<KeyEventArgs>? KeyDown;
|
||||
public event Action<MouseWheelEventArgs>? MouseWheel;
|
||||
public event Action<string>? CloseWindow;
|
||||
|
||||
public Texture GetStockTexture(ClydeStockTexture stockTexture)
|
||||
{
|
||||
return new DummyTexture((1, 1));
|
||||
@@ -63,6 +71,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetWindowMonitor(IClydeMonitor monitor)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void RequestWindowAttention()
|
||||
{
|
||||
// Nada.
|
||||
@@ -86,6 +99,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action OnWindowScaleChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public void Render()
|
||||
{
|
||||
// Nada.
|
||||
@@ -101,7 +120,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
TextureLoadParameters? loadParams = null)
|
||||
{
|
||||
using (var image = Image.Load<Rgba32>(stream))
|
||||
@@ -110,7 +129,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
public Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
return new DummyTexture((image.Width, image.Height));
|
||||
@@ -156,6 +175,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return new Viewport();
|
||||
}
|
||||
|
||||
public IEnumerable<IClydeMonitor> EnumerateMonitors()
|
||||
{
|
||||
// TODO: Actually return something.
|
||||
yield break;
|
||||
}
|
||||
|
||||
public ClydeHandle LoadShader(ParsedShader shader, string? name = null)
|
||||
{
|
||||
return default;
|
||||
@@ -267,6 +292,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
|
||||
@@ -4,7 +4,6 @@ varying highp vec2 Pos;
|
||||
uniform sampler2D lightMap;
|
||||
uniform highp vec4 modulate;
|
||||
|
||||
#line 1000
|
||||
// [SHADER_HEADER_CODE]
|
||||
|
||||
void main()
|
||||
@@ -13,7 +12,6 @@ void main()
|
||||
|
||||
lowp vec4 COLOR;
|
||||
|
||||
#line 10000
|
||||
// [SHADER_CODE]
|
||||
|
||||
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;
|
||||
|
||||
@@ -89,9 +89,10 @@ uniform highp vec2 TEXTURE_PIXEL_SIZE;
|
||||
// -- srgb emulation --
|
||||
|
||||
#ifdef HAS_SRGB
|
||||
highp vec4 zTexture(highp vec2 uv)
|
||||
|
||||
highp vec4 zTextureSpec(sampler2D tex, highp vec2 uv)
|
||||
{
|
||||
return texture2D(TEXTURE, uv);
|
||||
return texture2D(tex, uv);
|
||||
}
|
||||
|
||||
highp vec4 zAdjustResult(highp vec4 col)
|
||||
@@ -101,9 +102,9 @@ highp vec4 zAdjustResult(highp vec4 col)
|
||||
#else
|
||||
uniform lowp vec2 SRGB_EMU_CONFIG;
|
||||
|
||||
highp vec4 zTexture(highp vec2 uv)
|
||||
highp vec4 zTextureSpec(sampler2D tex, highp vec2 uv)
|
||||
{
|
||||
highp vec4 col = texture2D(TEXTURE, uv);
|
||||
highp vec4 col = texture2D(tex, uv);
|
||||
if (SRGB_EMU_CONFIG.x > 0.5)
|
||||
{
|
||||
return zFromSrgb(col);
|
||||
@@ -121,5 +122,10 @@ highp vec4 zAdjustResult(highp vec4 col)
|
||||
}
|
||||
#endif
|
||||
|
||||
highp vec4 zTexture(highp vec2 uv)
|
||||
{
|
||||
return zTextureSpec(TEXTURE, uv);
|
||||
}
|
||||
|
||||
// -- Utilities End --
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
internal abstract class ClydeBase
|
||||
{
|
||||
[Dependency] protected readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] protected readonly IGameControllerInternal _gameController = default!;
|
||||
[Dependency] protected readonly IConfigurationManager ConfigurationManager = default!;
|
||||
|
||||
protected WindowMode WindowMode { get; private set; } = WindowMode.Windowed;
|
||||
protected bool VSync { get; private set; } = true;
|
||||
@@ -31,11 +30,11 @@ namespace Robust.Client.Graphics
|
||||
|
||||
public virtual bool Initialize()
|
||||
{
|
||||
_configurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
|
||||
_configurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
|
||||
_configurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
|
||||
_configurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
|
||||
_configurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -51,8 +50,8 @@ namespace Robust.Client.Graphics
|
||||
|
||||
protected virtual void ReadConfig()
|
||||
{
|
||||
WindowMode = (WindowMode) _configurationManager.GetCVar(CVars.DisplayWindowMode);
|
||||
VSync = _configurationManager.GetCVar(CVars.DisplayVSync);
|
||||
WindowMode = (WindowMode) ConfigurationManager.GetCVar(CVars.DisplayWindowMode);
|
||||
VSync = ConfigurationManager.GetCVar(CVars.DisplayVSync);
|
||||
}
|
||||
|
||||
private void _vSyncChanged(bool newValue)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -40,63 +41,48 @@ namespace Robust.Client.Graphics
|
||||
return GetLineHeight(scale) - GetHeight(scale);
|
||||
}
|
||||
|
||||
[Obsolete("Use GetAscent")] public int Ascent => GetAscent(1);
|
||||
[Obsolete("Use GetHeight")] public int Height => GetHeight(1);
|
||||
[Obsolete("Use GetDescent")] public int Descent => GetDescent(1);
|
||||
[Obsolete("Use GetLineHeight")] public int LineHeight => GetLineHeight(1);
|
||||
[Obsolete("Use GetLineSeparation")] public int LineSeparation => GetLineSeparation(1);
|
||||
|
||||
// Yes, I am aware that using char is bad.
|
||||
// At the same time the font system is nowhere close to rendering Unicode so...
|
||||
/// <summary>
|
||||
/// Draw a character at a certain baseline position on screen.
|
||||
/// </summary>
|
||||
/// <param name="handle">The drawing handle to draw to.</param>
|
||||
/// <param name="chr">
|
||||
/// The Unicode code point to draw. Yes I'm aware about UTF-16 being crap,
|
||||
/// do you think this system can draw anything except ASCII?
|
||||
/// </param>
|
||||
/// <param name="rune">The Unicode code point to draw.</param>
|
||||
/// <param name="baseline">The baseline from which to draw the character.</param>
|
||||
/// <param name="scale">DPI scale factor to render the font at.</param>
|
||||
/// <param name="color">The color of the character to draw.</param>
|
||||
/// <param name="fallback">If the character is not available, render "<22>" instead.</param>
|
||||
/// <returns>How much to advance the cursor to draw the next character.</returns>
|
||||
[Obsolete("Use DrawChar with scale support.")]
|
||||
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color)
|
||||
{
|
||||
return DrawChar(handle, chr, baseline, 1, color);
|
||||
}
|
||||
public abstract float DrawChar(
|
||||
DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale,
|
||||
Color color, bool fallback=true);
|
||||
|
||||
public abstract float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale,
|
||||
Color color);
|
||||
[Obsolete("Use Rune variant instead")]
|
||||
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
|
||||
{
|
||||
return DrawChar(handle, (Rune) chr, baseline, scale, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets metrics describing the dimensions and positioning of a single glyph in the font.
|
||||
/// </summary>
|
||||
/// <param name="chr">The character to fetch the glyph metrics for.</param>
|
||||
/// <param name="rune">The unicode codepoint to fetch the glyph metrics for.</param>
|
||||
/// <param name="scale">DPI scale factor to render the font at.</param>
|
||||
/// <param name="fallback">
|
||||
/// If the character is not available, return data for "<22>" instead.
|
||||
/// This can still fail if the font does not define <20> itself.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>null</c> if this font does not have a glyph for the specified character,
|
||||
/// otherwise the metrics you asked for.
|
||||
/// </returns>
|
||||
/// <seealso cref="TryGetCharMetrics"/>
|
||||
[Obsolete("Use GetCharMetrics with scale support.")]
|
||||
public CharMetrics? GetCharMetrics(char chr)
|
||||
{
|
||||
return GetCharMetrics(chr, 1);
|
||||
}
|
||||
|
||||
public abstract CharMetrics? GetCharMetrics(char chr, float scale);
|
||||
public abstract CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true);
|
||||
|
||||
/// <summary>
|
||||
/// Try-pattern version of <see cref="GetCharMetrics"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Use TryGetCharMetrics with scale support.")]
|
||||
public bool TryGetCharMetrics(char chr, out CharMetrics metrics)
|
||||
public bool TryGetCharMetrics(Rune rune, float scale, out CharMetrics metrics, bool fallback=true)
|
||||
{
|
||||
return TryGetCharMetrics(chr, 1, out metrics);
|
||||
}
|
||||
|
||||
public bool TryGetCharMetrics(char chr, float scale, out CharMetrics metrics)
|
||||
{
|
||||
var maybe = GetCharMetrics(chr, scale);
|
||||
var maybe = GetCharMetrics(rune, scale);
|
||||
if (maybe.HasValue)
|
||||
{
|
||||
metrics = maybe.Value;
|
||||
@@ -128,15 +114,23 @@ namespace Robust.Client.Graphics
|
||||
public override int GetDescent(float scale) => Handle.GetDescent(scale);
|
||||
public override int GetLineHeight(float scale) => Handle.GetLineHeight(scale);
|
||||
|
||||
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
|
||||
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
|
||||
{
|
||||
var metrics = Handle.GetCharMetrics(chr, scale);
|
||||
var metrics = Handle.GetCharMetrics(rune, scale);
|
||||
if (!metrics.HasValue)
|
||||
{
|
||||
return 0;
|
||||
if (fallback && !Rune.IsWhiteSpace(rune))
|
||||
{
|
||||
rune = new Rune('<27>');
|
||||
metrics = Handle.GetCharMetrics(rune, scale);
|
||||
if (!metrics.HasValue)
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
var texture = Handle.GetCharTexture(chr, scale);
|
||||
var texture = Handle.GetCharTexture(rune, scale);
|
||||
if (texture == null)
|
||||
{
|
||||
return metrics.Value.Advance;
|
||||
@@ -147,9 +141,12 @@ namespace Robust.Client.Graphics
|
||||
return metrics.Value.Advance;
|
||||
}
|
||||
|
||||
public override CharMetrics? GetCharMetrics(char chr, float scale)
|
||||
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
|
||||
{
|
||||
return Handle.GetCharMetrics(chr, scale);
|
||||
var metrics = Handle.GetCharMetrics(rune, scale);
|
||||
if (metrics == null && !Rune.IsWhiteSpace(rune) && fallback)
|
||||
return Handle.GetCharMetrics(new Rune('<27>'), scale);
|
||||
return metrics;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +157,13 @@ namespace Robust.Client.Graphics
|
||||
public override int GetDescent(float scale) => default;
|
||||
public override int GetLineHeight(float scale) => default;
|
||||
|
||||
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
|
||||
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
|
||||
{
|
||||
// Nada, it's a dummy after all.
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override CharMetrics? GetCharMetrics(char chr, float scale)
|
||||
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
|
||||
{
|
||||
// Nada, it's a dummy after all.
|
||||
return null;
|
||||
|
||||
@@ -4,9 +4,6 @@ using System.IO;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SharpFont;
|
||||
@@ -20,18 +17,18 @@ namespace Robust.Client.Graphics
|
||||
private const int SheetWidth = 256;
|
||||
private const int SheetHeight = 256;
|
||||
|
||||
[Dependency] private readonly IConfigurationManager _configuration = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
private readonly IClyde _clyde;
|
||||
|
||||
private uint BaseFontDPI;
|
||||
private uint _baseFontDpi = 96;
|
||||
|
||||
private readonly Library _library;
|
||||
|
||||
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
|
||||
new();
|
||||
|
||||
public FontManager()
|
||||
public FontManager(IClyde clyde)
|
||||
{
|
||||
_clyde = clyde;
|
||||
_library = new Library();
|
||||
}
|
||||
|
||||
@@ -42,9 +39,9 @@ namespace Robust.Client.Graphics
|
||||
return handle;
|
||||
}
|
||||
|
||||
void IFontManagerInternal.Initialize()
|
||||
void IFontManagerInternal.SetFontDpi(uint fontDpi)
|
||||
{
|
||||
BaseFontDPI = (uint) _configuration.GetCVar(CVars.DisplayFontDpi);
|
||||
_baseFontDpi = fontDpi;
|
||||
}
|
||||
|
||||
public IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size)
|
||||
@@ -64,7 +61,7 @@ namespace Robust.Client.Graphics
|
||||
private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale)
|
||||
{
|
||||
var ftFace = instance.FaceHandle.Face;
|
||||
ftFace.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
|
||||
ftFace.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
|
||||
|
||||
var ascent = ftFace.Size.Metrics.Ascender.ToInt32();
|
||||
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
|
||||
@@ -83,7 +80,7 @@ namespace Robust.Client.Graphics
|
||||
return;
|
||||
|
||||
var face = instance.FaceHandle.Face;
|
||||
face.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
|
||||
face.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
|
||||
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
|
||||
face.Glyph.RenderGlyph(RenderMode.Normal);
|
||||
|
||||
@@ -189,7 +186,7 @@ namespace Robust.Client.Graphics
|
||||
OwnedTexture GenSheet()
|
||||
{
|
||||
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
|
||||
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}-sheet{scaled.AtlasTextures.Count}");
|
||||
$"font-{face.FamilyName}-{instance.Size}-{(uint) (_baseFontDpi * scale)}-sheet{scaled.AtlasTextures.Count}");
|
||||
scaled.AtlasTextures.Add(sheet);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -21,6 +22,7 @@ namespace Robust.Client.Graphics
|
||||
Vector2 DefaultWindowScale { get; }
|
||||
|
||||
void SetWindowTitle(string title);
|
||||
void SetWindowMonitor(IClydeMonitor monitor);
|
||||
|
||||
/// <summary>
|
||||
/// This is the magic method to make the game window ping you in the task bar.
|
||||
@@ -31,10 +33,12 @@ namespace Robust.Client.Graphics
|
||||
|
||||
event Action<WindowFocusedEventArgs> OnWindowFocused;
|
||||
|
||||
Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
event Action OnWindowScaleChanged;
|
||||
|
||||
OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
TextureLoadParameters? loadParams = null);
|
||||
|
||||
Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>;
|
||||
|
||||
/// <summary>
|
||||
@@ -104,6 +108,8 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
|
||||
IClydeViewport CreateViewport(Vector2i size, string? name = null);
|
||||
|
||||
IEnumerable<IClydeMonitor> EnumerateMonitors();
|
||||
}
|
||||
|
||||
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.
|
||||
|
||||
@@ -20,5 +20,6 @@ namespace Robust.Client.Graphics
|
||||
void SetVolume(float decibels);
|
||||
void SetOcclusion(float blocks);
|
||||
void SetPlaybackPosition(float seconds);
|
||||
void SetVelocity(Vector2 velocity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -16,6 +17,13 @@ namespace Robust.Client.Graphics
|
||||
bool Initialize();
|
||||
void Ready();
|
||||
|
||||
event Action<TextEventArgs> TextEntered;
|
||||
event Action<MouseMoveEventArgs> MouseMove;
|
||||
event Action<KeyEventArgs> KeyUp;
|
||||
event Action<KeyEventArgs> KeyDown;
|
||||
event Action<MouseWheelEventArgs> MouseWheel;
|
||||
event Action<string> CloseWindow;
|
||||
|
||||
ClydeHandle LoadShader(ParsedShader shader, string? name = null);
|
||||
|
||||
void ReloadShader(ClydeHandle handle, ParsedShader newShader);
|
||||
|
||||
18
Robust.Client/Graphics/IClydeMonitor.cs
Normal file
18
Robust.Client/Graphics/IClydeMonitor.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a connected monitor on the user's system.
|
||||
/// </summary>
|
||||
public interface IClydeMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// This ID is not consistent between startups of the game.
|
||||
/// </summary>
|
||||
int Id { get; }
|
||||
string Name { get; }
|
||||
Vector2i Size { get; }
|
||||
int RefreshRate { get; }
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
IFontFaceHandle Load(Stream stream);
|
||||
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
|
||||
void Initialize();
|
||||
void SetFontDpi(uint fontDpi);
|
||||
}
|
||||
|
||||
internal interface IFontFaceHandle
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
|
||||
[PublicAPI]
|
||||
public interface IOverlayManager
|
||||
{
|
||||
void AddOverlay(Overlay overlay);
|
||||
void RemoveOverlay(string id);
|
||||
bool HasOverlay(string id);
|
||||
bool AddOverlay(Overlay overlay);
|
||||
|
||||
Overlay GetOverlay(string id);
|
||||
T GetOverlay<T>(string id) where T : Overlay;
|
||||
bool RemoveOverlay(Overlay overlay);
|
||||
bool RemoveOverlay(Type overlayClass);
|
||||
bool RemoveOverlay<T>() where T : Overlay;
|
||||
|
||||
bool TryGetOverlay(string id, [NotNullWhen(true)] out Overlay? overlay);
|
||||
bool TryGetOverlay<T>(string id, [NotNullWhen(true)] out T? overlay) where T : Overlay;
|
||||
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
|
||||
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
|
||||
|
||||
Overlay GetOverlay(Type overlayClass);
|
||||
T GetOverlay<T>() where T : Overlay;
|
||||
|
||||
bool HasOverlay(Type overlayClass);
|
||||
bool HasOverlay<T>() where T : Overlay;
|
||||
|
||||
IEnumerable<Overlay> AllOverlays { get; }
|
||||
}
|
||||
|
||||
@@ -1,65 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.IoC;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Enums;
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
/// <summary>
|
||||
/// An overlay is used for fullscreen drawing in the game, for example parallax.
|
||||
/// An overlay is used for fullscreen drawing in the game. This can range from drawing parallax to a full screen shader.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public abstract class Overlay
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The ID of this overlay. This is used to identify it inside the <see cref="IOverlayManager"/>.
|
||||
/// Determines when this overlay is drawn in the rendering queue.
|
||||
/// </summary>
|
||||
public string ID { get; }
|
||||
|
||||
public virtual bool AlwaysDirty => false;
|
||||
public bool IsDirty => AlwaysDirty || _isDirty;
|
||||
public bool Drawing { get; private set; }
|
||||
|
||||
public virtual OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, <see cref="ScreenTexture"/> will be set to the current frame (at the moment before the overlay is rendered). This can be costly to performance, but
|
||||
/// some shaders will require it as a passed in uniform to operate.
|
||||
/// </summary>
|
||||
public virtual bool RequestScreenTexture => false;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="RequestScreenTexture"> is true, then this will be set to the texture corresponding to the current frame. If false, it will always be null.
|
||||
/// </summary>
|
||||
public Texture? ScreenTexture = null;
|
||||
|
||||
/// <summary>
|
||||
/// Overlays on the same OverlaySpace will be drawn from lowest ZIndex to highest ZIndex. As an example, ZIndex -1 will be drawn before ZIndex 2.
|
||||
/// This value is 0 by default. Overlays with same ZIndex will be drawn in an random order.
|
||||
/// </summary>
|
||||
public int? ZIndex { get; set; }
|
||||
|
||||
protected IOverlayManager OverlayManager { get; }
|
||||
|
||||
public int? ZIndex { get; set; }
|
||||
private bool Disposed = false;
|
||||
|
||||
public virtual bool SubHandlesUseMainShader { get; } = true;
|
||||
|
||||
private bool _isDirty = true;
|
||||
|
||||
private readonly List<DrawingHandleBase> TempHandles = new();
|
||||
|
||||
private bool Disposed;
|
||||
|
||||
protected Overlay(string id)
|
||||
public Overlay()
|
||||
{
|
||||
OverlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
ID = id;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispose(true);
|
||||
Disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~Overlay()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
/// <summary>
|
||||
/// If this function returns true, the target framebuffer will be wiped before applying this overlay to it.
|
||||
/// </summary>
|
||||
public virtual bool OverwriteTargetFrameBuffer(){
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,50 +58,34 @@ namespace Robust.Client.Graphics
|
||||
/// <param name="currentSpace">Current space that is being drawn. This function is called for every space that was set up in initialization.</param>
|
||||
protected abstract void Draw(DrawingHandleBase handle, OverlaySpace currentSpace);
|
||||
|
||||
public void Dirty()
|
||||
{
|
||||
_isDirty = true;
|
||||
protected internal virtual void FrameUpdate(FrameEventArgs args) { }
|
||||
|
||||
~Overlay() {
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (Disposed)
|
||||
return;
|
||||
else
|
||||
DisposeBehavior();
|
||||
}
|
||||
|
||||
protected virtual void DisposeBehavior(){
|
||||
Disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected internal virtual void FrameUpdate(FrameEventArgs args) { }
|
||||
|
||||
internal void ClydeRender(IRenderHandle renderHandle, OverlaySpace currentSpace)
|
||||
{
|
||||
DrawingHandleBase handle;
|
||||
if (currentSpace == OverlaySpace.WorldSpace)
|
||||
handle = renderHandle.DrawingHandleWorld;
|
||||
else
|
||||
if (currentSpace == OverlaySpace.ScreenSpace || currentSpace == OverlaySpace.ScreenSpaceBelowWorld)
|
||||
handle = renderHandle.DrawingHandleScreen;
|
||||
else
|
||||
handle = renderHandle.DrawingHandleWorld;
|
||||
|
||||
Draw(handle, currentSpace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines in which canvas layers an overlay gets drawn.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum OverlaySpace : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for matching bit flags.
|
||||
/// </summary>
|
||||
None = 0b0000,
|
||||
|
||||
/// <summary>
|
||||
/// This overlay will be drawn in the UI root, thus being in screen space.
|
||||
/// </summary>
|
||||
ScreenSpace = 0b0001,
|
||||
|
||||
/// <summary>
|
||||
/// This overlay will be drawn in the world root, thus being in world space.
|
||||
/// </summary>
|
||||
WorldSpace = 0b0010,
|
||||
|
||||
/// <summary>
|
||||
/// Drawn in screen coordinates, but behind the world.
|
||||
/// </summary>
|
||||
ScreenSpaceBelowWorld = 0b0100,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
internal class OverlayManager : IOverlayManagerInternal
|
||||
{
|
||||
private readonly Dictionary<string, Overlay> _overlays = new();
|
||||
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
|
||||
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
|
||||
|
||||
public void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
@@ -17,59 +19,80 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOverlay(Overlay overlay)
|
||||
public bool AddOverlay(Overlay overlay)
|
||||
{
|
||||
if (_overlays.ContainsKey(overlay.ID))
|
||||
{
|
||||
throw new InvalidOperationException($"We already have an overlay with ID '{overlay.ID}'");
|
||||
if(_overlays.ContainsKey(overlay.GetType()))
|
||||
return false;
|
||||
_overlays.Add(overlay.GetType(), overlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public bool RemoveOverlay(Type overlayClass)
|
||||
{
|
||||
if(!overlayClass.IsSubclassOf(typeof(Overlay))){
|
||||
Logger.Error("RemoveOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
|
||||
return false;
|
||||
}
|
||||
|
||||
_overlays.Add(overlay.ID, overlay);
|
||||
return _overlays.Remove(overlayClass);
|
||||
}
|
||||
|
||||
public Overlay GetOverlay(string id)
|
||||
{
|
||||
return _overlays[id];
|
||||
public bool RemoveOverlay<T>() where T : Overlay{
|
||||
return RemoveOverlay(typeof(T));
|
||||
}
|
||||
|
||||
public T GetOverlay<T>(string id) where T : Overlay
|
||||
{
|
||||
return (T) GetOverlay(id);
|
||||
public bool RemoveOverlay(Overlay overlay) {
|
||||
return _overlays.Remove(overlay.GetType());
|
||||
}
|
||||
|
||||
public bool HasOverlay(string id)
|
||||
{
|
||||
return _overlays.ContainsKey(id);
|
||||
}
|
||||
|
||||
public void RemoveOverlay(string id)
|
||||
|
||||
|
||||
|
||||
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
|
||||
{
|
||||
if (!_overlays.TryGetValue(id, out var overlay))
|
||||
{
|
||||
return;
|
||||
overlay = null;
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay))){
|
||||
Logger.Error("TryGetOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
|
||||
return false;
|
||||
}
|
||||
|
||||
overlay.Dispose();
|
||||
_overlays.Remove(id);
|
||||
return _overlays.TryGetValue(overlayClass, out overlay);
|
||||
}
|
||||
|
||||
public bool TryGetOverlay(string id, [NotNullWhen(true)] out Overlay? overlay)
|
||||
{
|
||||
return _overlays.TryGetValue(id, out overlay);
|
||||
}
|
||||
|
||||
public bool TryGetOverlay<T>(string id, [NotNullWhen(true)] out T? overlay) where T : Overlay
|
||||
{
|
||||
if (_overlays.TryGetValue(id, out var value))
|
||||
{
|
||||
overlay = (T) value;
|
||||
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay {
|
||||
overlay = null;
|
||||
if(_overlays.TryGetValue(typeof(T), out Overlay? toReturn)){
|
||||
overlay = (T)toReturn;
|
||||
return true;
|
||||
}
|
||||
|
||||
overlay = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
|
||||
|
||||
|
||||
|
||||
public Overlay GetOverlay(Type overlayClass) {
|
||||
return _overlays[overlayClass];
|
||||
}
|
||||
|
||||
public T GetOverlay<T>() where T : Overlay {
|
||||
return (T)_overlays[typeof(T)];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public bool HasOverlay(Type overlayClass) {
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
Logger.Error("HasOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
|
||||
return _overlays.ContainsKey(overlayClass);
|
||||
}
|
||||
|
||||
public bool HasOverlay<T>() where T : Overlay {
|
||||
return _overlays.ContainsKey(typeof(T));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ namespace Robust.Client.Graphics
|
||||
public sealed class State : IRsiStateLike
|
||||
{
|
||||
// List of delays for the frame to reach the next frame.
|
||||
private readonly float[] Delays;
|
||||
public readonly float[] Delays;
|
||||
|
||||
// 2D array for the texture to use for each animation frame at each direction.
|
||||
private readonly Texture[][] Icons;
|
||||
public readonly Texture[][] Icons;
|
||||
|
||||
internal State(Vector2i size, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons)
|
||||
{
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "RSI Image Format Validation Schema V1",
|
||||
"description": "Robust Station Image",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"size": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {"type": "integer", "minimum": 1},
|
||||
"y": {"type": "integer", "minimum": 1}
|
||||
},
|
||||
"required": ["x","y"]
|
||||
},
|
||||
"directions": {
|
||||
"type": "integer",
|
||||
"enum": [1,4,8]
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"flags": {"type": "object"}, //To be de-serialized as a Dictionary
|
||||
"directions": {"$ref": "#/definitions/directions"},
|
||||
"delays": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {"type": "number", "minimum": 0, "exclusiveMinimum": true} //number == float
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"] //'delays' is marked as optional in the spec
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"version": {"type": "integer", "minimum": 1, "maximum": 1},
|
||||
"size": {"$ref": "#/definitions/size"},
|
||||
"states": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/definitions/state"},
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"required": ["version","size","states"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Input
|
||||
@@ -58,6 +59,7 @@ namespace Robust.Client.Input
|
||||
}
|
||||
|
||||
public uint CodePoint { get; }
|
||||
public Rune AsRune => new Rune(CodePoint);
|
||||
}
|
||||
|
||||
public class KeyEventArgs : ModifierInputEventArgs
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -67,7 +68,7 @@ namespace Robust.Client.Physics
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(nameof(PhysicsIslandOverlay));
|
||||
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(typeof(PhysicsIslandOverlay));
|
||||
}
|
||||
|
||||
private void HandleIslandSolveMessage(IslandSolveMessage message)
|
||||
@@ -94,7 +95,7 @@ namespace Robust.Client.Physics
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public PhysicsIslandOverlay() : base(nameof(PhysicsIslandOverlay))
|
||||
public PhysicsIslandOverlay()
|
||||
{
|
||||
_islandSystem = EntitySystem.Get<DebugPhysicsIslandSystem>();
|
||||
_eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
|
||||
namespace Robust.Client.Placement
|
||||
{
|
||||
public partial class PlacementManager
|
||||
@@ -7,10 +9,9 @@ namespace Robust.Client.Placement
|
||||
internal class PlacementOverlay : Overlay
|
||||
{
|
||||
private readonly PlacementManager _manager;
|
||||
public override bool AlwaysDirty => true;
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public PlacementOverlay(PlacementManager manager) : base("placement")
|
||||
public PlacementOverlay(PlacementManager manager)
|
||||
{
|
||||
_manager = manager;
|
||||
ZIndex = 100;
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Robust.Client.Player
|
||||
|
||||
void Initialize();
|
||||
void Startup(INetChannel channel);
|
||||
void Update(float frameTime);
|
||||
void Shutdown();
|
||||
|
||||
void ApplyPlayerStates(IEnumerable<PlayerState>? list);
|
||||
|
||||
@@ -95,12 +95,6 @@ namespace Robust.Client.Player
|
||||
_network.ClientSendMessage(msgList);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
// Uh, nothing anymore I guess.
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Shutdown()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
|
||||
[assembly: InternalsVisibleTo("Robust.Lite")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
|
||||
#if NET5_0
|
||||
[module: SkipLocalsInit]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -53,20 +54,17 @@ namespace Robust.Client.Prototypes
|
||||
private void ReloadPrototypeQueue()
|
||||
{
|
||||
#if !FULL_RELEASE
|
||||
var then = DateTime.Now;
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var msg = NetManager.CreateNetMessage<MsgReloadPrototypes>();
|
||||
msg.Paths = _reloadQueue.ToArray();
|
||||
NetManager.ClientSendMessage(msg);
|
||||
|
||||
foreach (var path in _reloadQueue)
|
||||
{
|
||||
ReloadPrototypes(path);
|
||||
}
|
||||
ReloadPrototypes(_reloadQueue);
|
||||
|
||||
_reloadQueue.Clear();
|
||||
|
||||
Logger.Info($"Reloaded prototypes in {(int) (DateTime.Now - then).TotalMilliseconds} ms");
|
||||
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace Robust.Client.ResourceManagement
|
||||
void RsiLoaded(RsiLoadedEventArgs eventArgs);
|
||||
|
||||
void MountLoaderApi(IFileApi api, string apiPrefix, ResourcePath? prefix=null);
|
||||
void PreloadTextures();
|
||||
}
|
||||
}
|
||||
|
||||
196
Robust.Client/ResourceManagement/ResourceCache.Preload.cs
Normal file
196
Robust.Client/ResourceManagement/ResourceCache.Preload.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
internal partial class ResourceCache
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
public void PreloadTextures()
|
||||
{
|
||||
var sawmill = _logManager.GetSawmill("res.preload");
|
||||
|
||||
if (!_configurationManager.GetCVar(CVars.TexturePreloadingEnabled))
|
||||
{
|
||||
sawmill.Debug($"Skipping texture preloading due to CVar value.");
|
||||
return;
|
||||
}
|
||||
|
||||
PreloadTextures(sawmill);
|
||||
PreloadRsis(sawmill);
|
||||
}
|
||||
|
||||
private void PreloadTextures(ISawmill sawmill)
|
||||
{
|
||||
sawmill.Debug("Preloading textures...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resList = GetTypeDict<TextureResource>();
|
||||
|
||||
var texList = ContentFindFiles("/Textures/")
|
||||
// Skip PNG files inside RSIs.
|
||||
.Where(p => p.Extension == "png" && !p.ToString().Contains(".rsi/") && !resList.ContainsKey(p))
|
||||
.Select(p => new TextureResource.LoadStepData {Path = p})
|
||||
.ToArray();
|
||||
|
||||
Parallel.ForEach(texList, data =>
|
||||
{
|
||||
try
|
||||
{
|
||||
TextureResource.LoadPreTexture(this, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Mark failed loads as bad and skip them in the next few stages.
|
||||
// Avoids any silly array resizing or similar.
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
TextureResource.LoadTexture(_clyde, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
}
|
||||
}
|
||||
|
||||
var errors = 0;
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
{
|
||||
errors += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var texResource = new TextureResource();
|
||||
texResource.LoadFinish(this, data);
|
||||
resList[data.Path] = texResource;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
|
||||
texList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
}
|
||||
|
||||
private void PreloadRsis(ISawmill sawmill)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resList = GetTypeDict<RSIResource>();
|
||||
|
||||
var rsiList = ContentFindFiles("/Textures/")
|
||||
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
|
||||
.Select(c => c.Directory)
|
||||
.Where(p => !resList.ContainsKey(p))
|
||||
.Select(p => new RSIResource.LoadStepData {Path = p})
|
||||
.ToArray();
|
||||
|
||||
Parallel.ForEach(rsiList, data =>
|
||||
{
|
||||
try
|
||||
{
|
||||
RSIResource.LoadPreTexture(this, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Mark failed loads as bad and skip them in the next few stages.
|
||||
// Avoids any silly array resizing or similar.
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var data in rsiList)
|
||||
{
|
||||
if (data.Bad)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
RSIResource.LoadTexture(_clyde, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
}
|
||||
}
|
||||
|
||||
Parallel.ForEach(rsiList, data =>
|
||||
{
|
||||
if (data.Bad)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
RSIResource.LoadPostTexture(data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data.Bad = true;
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
}
|
||||
});
|
||||
|
||||
var errors = 0;
|
||||
foreach (var data in rsiList)
|
||||
{
|
||||
if (data.Bad)
|
||||
{
|
||||
errors += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var rsiRes = new RSIResource();
|
||||
rsiRes.LoadFinish(this, data);
|
||||
resList[data.Path] = rsiRes;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} RSIs ({CountErrored} errored) in {LoadTime}",
|
||||
rsiList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
#if DEBUG
|
||||
using NJsonSchema;
|
||||
#endif
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
@@ -24,6 +20,14 @@ namespace Robust.Client.ResourceManagement
|
||||
/// </summary>
|
||||
public sealed class RSIResource : BaseResource
|
||||
{
|
||||
private static readonly float[] OneArray = {1};
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions =
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
public RSI RSI { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,64 +42,59 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
public override void Load(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
var manifestPath = path / "meta.json";
|
||||
string manifestContents;
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
|
||||
using (var manifestFile = cache.ContentFileRead(manifestPath))
|
||||
using (var reader = new StreamReader(manifestFile))
|
||||
{
|
||||
manifestContents = reader.ReadToEnd();
|
||||
}
|
||||
var loadStepData = new LoadStepData {Path = path};
|
||||
LoadPreTexture(cache, loadStepData);
|
||||
|
||||
#if DEBUG
|
||||
if (RSISchema != null)
|
||||
{
|
||||
var errors = RSISchema.Validate(manifestContents);
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
Logger.Error($"Unable to load RSI from '{path}', {errors.Count} errors:");
|
||||
// Load atlas.
|
||||
LoadTexture(clyde, loadStepData);
|
||||
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Logger.Error("{0}", error.ToString());
|
||||
}
|
||||
LoadPostTexture(loadStepData);
|
||||
LoadFinish(cache, loadStepData);
|
||||
|
||||
throw new RSILoadException($"{errors.Count} errors while loading RSI. See console.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
loadStepData.AtlasSheet.Dispose();
|
||||
}
|
||||
|
||||
// Ok schema validated just fine.
|
||||
var manifestJson = JObject.Parse(manifestContents);
|
||||
internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)
|
||||
{
|
||||
loadStepData.AtlasTexture = clyde.LoadTextureFromImage(
|
||||
loadStepData.AtlasSheet,
|
||||
loadStepData.Path.ToString());
|
||||
}
|
||||
|
||||
var toAtlas = new List<(Image<Rgba32> src, Texture[][] output, int[][] indices, Vector2i[][] offsets, int totalFrameCount)>();
|
||||
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
|
||||
{
|
||||
var metadata = LoadRsiMetadata(cache, data.Path);
|
||||
|
||||
var metaData = ParseMetaData(manifestJson);
|
||||
var frameSize = metaData.Size;
|
||||
var rsi = new RSI(frameSize, path);
|
||||
var stateCount = metadata.States.Length;
|
||||
var toAtlas = new StateReg[stateCount];
|
||||
|
||||
var callbackOffsets = new Dictionary<RSI.StateId, Vector2i[][]>();
|
||||
var frameSize = metadata.Size;
|
||||
var rsi = new RSI(frameSize, data.Path);
|
||||
|
||||
var callbackOffsets = new Dictionary<RSI.StateId, Vector2i[][]>(stateCount);
|
||||
|
||||
// Do every state.
|
||||
foreach (var stateObject in metaData.States)
|
||||
for (var index = 0; index < metadata.States.Length; index++)
|
||||
{
|
||||
// Load image from disk.
|
||||
var texPath = path / (stateObject.StateId + ".png");
|
||||
var stream = cache.ContentFileRead(texPath);
|
||||
Image<Rgba32> image;
|
||||
using (stream)
|
||||
{
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
var sheetSize = new Vector2i(image.Width, image.Height);
|
||||
ref var reg = ref toAtlas[index];
|
||||
|
||||
if (sheetSize.X % frameSize.X != 0 || sheetSize.Y % frameSize.Y != 0)
|
||||
var stateObject = metadata.States[index];
|
||||
// Load image from disk.
|
||||
var texPath = data.Path / (stateObject.StateId + ".png");
|
||||
using (var stream = cache.ContentFileRead(texPath))
|
||||
{
|
||||
reg.Src = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
|
||||
if (reg.Src.Width % frameSize.X != 0 || reg.Src.Height % frameSize.Y != 0)
|
||||
{
|
||||
throw new RSILoadException("State image size is not a multiple of the icon size.");
|
||||
}
|
||||
|
||||
// Load all frames into a list so we can operate on it more sanely.
|
||||
var frameCount = stateObject.Delays.Sum(delayList => delayList.Length);
|
||||
reg.TotalFrameCount = stateObject.Delays.Sum(delayList => delayList.Length);
|
||||
|
||||
var (foldedDelays, foldedIndices) = FoldDelays(stateObject.Delays);
|
||||
|
||||
@@ -108,29 +107,34 @@ namespace Robust.Client.ResourceManagement
|
||||
callbackOffset[i] = new Vector2i[foldedIndices[0].Length];
|
||||
}
|
||||
|
||||
var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays, textures);
|
||||
reg.Output = textures;
|
||||
reg.Indices = foldedIndices;
|
||||
reg.Offsets = callbackOffset;
|
||||
|
||||
var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays,
|
||||
textures);
|
||||
rsi.AddState(state);
|
||||
|
||||
toAtlas.Add((image, textures, foldedIndices, callbackOffset, frameCount));
|
||||
callbackOffsets[stateObject.StateId] = callbackOffset;
|
||||
}
|
||||
|
||||
// Poorly hacked in texture atlas support here.
|
||||
var totalFrameCount = toAtlas.Sum(p => p.totalFrameCount);
|
||||
var totalFrameCount = toAtlas.Sum(p => p.TotalFrameCount);
|
||||
|
||||
// Generate atlas.
|
||||
var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount));
|
||||
var dimensionY = (int) MathF.Ceiling((float) totalFrameCount / dimensionX);
|
||||
|
||||
using var sheet = new Image<Rgba32>(dimensionX * frameSize.X, dimensionY * frameSize.Y);
|
||||
var sheet = new Image<Rgba32>(dimensionX * frameSize.X, dimensionY * frameSize.Y);
|
||||
|
||||
var sheetIndex = 0;
|
||||
foreach (var (src, _, _, _, frameCount) in toAtlas)
|
||||
for (var index = 0; index < toAtlas.Length; index++)
|
||||
{
|
||||
ref var reg = ref toAtlas[index];
|
||||
// Blit all the frames over.
|
||||
for (var i = 0; i < frameCount; i++)
|
||||
for (var i = 0; i < reg.TotalFrameCount; i++)
|
||||
{
|
||||
var srcWidth = (src.Width / frameSize.X);
|
||||
var srcWidth = (reg.Src.Width / frameSize.X);
|
||||
var srcColumn = i % srcWidth;
|
||||
var srcRow = i / srcWidth;
|
||||
var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y);
|
||||
@@ -141,30 +145,49 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
var srcBox = UIBox2i.FromDimensions(srcPos, frameSize);
|
||||
|
||||
src.Blit(srcBox, sheet, sheetPos);
|
||||
reg.Src.Blit(srcBox, sheet, sheetPos);
|
||||
}
|
||||
|
||||
sheetIndex += frameCount;
|
||||
sheetIndex += reg.TotalFrameCount;
|
||||
}
|
||||
|
||||
// Load atlas.
|
||||
var texture = Texture.LoadFromImage(sheet, path.ToString());
|
||||
for (var i = 0; i < toAtlas.Length; i++)
|
||||
{
|
||||
ref var reg = ref toAtlas[i];
|
||||
reg.Src.Dispose();
|
||||
}
|
||||
|
||||
data.Rsi = rsi;
|
||||
data.AtlasSheet = sheet;
|
||||
data.AtlasList = toAtlas;
|
||||
data.FrameSize = frameSize;
|
||||
data.DimX = dimensionX;
|
||||
data.CallbackOffsets = callbackOffsets;
|
||||
}
|
||||
|
||||
internal static void LoadPostTexture(LoadStepData data)
|
||||
{
|
||||
var dimX = data.DimX;
|
||||
var toAtlas = data.AtlasList;
|
||||
var frameSize = data.FrameSize;
|
||||
var texture = data.AtlasTexture;
|
||||
|
||||
var sheetOffset = 0;
|
||||
foreach (var (_, output, indices, offsets, frameCount) in toAtlas)
|
||||
for (var toAtlasIndex = 0; toAtlasIndex < toAtlas.Length; toAtlasIndex++)
|
||||
{
|
||||
for (var i = 0; i < indices.Length; i++)
|
||||
ref var reg = ref toAtlas[toAtlasIndex];
|
||||
for (var i = 0; i < reg.Indices.Length; i++)
|
||||
{
|
||||
var dirIndices = indices[i];
|
||||
var dirOutput = output[i];
|
||||
var dirOffsets = offsets[i];
|
||||
var dirIndices = reg.Indices[i];
|
||||
var dirOutput = reg.Output[i];
|
||||
var dirOffsets = reg.Offsets[i];
|
||||
|
||||
for (var j = 0; j < dirIndices.Length; j++)
|
||||
{
|
||||
var index = sheetOffset + dirIndices[j];
|
||||
|
||||
var sheetColumn = index % dimensionX;
|
||||
var sheetRow = index / dimensionX;
|
||||
var sheetColumn = index % dimX;
|
||||
var sheetRow = index / dimX;
|
||||
var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y);
|
||||
|
||||
dirOffsets[j] = sheetPos;
|
||||
@@ -172,22 +195,104 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
}
|
||||
|
||||
sheetOffset += frameCount;
|
||||
sheetOffset += reg.TotalFrameCount;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (image, _, _, _, _) in toAtlas)
|
||||
{
|
||||
image.Dispose();
|
||||
}
|
||||
|
||||
RSI = rsi;
|
||||
internal void LoadFinish(IResourceCache cache, LoadStepData data)
|
||||
{
|
||||
RSI = data.Rsi;
|
||||
|
||||
if (cache is IResourceCacheInternal cacheInternal)
|
||||
{
|
||||
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(path, this, sheet, callbackOffsets));
|
||||
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
|
||||
}
|
||||
}
|
||||
|
||||
private static RsiMetadata LoadRsiMetadata(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
var manifestPath = path / "meta.json";
|
||||
string manifestContents;
|
||||
|
||||
using (var manifestFile = cache.ContentFileRead(manifestPath))
|
||||
using (var reader = new StreamReader(manifestFile))
|
||||
{
|
||||
manifestContents = reader.ReadToEnd();
|
||||
}
|
||||
|
||||
// Ok schema validated just fine.
|
||||
var manifestJson = JsonSerializer.Deserialize<RsiJsonMetadata>(manifestContents, SerializerOptions);
|
||||
|
||||
if (manifestJson == null)
|
||||
throw new RSILoadException("Manifest JSON was null!");
|
||||
|
||||
var size = manifestJson.Size;
|
||||
var states = new StateMetadata[manifestJson.States.Length];
|
||||
|
||||
for (var stateI = 0; stateI < manifestJson.States.Length; stateI++)
|
||||
{
|
||||
var stateObject = manifestJson.States[stateI];
|
||||
var stateName = stateObject.Name;
|
||||
RSI.State.DirectionType directions;
|
||||
int dirValue;
|
||||
|
||||
if (stateObject.Directions is { } dirVal)
|
||||
{
|
||||
dirValue = dirVal;
|
||||
directions = dirVal switch
|
||||
{
|
||||
1 => RSI.State.DirectionType.Dir1,
|
||||
4 => RSI.State.DirectionType.Dir4,
|
||||
8 => RSI.State.DirectionType.Dir8,
|
||||
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
dirValue = 1;
|
||||
directions = RSI.State.DirectionType.Dir1;
|
||||
}
|
||||
|
||||
// We can ignore selectors and flags for now,
|
||||
// because they're not used yet!
|
||||
|
||||
// Get the lists of delays.
|
||||
float[][] delays;
|
||||
if (stateObject.Delays != null)
|
||||
{
|
||||
delays = stateObject.Delays;
|
||||
|
||||
if (delays.Length != dirValue)
|
||||
{
|
||||
throw new RSILoadException(
|
||||
"DirectionsdirectionFramesList count does not match amount of delays specified.");
|
||||
}
|
||||
|
||||
for (var i = 0; i < delays.Length; i++)
|
||||
{
|
||||
var delayList = delays[i];
|
||||
if (delayList.Length == 0)
|
||||
{
|
||||
delays[i] = OneArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
delays = new float[dirValue][];
|
||||
// No delays specified, default to 1 frame per dir.
|
||||
for (var i = 0; i < dirValue; i++)
|
||||
{
|
||||
delays[i] = OneArray;
|
||||
}
|
||||
}
|
||||
|
||||
states[stateI] = new StateMetadata(new RSI.StateId(stateName), directions, delays);
|
||||
}
|
||||
|
||||
return new RsiMetadata(size, states);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Folds a per-directional sets of animation delays
|
||||
/// into an equivalent set of animation delays and indices that works for every direction.
|
||||
@@ -336,109 +441,38 @@ namespace Robust.Client.ResourceManagement
|
||||
return (floatDelays, arrayIndices);
|
||||
}
|
||||
|
||||
internal static RsiMetadata ParseMetaData(JObject manifestJson)
|
||||
internal sealed class LoadStepData
|
||||
{
|
||||
var size = manifestJson["size"]!.ToObject<Vector2i>();
|
||||
var states = new List<StateMetadata>();
|
||||
|
||||
foreach (var stateObject in manifestJson["states"]!.Cast<JObject>())
|
||||
{
|
||||
var stateName = stateObject["name"]!.ToObject<string>()!;
|
||||
RSI.State.DirectionType directions;
|
||||
int dirValue;
|
||||
|
||||
if (stateObject.TryGetValue("directions", out var dirJToken))
|
||||
{
|
||||
dirValue= dirJToken.ToObject<int>();
|
||||
directions = dirValue switch
|
||||
{
|
||||
1 => RSI.State.DirectionType.Dir1,
|
||||
4 => RSI.State.DirectionType.Dir4,
|
||||
8 => RSI.State.DirectionType.Dir8,
|
||||
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
dirValue = 1;
|
||||
directions = RSI.State.DirectionType.Dir1;
|
||||
}
|
||||
|
||||
// We can ignore selectors and flags for now,
|
||||
// because they're not used yet!
|
||||
|
||||
// Get the lists of delays.
|
||||
float[][] delays;
|
||||
if (stateObject.TryGetValue("delays", out var delayToken))
|
||||
{
|
||||
delays = delayToken.ToObject<float[][]>()!;
|
||||
|
||||
if (delays.Length != dirValue)
|
||||
{
|
||||
throw new RSILoadException(
|
||||
"DirectionsdirectionFramesList count does not match amount of delays specified.");
|
||||
}
|
||||
|
||||
for (var i = 0; i < delays.Length; i++)
|
||||
{
|
||||
var delayList = delays[i];
|
||||
if (delayList.Length == 0)
|
||||
{
|
||||
delays[i] = new float[] {1};
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
delays = new float[dirValue][];
|
||||
// No delays specified, default to 1 frame per dir.
|
||||
for (var i = 0; i < dirValue; i++)
|
||||
{
|
||||
delays[i] = new float[] {1};
|
||||
}
|
||||
}
|
||||
|
||||
states.Add(new StateMetadata(new RSI.StateId(stateName), directions, delays));
|
||||
}
|
||||
|
||||
return new RsiMetadata(size, states);
|
||||
public bool Bad;
|
||||
public ResourcePath Path = default!;
|
||||
public Image<Rgba32> AtlasSheet = default!;
|
||||
public int DimX;
|
||||
public StateReg[] AtlasList = default!;
|
||||
public Vector2i FrameSize;
|
||||
public Dictionary<RSI.StateId, Vector2i[][]> CallbackOffsets = default!;
|
||||
public Texture AtlasTexture = default!;
|
||||
public RSI Rsi = default!;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static readonly JsonSchema? RSISchema = GetSchema();
|
||||
|
||||
private static JsonSchema? GetSchema()
|
||||
internal struct StateReg
|
||||
{
|
||||
try
|
||||
{
|
||||
string schema;
|
||||
using (var schemaStream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("Robust.Client.Graphics.RSI.RSISchema.json")!)
|
||||
using (var schemaReader = new StreamReader(schemaStream))
|
||||
{
|
||||
schema = schemaReader.ReadToEnd();
|
||||
}
|
||||
|
||||
return JsonSchema.FromJsonAsync(schema).Result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Console.WriteLine("Failed to load RSI JSON Schema!\n{0}", e);
|
||||
return null;
|
||||
}
|
||||
public Image<Rgba32> Src;
|
||||
public Texture[][] Output;
|
||||
public int[][] Indices;
|
||||
public Vector2i[][] Offsets;
|
||||
public int TotalFrameCount;
|
||||
}
|
||||
#endif
|
||||
|
||||
internal sealed class RsiMetadata
|
||||
{
|
||||
public RsiMetadata(Vector2i size, List<StateMetadata> states)
|
||||
public RsiMetadata(Vector2i size, StateMetadata[] states)
|
||||
{
|
||||
Size = size;
|
||||
States = states;
|
||||
}
|
||||
|
||||
public Vector2i Size { get; }
|
||||
public List<StateMetadata> States { get; }
|
||||
public StateMetadata[] States { get; }
|
||||
}
|
||||
|
||||
internal sealed class StateMetadata
|
||||
@@ -466,6 +500,17 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
public float[][] Delays { get; }
|
||||
}
|
||||
|
||||
// To be directly deserialized.
|
||||
[UsedImplicitly]
|
||||
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States)
|
||||
{
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
@@ -9,41 +10,52 @@ using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
public class TextureResource : BaseResource
|
||||
public sealed class TextureResource : BaseResource
|
||||
{
|
||||
public const float ClickThreshold = 0.25f;
|
||||
private OwnedTexture _texture = default!;
|
||||
public override ResourcePath Fallback => new("/Textures/noSprite.png");
|
||||
|
||||
public override ResourcePath? Fallback => new("/Textures/noSprite.png");
|
||||
public Texture Texture { get; private set; } = default!;
|
||||
public Texture Texture => _texture;
|
||||
|
||||
public override void Load(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
if (!cache.TryContentFileRead(path, out var stream))
|
||||
{
|
||||
throw new FileNotFoundException("Content file does not exist for texture");
|
||||
}
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
|
||||
using (stream)
|
||||
{
|
||||
// Primarily for tracking down iCCP sRGB errors in the image files.
|
||||
Logger.DebugS("res.tex", $"Loading texture {path}.");
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
var loadParameters = _tryLoadTextureParameters(cache, path) ?? TextureLoadParameters.Default;
|
||||
|
||||
var manager = IoCManager.Resolve<IClyde>();
|
||||
|
||||
using var image = Image.Load<Rgba32>(stream);
|
||||
|
||||
Texture = manager.LoadTextureFromImage(image, path.ToString(), loadParameters);
|
||||
|
||||
if (cache is IResourceCacheInternal cacheInternal)
|
||||
{
|
||||
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(path, image, this));
|
||||
}
|
||||
}
|
||||
LoadPreTexture(cache, data);
|
||||
LoadTexture(clyde, data);
|
||||
LoadFinish(cache, data);
|
||||
}
|
||||
|
||||
private static TextureLoadParameters? _tryLoadTextureParameters(IResourceCache cache, ResourcePath path)
|
||||
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
|
||||
{
|
||||
using (var stream = cache.ContentFileRead(data.Path))
|
||||
{
|
||||
data.Image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
|
||||
data.LoadParameters = TryLoadTextureParameters(cache, data.Path) ?? TextureLoadParameters.Default;
|
||||
}
|
||||
|
||||
internal static void LoadTexture(IClyde clyde, LoadStepData data)
|
||||
{
|
||||
data.Texture = clyde.LoadTextureFromImage(data.Image, data.Path.ToString(), data.LoadParameters);
|
||||
}
|
||||
|
||||
internal void LoadFinish(IResourceCache cache, LoadStepData data)
|
||||
{
|
||||
_texture = data.Texture;
|
||||
|
||||
if (cache is IResourceCacheInternal cacheInternal)
|
||||
{
|
||||
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(data.Path, data.Image, this));
|
||||
}
|
||||
|
||||
data.Image.Dispose();
|
||||
}
|
||||
|
||||
private static TextureLoadParameters? TryLoadTextureParameters(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
var metaPath = path.WithName(path.Filename + ".yml");
|
||||
if (cache.TryContentFileRead(metaPath, out var stream))
|
||||
@@ -70,6 +82,38 @@ namespace Robust.Client.ResourceManagement
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Reload(IResourceCache cache, ResourcePath path, CancellationToken ct = default)
|
||||
{
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
|
||||
var data = new LoadStepData {Path = path};
|
||||
LoadPreTexture(cache, data);
|
||||
|
||||
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
|
||||
{
|
||||
// Dimensions match, rewrite texture in place.
|
||||
_texture.SetSubImage(Vector2i.Zero, data.Image);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dimensions do not match, make new texture.
|
||||
_texture.Dispose();
|
||||
LoadTexture(clyde, data);
|
||||
_texture = data.Texture;
|
||||
}
|
||||
|
||||
data.Image.Dispose();
|
||||
}
|
||||
|
||||
internal sealed class LoadStepData
|
||||
{
|
||||
public ResourcePath Path = default!;
|
||||
public Image<Rgba32> Image = default!;
|
||||
public TextureLoadParameters LoadParameters;
|
||||
public OwnedTexture Texture = default!;
|
||||
public bool Bad;
|
||||
}
|
||||
|
||||
// TODO: Due to a bug in Roslyn, NotNullIfNotNullAttribute doesn't work.
|
||||
// So this can't work with both nullables and non-nullables at the same time.
|
||||
// I decided to only have it work with non-nullables as such.
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
|
||||
<PackageReference Include="nfluidsynth" Version="0.3.1" />
|
||||
<PackageReference Include="NJsonSchema" Version="10.3.8" />
|
||||
<PackageReference Include="NVorbis" Version="0.10.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
|
||||
@@ -39,9 +38,6 @@
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Graphics\RSI\RSISchema.json" Condition="'$(Configuration)' == 'Debug'">
|
||||
<LogicalName>Robust.Client.Graphics.RSI.RSISchema.json</LogicalName>
|
||||
</EmbeddedResource>
|
||||
|
||||
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -9,9 +9,7 @@ namespace Robust.Client.State
|
||||
|
||||
State CurrentState { get; }
|
||||
void RequestStateChange<T>() where T : State, new();
|
||||
void Update(FrameEventArgs e);
|
||||
void FrameUpdate(FrameEventArgs e);
|
||||
void FormResize();
|
||||
void RequestStateChange(Type type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.State
|
||||
{
|
||||
@@ -14,16 +14,6 @@ namespace Robust.Client.State
|
||||
/// </summary>
|
||||
public abstract void Shutdown();
|
||||
|
||||
/// <summary>
|
||||
/// Update the contents of this screen.
|
||||
/// </summary>
|
||||
public virtual void Update(FrameEventArgs e) { }
|
||||
|
||||
public virtual void FrameUpdate(FrameEventArgs e) { }
|
||||
|
||||
/// <summary>
|
||||
/// The screen has changed size, usually from resizing window. This is called automatically right after Startup.
|
||||
/// </summary>
|
||||
public virtual void FormResize() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Log;
|
||||
using System;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -17,21 +17,11 @@ namespace Robust.Client.State
|
||||
CurrentState = new DefaultState();
|
||||
}
|
||||
|
||||
public void Update(FrameEventArgs e)
|
||||
{
|
||||
CurrentState?.Update(e);
|
||||
}
|
||||
|
||||
public void FrameUpdate(FrameEventArgs e)
|
||||
{
|
||||
CurrentState?.FrameUpdate(e);
|
||||
}
|
||||
|
||||
public void FormResize()
|
||||
{
|
||||
CurrentState?.FormResize();
|
||||
}
|
||||
|
||||
public void RequestStateChange<T>() where T : State, new()
|
||||
{
|
||||
RequestStateChange(typeof(T));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -817,25 +817,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <summary>
|
||||
/// Called when the size of the control changes.
|
||||
/// </summary>
|
||||
protected virtual void Resized()
|
||||
{
|
||||
}
|
||||
|
||||
internal void DoUpdate(FrameEventArgs args)
|
||||
{
|
||||
Update(args);
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.DoUpdate(args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called every process frame.
|
||||
/// </summary>
|
||||
protected virtual void Update(FrameEventArgs args)
|
||||
{
|
||||
}
|
||||
protected virtual void Resized() { }
|
||||
|
||||
internal void DoFrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
|
||||
@@ -385,16 +385,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var offsetY = (int) (box.Height - font.GetHeight(UIScale)) / 2;
|
||||
var baseLine = new Vector2i(0, offsetY + font.GetAscent(UIScale)) + box.TopLeft;
|
||||
|
||||
foreach (var chr in text)
|
||||
foreach (var rune in text.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(baseLine.X < box.Left || baseLine.X + metrics.Advance > box.Right))
|
||||
{
|
||||
font.DrawChar(handle, chr, baseLine, UIScale, color);
|
||||
font.DrawChar(handle, rune, baseLine, UIScale, color);
|
||||
}
|
||||
|
||||
baseLine += (metrics.Advance, 0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -180,15 +181,15 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
var baseLine = CalcBaseline();
|
||||
|
||||
foreach (var chr in _text)
|
||||
foreach (var rune in _text.EnumerateRunes())
|
||||
{
|
||||
if (chr == '\n')
|
||||
if (rune == new Rune('\n'))
|
||||
{
|
||||
newlines += 1;
|
||||
baseLine = CalcBaseline();
|
||||
}
|
||||
|
||||
var advance = font.DrawChar(handle, chr, baseLine, UIScale, actualFontColor);
|
||||
var advance = font.DrawChar(handle, rune, baseLine, UIScale, actualFontColor);
|
||||
baseLine += (advance, 0);
|
||||
}
|
||||
}
|
||||
@@ -252,16 +253,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
var font = ActualFont;
|
||||
var height = font.GetHeight(UIScale);
|
||||
foreach (var chr in _text)
|
||||
foreach (var rune in _text.EnumerateRunes())
|
||||
{
|
||||
if (chr == '\n')
|
||||
if (rune == new Rune('\n'))
|
||||
{
|
||||
_cachedTextWidths.Add(0);
|
||||
height += font.GetLineHeight(UIScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
var metrics = font.GetCharMetrics(chr, UIScale);
|
||||
var metrics = font.GetCharMetrics(rune, UIScale);
|
||||
if (metrics == null)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Input;
|
||||
@@ -23,6 +24,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public const string StyleClassLineEditNotEditable = "notEditable";
|
||||
public const string StylePseudoClassPlaceholder = "placeholder";
|
||||
|
||||
// It is assumed that these two positions are NEVER inside a surrogate pair in the text buffer.
|
||||
private int _cursorPosition;
|
||||
private int _selectionStart;
|
||||
private string _text = "";
|
||||
@@ -126,7 +128,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
get => _cursorPosition;
|
||||
set
|
||||
{
|
||||
_cursorPosition = MathHelper.Clamp(value, 0, _text.Length);
|
||||
var clamped = MathHelper.Clamp(value, 0, _text.Length);
|
||||
if (_text.Length != 0 && _text.Length != clamped && !Rune.TryGetRuneAt(_text, clamped, out _))
|
||||
throw new ArgumentException("Cannot set cursor inside surrogate pair.");
|
||||
|
||||
_cursorPosition = clamped;
|
||||
_selectionStart = _cursorPosition;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +140,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public int SelectionStart
|
||||
{
|
||||
get => _selectionStart;
|
||||
set => _selectionStart = MathHelper.Clamp(value, 0, _text.Length);
|
||||
set
|
||||
{
|
||||
var clamped = MathHelper.Clamp(value, 0, _text.Length);
|
||||
if (_text.Length != 0 && _text.Length != clamped && !Rune.TryGetRuneAt(_text, clamped, out _))
|
||||
throw new ArgumentException("Cannot set cursor inside surrogate pair.");
|
||||
|
||||
_selectionStart = clamped;
|
||||
}
|
||||
}
|
||||
|
||||
public int SelectionLower => Math.Min(_selectionStart, _cursorPosition);
|
||||
@@ -275,7 +288,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
InsertAtCursor(((char) args.CodePoint).ToString());
|
||||
InsertAtCursor(args.AsRune.ToString());
|
||||
}
|
||||
|
||||
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
@@ -302,8 +315,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
else if (_cursorPosition != 0)
|
||||
{
|
||||
_text = _text.Remove(_cursorPosition - 1, 1);
|
||||
_cursorPosition -= 1;
|
||||
var remPos = _cursorPosition - 1;
|
||||
var remAmt = 1;
|
||||
// If this is a low surrogate remove two chars to remove the whole pair.
|
||||
if (char.IsLowSurrogate(_text[remPos]))
|
||||
{
|
||||
remPos -= 1;
|
||||
remAmt = 2;
|
||||
}
|
||||
_text = _text.Remove(remPos, remAmt);
|
||||
_cursorPosition -= remAmt;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -330,7 +351,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
else if (_cursorPosition < _text.Length)
|
||||
{
|
||||
_text = _text.Remove(_cursorPosition, 1);
|
||||
var remAmt = 1;
|
||||
if (char.IsHighSurrogate(_text[_cursorPosition]))
|
||||
remAmt = 2;
|
||||
_text = _text.Remove(_cursorPosition, remAmt);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -352,10 +376,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_cursorPosition != 0)
|
||||
{
|
||||
_cursorPosition -= 1;
|
||||
}
|
||||
ShiftCursorLeft();
|
||||
|
||||
_selectionStart = _cursorPosition;
|
||||
}
|
||||
@@ -370,10 +391,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_cursorPosition != _text.Length)
|
||||
{
|
||||
_cursorPosition += 1;
|
||||
}
|
||||
ShiftCursorRight();
|
||||
|
||||
_selectionStart = _cursorPosition;
|
||||
}
|
||||
@@ -404,19 +422,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.TextCursorSelectLeft)
|
||||
{
|
||||
if (_cursorPosition != 0)
|
||||
{
|
||||
_cursorPosition -= 1;
|
||||
}
|
||||
ShiftCursorLeft();
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.TextCursorSelectRight)
|
||||
{
|
||||
if (_cursorPosition != _text.Length)
|
||||
{
|
||||
_cursorPosition += 1;
|
||||
}
|
||||
ShiftCursorRight();
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
@@ -525,6 +537,32 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
// Reset this so the cursor is always visible immediately after a keybind is pressed.
|
||||
_resetCursorBlink();
|
||||
|
||||
void ShiftCursorLeft()
|
||||
{
|
||||
if (_cursorPosition == 0)
|
||||
return;
|
||||
|
||||
_cursorPosition -= 1;
|
||||
|
||||
if (char.IsLowSurrogate(_text[_cursorPosition]))
|
||||
_cursorPosition -= 1;
|
||||
}
|
||||
|
||||
void ShiftCursorRight()
|
||||
{
|
||||
if (_cursorPosition == _text.Length)
|
||||
return;
|
||||
|
||||
_cursorPosition += 1;
|
||||
|
||||
// Before you confuse yourself on "shouldn't this be high surrogate since shifting left checks low"
|
||||
// (Because yes, I did myself too a week after writing it)
|
||||
// char.IsLowSurrogate(_text[_cursorPosition]) means "is the cursor between a surrogate pair"
|
||||
// because we ALREADY moved.
|
||||
if (_cursorPosition != _text.Length && char.IsLowSurrogate(_text[_cursorPosition]))
|
||||
_cursorPosition += 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
@@ -555,11 +593,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var index = 0;
|
||||
var chrPosX = contentBox.Left - _drawOffset;
|
||||
var lastChrPostX = contentBox.Left - _drawOffset;
|
||||
foreach (var chr in _text)
|
||||
foreach (var rune in _text.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
{
|
||||
index += 1;
|
||||
index += rune.Utf16SequenceLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -570,7 +608,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
lastChrPostX = chrPosX;
|
||||
chrPosX += metrics.Advance;
|
||||
index += 1;
|
||||
index += rune.Utf16SequenceLength;
|
||||
|
||||
if (chrPosX > contentBox.Right)
|
||||
{
|
||||
@@ -586,6 +624,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (index > 0 && distanceRight > distanceLeft)
|
||||
{
|
||||
index -= 1;
|
||||
|
||||
if (char.IsLowSurrogate(_text[index]))
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
@@ -652,60 +693,87 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
// Approach for NextWordPosition and PrevWordPosition taken from Avalonia.
|
||||
private int NextWordPosition(string str, int cursor)
|
||||
internal static int NextWordPosition(string str, int cursor)
|
||||
{
|
||||
if (cursor >= str.Length)
|
||||
{
|
||||
return str.Length;
|
||||
}
|
||||
|
||||
var charClass = GetCharClass(str[cursor]);
|
||||
var charClass = GetCharClass(Rune.GetRuneAt(str, cursor));
|
||||
|
||||
var i = cursor;
|
||||
for (; i < str.Length && GetCharClass(str[i]) == charClass; i++)
|
||||
{
|
||||
}
|
||||
|
||||
for (; i < str.Length && GetCharClass(str[i]) == CharClass.Whitespace; i++)
|
||||
{
|
||||
}
|
||||
IterForward(charClass);
|
||||
IterForward(CharClass.Whitespace);
|
||||
|
||||
return i;
|
||||
|
||||
void IterForward(CharClass cClass)
|
||||
{
|
||||
while (i < str.Length)
|
||||
{
|
||||
var rune = Rune.GetRuneAt(str, i);
|
||||
|
||||
if (GetCharClass(rune) != cClass)
|
||||
break;
|
||||
|
||||
i += rune.Utf16SequenceLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int PrevWordPosition(string str, int cursor)
|
||||
internal static int PrevWordPosition(string str, int cursor)
|
||||
{
|
||||
if (cursor == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var charClass = GetCharClass(str[cursor - 1]);
|
||||
var startRune = GetRuneBackwards(str, cursor - 1);
|
||||
var charClass = GetCharClass(startRune);
|
||||
|
||||
var i = cursor;
|
||||
for (; i > 0 && GetCharClass(str[i - 1]) == charClass; i--)
|
||||
{
|
||||
}
|
||||
IterBackward();
|
||||
|
||||
if (charClass == CharClass.Whitespace)
|
||||
{
|
||||
charClass = GetCharClass(str[i - 1]);
|
||||
for (; i > 0 && GetCharClass(str[i - 1]) == charClass; i--)
|
||||
{
|
||||
}
|
||||
if (!Rune.TryGetRuneAt(str, i - 1, out var midRune))
|
||||
midRune = Rune.GetRuneAt(str, i - 2);
|
||||
charClass = GetCharClass(midRune);
|
||||
|
||||
IterBackward();
|
||||
}
|
||||
|
||||
return i;
|
||||
|
||||
void IterBackward()
|
||||
{
|
||||
while (i > 0)
|
||||
{
|
||||
var rune = GetRuneBackwards(str, i - 1);
|
||||
|
||||
if (GetCharClass(rune) != charClass)
|
||||
break;
|
||||
|
||||
i -= rune.Utf16SequenceLength;
|
||||
}
|
||||
}
|
||||
|
||||
static Rune GetRuneBackwards(string str, int i)
|
||||
{
|
||||
return Rune.TryGetRuneAt(str, i, out var rune) ? rune : Rune.GetRuneAt(str, i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private CharClass GetCharClass(char chr)
|
||||
internal static CharClass GetCharClass(Rune rune)
|
||||
{
|
||||
if (char.IsWhiteSpace(chr))
|
||||
if (Rune.IsWhiteSpace(rune))
|
||||
{
|
||||
return CharClass.Whitespace;
|
||||
}
|
||||
|
||||
if (char.IsLetterOrDigit(chr))
|
||||
if (Rune.IsLetterOrDigit(rune))
|
||||
{
|
||||
return CharClass.AlphaNumeric;
|
||||
}
|
||||
@@ -713,7 +781,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return CharClass.Other;
|
||||
}
|
||||
|
||||
private enum CharClass : byte
|
||||
internal enum CharClass : byte
|
||||
{
|
||||
Other,
|
||||
AlphaNumeric,
|
||||
@@ -767,7 +835,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var posX = 0;
|
||||
var actualCursorPosition = 0;
|
||||
var actualSelectionStartPosition = 0;
|
||||
foreach (var chr in renderedText)
|
||||
foreach (var chr in renderedText.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
{
|
||||
@@ -776,7 +844,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
posX += metrics.Advance;
|
||||
count += 1;
|
||||
count += chr.Utf16SequenceLength;
|
||||
|
||||
if (count == _master._cursorPosition)
|
||||
{
|
||||
@@ -816,9 +884,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var baseLine = (-drawOffset, offsetY + font.GetAscent(UIScale)) +
|
||||
contentBox.TopLeft;
|
||||
|
||||
foreach (var chr in renderedText)
|
||||
foreach (var rune in renderedText.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -832,7 +900,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
// Make sure we're not off the left edge of the box.
|
||||
if (baseLine.X + metrics.BearingX + metrics.Width >= contentBox.Left)
|
||||
{
|
||||
font.DrawChar(handle, chr, baseLine, UIScale, renderedTextColor);
|
||||
font.DrawChar(handle, rune, baseLine, UIScale, renderedTextColor);
|
||||
}
|
||||
|
||||
baseLine += (metrics.Advance, 0);
|
||||
|
||||
@@ -163,9 +163,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
var titleLength = 0;
|
||||
// Get string length.
|
||||
foreach (var chr in title)
|
||||
foreach (var rune in title.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -196,14 +196,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
var baseLine = new Vector2(0, font.GetAscent(UIScale)) + contentBox.TopLeft;
|
||||
|
||||
foreach (var chr in title)
|
||||
foreach (var rune in title.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
font.DrawChar(handle, chr, baseLine, UIScale, active ? fontColorActive : fontColorInactive);
|
||||
font.DrawChar(handle, rune, baseLine, UIScale, active ? fontColorActive : fontColorInactive);
|
||||
baseLine += new Vector2(metrics.Advance, 0);
|
||||
}
|
||||
|
||||
@@ -295,9 +295,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
var titleLength = 0;
|
||||
// Get string length.
|
||||
foreach (var chr in title)
|
||||
foreach (var rune in title.EnumerateRunes())
|
||||
{
|
||||
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -219,9 +219,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
var offset = itemSelected.GetContentOffset(Vector2.Zero);
|
||||
var baseLine = offset + (hOffset, vOffset + font.GetAscent(UIScale));
|
||||
foreach (var chr in item.Text)
|
||||
foreach (var rune in item.Text.EnumerateRunes())
|
||||
{
|
||||
baseLine += (font.DrawChar(handle, chr, baseLine, UIScale, Color.White), 0);
|
||||
baseLine += (font.DrawChar(handle, rune, baseLine, UIScale, Color.White), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -301,43 +303,95 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
private async void _loadHistoryFromDisk()
|
||||
{
|
||||
CommandBar.ClearHistory();
|
||||
Stream stream;
|
||||
try
|
||||
var sawmill = Logger.GetSawmill("dbgconsole");
|
||||
var data = await Task.Run(async () =>
|
||||
{
|
||||
stream = _resourceManager.UserData.OpenRead(HistoryPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Nada, nothing to load in that case.
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
|
||||
Stream? stream = null;
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var data = JsonConvert.DeserializeObject<List<string>>(await reader.ReadToEndAsync());
|
||||
CommandBar.ClearHistory();
|
||||
CommandBar.History.AddRange(data);
|
||||
CommandBar.HistoryIndex = CommandBar.History.Count;
|
||||
try
|
||||
{
|
||||
stream = _resourceManager.UserData.OpenRead(HistoryPath);
|
||||
break;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Nada, nothing to load in that case.
|
||||
return null;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// File locked probably??
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream?.Dispose();
|
||||
}
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
sawmill.Warning("Failed to load debug console history!");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<string[]>(stream);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Warning("Failed to load debug console history due to exception!\n{e}");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
stream.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
if (data == null)
|
||||
return;
|
||||
|
||||
CommandBar.ClearHistory();
|
||||
CommandBar.History.AddRange(data);
|
||||
CommandBar.HistoryIndex = CommandBar.History.Count;
|
||||
}
|
||||
|
||||
private void _flushHistoryToDisk()
|
||||
private async void _flushHistoryToDisk()
|
||||
{
|
||||
using (var stream = _resourceManager.UserData.Create(HistoryPath))
|
||||
using (var writer = new StreamWriter(stream, EncodingHelpers.UTF8))
|
||||
CommandBar.HistoryIndex = CommandBar.History.Count;
|
||||
|
||||
var sawmill = Logger.GetSawmill("dbgconsole");
|
||||
var newHistory = JsonSerializer.Serialize(CommandBar.History);
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var data = JsonConvert.SerializeObject(CommandBar.History);
|
||||
CommandBar.HistoryIndex = CommandBar.History.Count;
|
||||
writer.Write(data);
|
||||
}
|
||||
Stream? stream = null;
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
stream = _resourceManager.UserData.Create(HistoryPath);
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Probably locking.
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
sawmill.Warning("Failed to save debug console history!");
|
||||
return;
|
||||
}
|
||||
|
||||
// ReSharper disable once UseAwaitUsing
|
||||
using var writer = new StreamWriter(stream, EncodingHelpers.UTF8);
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
writer.Write(newHistory);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -45,9 +45,9 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
protected override void Update(FrameEventArgs args)
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.Update(args);
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if ((_gameTiming.RealTime - _lastUpdate).Seconds < 1 || !VisibleInTree)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -51,9 +51,9 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
MouseFilter = contents.MouseFilter = MouseFilterMode.Ignore;
|
||||
}
|
||||
|
||||
protected override void Update(FrameEventArgs args)
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.Update(args);
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if ((GameTiming.RealTime - LastUpdate).Seconds < 1 || !VisibleInTree)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -36,9 +36,9 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
HorizontalAlignment = HAlignment.Left;
|
||||
}
|
||||
|
||||
protected override void Update(FrameEventArgs args)
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.Update(args);
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!VisibleInTree)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -67,6 +67,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
Title = Loc.GetString("Entity Spawn Panel");
|
||||
|
||||
|
||||
SetSize = (250, 300);
|
||||
MinSize = (250, 200);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
ShadowOffsetYOverride = 1;
|
||||
}
|
||||
|
||||
protected override void Update(FrameEventArgs args)
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (!VisibleInTree)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -31,9 +33,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
// We keep track of frame times in a ring buffer.
|
||||
private readonly float[] _frameTimes = new float[TrackedFrames];
|
||||
private readonly BitArray _gcMarkers = new(TrackedFrames);
|
||||
|
||||
// Position of the last frame in the ring buffer.
|
||||
private int _frameIndex;
|
||||
private int _lastGCCount;
|
||||
|
||||
public FrameGraph(IGameTiming gameTiming)
|
||||
{
|
||||
@@ -49,14 +53,21 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
var gcCount = GC.CollectionCount(0);
|
||||
|
||||
_frameTimes[_frameIndex] = (float)_gameTiming.RealFrameTime.TotalSeconds;
|
||||
_gcMarkers[_frameIndex] = gcCount > _lastGCCount;
|
||||
|
||||
_frameIndex = (_frameIndex + 1) % TrackedFrames;
|
||||
_lastGCCount = gcCount;
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
Span<Vector2> triangle = stackalloc Vector2[3];
|
||||
|
||||
float maxHeight = 0;
|
||||
for (var i = 0; i < _frameTimes.Length; i++)
|
||||
{
|
||||
@@ -88,6 +99,16 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
color = Color.Lime;
|
||||
}
|
||||
handle.DrawRect(rect, color);
|
||||
|
||||
var gc = _gcMarkers[currentFrameIndex];
|
||||
if (gc)
|
||||
{
|
||||
triangle[0] = (rect.Left, 0);
|
||||
triangle[1] = (rect.Right, 0);
|
||||
triangle[2] = (rect.Center.X, 5);
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, triangle, Color.LightBlue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
// Prevent window headers from getting off screen due to game window resizes.
|
||||
|
||||
protected override void Update(FrameEventArgs args)
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
var (spaceX, spaceY) = Parent!.Size;
|
||||
if (Position.Y > spaceY)
|
||||
|
||||
@@ -17,8 +17,6 @@ namespace Robust.Client.UserInterface
|
||||
void Initialize();
|
||||
void InitializeTesting();
|
||||
|
||||
void Update(FrameEventArgs args);
|
||||
|
||||
void FrameUpdate(FrameEventArgs args);
|
||||
|
||||
/// <returns>True if a UI control was hit and the key event should not pass through past UI.</returns>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -65,7 +66,7 @@ namespace Robust.Client.UserInterface
|
||||
var wordSizePixels = 0;
|
||||
// The horizontal position of the text cursor.
|
||||
var posX = 0;
|
||||
var lastChar = 'A';
|
||||
var lastRune = new Rune('A');
|
||||
// If a word is larger than maxSizeX, we split it.
|
||||
// We need to keep track of some data to split it into two words.
|
||||
(int breakIndex, int wordSizePixels)? forceSplitData = null;
|
||||
@@ -83,14 +84,14 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
var text = tagText.Text;
|
||||
// And go over every character.
|
||||
for (var i = 0; i < text.Length; i++, breakIndexCounter++)
|
||||
foreach (var rune in text.EnumerateRunes())
|
||||
{
|
||||
var chr = text[i];
|
||||
breakIndexCounter += 1;
|
||||
|
||||
if (IsWordBoundary(lastChar, chr) || chr == '\n')
|
||||
if (IsWordBoundary(lastRune, rune) || rune == new Rune('\n'))
|
||||
{
|
||||
// Word boundary means we know where the word ends.
|
||||
if (posX > maxSizeX && lastChar != ' ')
|
||||
if (posX > maxSizeX && lastRune != new Rune(' '))
|
||||
{
|
||||
DebugTools.Assert(wordStartBreakIndex.HasValue,
|
||||
"wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line.");
|
||||
@@ -109,22 +110,22 @@ namespace Robust.Client.UserInterface
|
||||
forceSplitData = null;
|
||||
|
||||
// Just manually handle newlines.
|
||||
if (chr == '\n')
|
||||
if (rune == new Rune('\n'))
|
||||
{
|
||||
LineBreaks.Add(breakIndexCounter);
|
||||
Height += font.GetLineHeight(uiScale);
|
||||
maxUsedWidth = Math.Max(maxUsedWidth, posX);
|
||||
posX = 0;
|
||||
lastChar = chr;
|
||||
lastRune = rune;
|
||||
wordStartBreakIndex = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Uh just skip unknown characters I guess.
|
||||
if (!font.TryGetCharMetrics(chr, uiScale, out var metrics))
|
||||
if (!font.TryGetCharMetrics(rune, uiScale, out var metrics))
|
||||
{
|
||||
lastChar = chr;
|
||||
lastRune = rune;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
lastChar = chr;
|
||||
lastRune = rune;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +186,7 @@ namespace Robust.Client.UserInterface
|
||||
Logger.Error("wordStartBreakIndex: null (duh)");
|
||||
Logger.Error($"wordSizePixels: {wordSizePixels}");
|
||||
Logger.Error($"posX: {posX}");
|
||||
Logger.Error($"lastChar: {lastChar}");
|
||||
Logger.Error($"lastChar: {lastRune}");
|
||||
Logger.Error($"forceSplitData: {forceSplitData}");
|
||||
Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
|
||||
|
||||
@@ -247,9 +248,10 @@ namespace Robust.Client.UserInterface
|
||||
case FormattedMessage.TagText tagText:
|
||||
{
|
||||
var text = tagText.Text;
|
||||
for (var i = 0; i < text.Length; i++, globalBreakCounter++)
|
||||
foreach (var rune in text.EnumerateRunes())
|
||||
{
|
||||
var chr = text[i];
|
||||
globalBreakCounter += 1;
|
||||
|
||||
if (lineBreakIndex < LineBreaks.Count &&
|
||||
LineBreaks[lineBreakIndex] == globalBreakCounter)
|
||||
{
|
||||
@@ -257,7 +259,7 @@ namespace Robust.Client.UserInterface
|
||||
lineBreakIndex += 1;
|
||||
}
|
||||
|
||||
var advance = font.DrawChar(handle, chr, baseLine, uiScale, currentColorTag.Color);
|
||||
var advance = font.DrawChar(handle, rune, baseLine, uiScale, currentColorTag.Color);
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
|
||||
@@ -268,9 +270,9 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static bool IsWordBoundary(char a, char b)
|
||||
private static bool IsWordBoundary(Rune a, Rune b)
|
||||
{
|
||||
return a == ' ' || b == ' ' || a == '-' || b == '-';
|
||||
return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -133,6 +133,7 @@ namespace Robust.Client.UserInterface
|
||||
QueueMeasureUpdate(RootControl);
|
||||
|
||||
_displayManager.OnWindowResized += args => _updateRootSize();
|
||||
_displayManager.OnWindowScaleChanged += UpdateUIScale;
|
||||
|
||||
StateRoot = new LayoutContainer
|
||||
{
|
||||
@@ -174,15 +175,9 @@ namespace Robust.Client.UserInterface
|
||||
_initializeCommon();
|
||||
}
|
||||
|
||||
public void Update(FrameEventArgs args)
|
||||
{
|
||||
RootControl.DoUpdate(args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
RootControl.DoFrameUpdate(args);
|
||||
|
||||
// Process queued style & layout updates.
|
||||
while (_styleUpdateQueue.Count != 0)
|
||||
{
|
||||
@@ -220,6 +215,8 @@ namespace Robust.Client.UserInterface
|
||||
RunArrange(control);
|
||||
}
|
||||
|
||||
RootControl.DoFrameUpdate(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)
|
||||
@@ -833,7 +830,13 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
private void _uiScaleChanged(float newValue)
|
||||
{
|
||||
UIScale = newValue == 0f ? DefaultUIScale : newValue;
|
||||
UpdateUIScale();
|
||||
}
|
||||
|
||||
private void UpdateUIScale()
|
||||
{
|
||||
var newVal = _configurationManager.GetCVar(CVars.DisplayUIScale);
|
||||
UIScale = newVal == 0f ? DefaultUIScale : newVal;
|
||||
|
||||
if (RootControl == null)
|
||||
{
|
||||
|
||||
@@ -42,19 +42,20 @@ namespace Robust.Client.Utility
|
||||
{
|
||||
var dstSpan = destination.GetPixelSpan();
|
||||
var dstWidth = destination.Width;
|
||||
var srcHeight = sourceRect.Height;
|
||||
var srcWidth = sourceRect.Width;
|
||||
|
||||
var (ox, oy) = destinationOffset;
|
||||
|
||||
for (var y = 0; y < sourceRect.Height; y++)
|
||||
for (var y = 0; y < srcHeight; y++)
|
||||
{
|
||||
var sourceRowOffset = sourceWidth * (y + sourceRect.Top) + sourceRect.Left;
|
||||
var destRowOffset = dstWidth * (y + oy) + ox;
|
||||
|
||||
for (var x = 0; x < sourceRect.Width; x++)
|
||||
{
|
||||
var pixel = source[x + sourceRowOffset];
|
||||
dstSpan[x + destRowOffset] = pixel;
|
||||
}
|
||||
var srcRow = source[sourceRowOffset..(sourceRowOffset + srcWidth)];
|
||||
var dstRow = dstSpan[destRowOffset..(destRowOffset + srcWidth)];
|
||||
|
||||
srcRow.CopyTo(dstRow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -41,6 +41,40 @@ namespace Robust.Client.Utility
|
||||
return specifier.RsiStateLike().Default;
|
||||
}
|
||||
|
||||
public static float[] FrameDelays(this SpriteSpecifier specifier) {
|
||||
var resc = IoCManager.Resolve<IResourceCache>();
|
||||
switch (specifier) {
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
if (resc.TryGetResource<RSIResource>(SpriteComponent.TextureRoot / rsi.RsiPath, out var theRsi)) {
|
||||
if (theRsi.RSI.TryGetState(rsi.RsiState, out var state)) {
|
||||
return state.Delays;
|
||||
}
|
||||
}
|
||||
Logger.Error("Failed to load RSI {0}", rsi.RsiPath);
|
||||
return new float[0];
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Texture[] FrameArr(this SpriteSpecifier specifier) {
|
||||
var resc = IoCManager.Resolve<IResourceCache>();
|
||||
switch (specifier) {
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
if (resc.TryGetResource<RSIResource>(SpriteComponent.TextureRoot / rsi.RsiPath, out var theRsi)) {
|
||||
if (theRsi.RSI.TryGetState(rsi.RsiState, out var state)) {
|
||||
return state.Icons[0];
|
||||
}
|
||||
}
|
||||
Logger.Error("Failed to load RSI {0}", rsi.RsiPath);
|
||||
return new Texture[0];
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
return specifier.RsiStateLike();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Prometheus;
|
||||
@@ -93,8 +94,8 @@ namespace Robust.Server
|
||||
private IGameLoop _mainLoop = default!;
|
||||
|
||||
private TimeSpan _lastTitleUpdate;
|
||||
private int _lastReceivedBytes;
|
||||
private int _lastSentBytes;
|
||||
private long _lastReceivedBytes;
|
||||
private long _lastSentBytes;
|
||||
|
||||
private string? _shutdownReason;
|
||||
|
||||
@@ -141,6 +142,10 @@ namespace Robust.Server
|
||||
/// <inheritdoc />
|
||||
public bool Start(Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
var profilePath = Path.Join(Environment.CurrentDirectory, "AAAAAAAA");
|
||||
ProfileOptimization.SetProfileRoot(profilePath);
|
||||
ProfileOptimization.StartProfile("AAAAAAAAAA");
|
||||
|
||||
_config.Initialize(true);
|
||||
|
||||
if (LoadConfigAndUserData)
|
||||
@@ -178,6 +183,8 @@ namespace Robust.Server
|
||||
_config.OverrideConVars(_commandLineArgs.CVars);
|
||||
}
|
||||
|
||||
ProfileOptSetup.Setup(_config);
|
||||
|
||||
//Sets up Logging
|
||||
_logHandlerFactory = logHandlerFactory;
|
||||
|
||||
@@ -327,6 +334,8 @@ namespace Robust.Server
|
||||
WindowsTickPeriod.TimeBeginPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
|
||||
}
|
||||
|
||||
GC.Collect();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -564,7 +573,7 @@ namespace Robust.Server
|
||||
}
|
||||
|
||||
// Pass Histogram into the IEntityManager.Update so it can do more granular measuring.
|
||||
_entities.Update(frameEventArgs.DeltaSeconds, TickUsage);
|
||||
_entities.TickUpdate(frameEventArgs.DeltaSeconds, TickUsage);
|
||||
|
||||
using (TickUsage.WithLabels("PostEngine").NewTimer())
|
||||
{
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerAttachSystemMessage : EntitySystemMessage
|
||||
public class PlayerAttachSystemMessage : EntityEventArgs
|
||||
{
|
||||
public PlayerAttachSystemMessage(IEntity entity, IPlayerSession newPlayer)
|
||||
{
|
||||
@@ -63,7 +63,7 @@ namespace Robust.Server.GameObjects
|
||||
public IPlayerSession NewPlayer { get; }
|
||||
}
|
||||
|
||||
public class PlayerDetachedSystemMessage : EntitySystemMessage
|
||||
public class PlayerDetachedSystemMessage : EntityEventArgs
|
||||
{
|
||||
public PlayerDetachedSystemMessage(IEntity entity)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Robust.Server.GameObjects
|
||||
private Vector2 _zoom = Vector2.One/2f;
|
||||
private Vector2 _offset;
|
||||
private Angle _rotation;
|
||||
private uint _visibilityMask;
|
||||
|
||||
public override bool DrawFov
|
||||
{
|
||||
@@ -69,9 +70,22 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public override uint VisibilityMask
|
||||
{
|
||||
get => _visibilityMask;
|
||||
set
|
||||
{
|
||||
if(_visibilityMask == value)
|
||||
return;
|
||||
|
||||
_visibilityMask = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation);
|
||||
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation, VisibilityMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -24,7 +25,7 @@ namespace Robust.Server.GameObjects
|
||||
[DataField("visible")]
|
||||
private bool _visible = true;
|
||||
|
||||
[DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))]
|
||||
[DataField("drawdepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
|
||||
private int _drawDepth = DrawDepthTag.Default;
|
||||
|
||||
[DataField("scale")]
|
||||
@@ -54,17 +55,19 @@ namespace Robust.Server.GameObjects
|
||||
get => _drawDepth;
|
||||
set
|
||||
{
|
||||
if (_drawDepth == value) return;
|
||||
_drawDepth = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Visible
|
||||
public override bool Visible
|
||||
{
|
||||
get => _visible;
|
||||
set
|
||||
{
|
||||
if (_visible == value) return;
|
||||
_visible = value;
|
||||
Dirty();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Robust.Server.GameObjects
|
||||
/// <remarks>
|
||||
/// List is empty if it's no longer intersecting any.
|
||||
/// </remarks>
|
||||
public sealed class TileLookupUpdateMessage : EntitySystemMessage
|
||||
public sealed class TileLookupUpdateMessage : EntityEventArgs
|
||||
{
|
||||
public Dictionary<GridId, List<Vector2i>>? NewIndices { get; }
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Server.Player;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class AudioSystem : EntitySystem, IAudioSystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private const int AudioDistanceRange = 25;
|
||||
|
||||
private uint _streamIndex;
|
||||
@@ -66,147 +63,6 @@ namespace Robust.Server.GameObjects
|
||||
return unchecked(_streamIndex++);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
/// <param name="predicate">The predicate that will be used to send the audio to players, or null to send to everyone.</param>
|
||||
/// <param name="excludedSession">Session that won't receive the audio message.</param>
|
||||
/// <param name="recipients"></param>
|
||||
[Obsolete("Use the Play() overload.")]
|
||||
public IPlayingAudioStream PlayGlobal(string filename, AudioParams? audioParams = null, Func<IPlayerSession, bool>? predicate = null, IPlayerSession? excludedSession = null)
|
||||
{
|
||||
var id = CacheIdentifier();
|
||||
var msg = new PlayAudioGlobalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
AudioParams = audioParams ?? AudioParams.Default,
|
||||
Identifier = id
|
||||
};
|
||||
|
||||
if (predicate == null && excludedSession == null)
|
||||
{
|
||||
RaiseNetworkEvent(msg);
|
||||
return new AudioSourceServer(this, id);
|
||||
}
|
||||
|
||||
IList<IPlayerSession> players = predicate != null ? _playerManager.GetPlayersBy(predicate) : _playerManager.GetAllPlayers();
|
||||
|
||||
for (var i = players.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var player = players[i];
|
||||
if (player == excludedSession)
|
||||
{
|
||||
players.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(msg, player.ConnectedClient);
|
||||
}
|
||||
|
||||
return new AudioSourceServer(this, id, players);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
/// <param name="range">The max range at which the audio will be heard. Less than or equal to 0 to send to every player.</param>
|
||||
/// <param name="excludedSession">Sessions that won't receive the audio message.</param>
|
||||
[Obsolete("Use the Play() overload.")]
|
||||
public IPlayingAudioStream PlayFromEntity(string filename, IEntity entity, AudioParams? audioParams = null, int range = AudioDistanceRange, IPlayerSession? excludedSession = null)
|
||||
{
|
||||
var id = CacheIdentifier();
|
||||
|
||||
var msg = new PlayAudioEntityMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Coordinates = entity.Transform.Coordinates,
|
||||
EntityUid = entity.Uid,
|
||||
AudioParams = audioParams ?? AudioParams.Default,
|
||||
Identifier = id,
|
||||
};
|
||||
|
||||
// send to every player
|
||||
if (range <= 0 && excludedSession == null)
|
||||
{
|
||||
RaiseNetworkEvent(msg);
|
||||
return new AudioSourceServer(this, id);
|
||||
}
|
||||
|
||||
List<IPlayerSession> players;
|
||||
|
||||
if (range > 0.0f)
|
||||
players = _playerManager.GetPlayersInRange(entity.Transform.Coordinates, range);
|
||||
else
|
||||
players = _playerManager.GetAllPlayers();
|
||||
|
||||
for (var i = players.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var player = players[i];
|
||||
if (player == excludedSession)
|
||||
{
|
||||
players.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(msg, player.ConnectedClient);
|
||||
}
|
||||
|
||||
return new AudioSourceServer(this, id, players);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
/// <param name="range">The max range at which the audio will be heard. Less than or equal to 0 to send to every player.</param>
|
||||
/// <param name="excludedSession">Session that won't receive the audio message.</param>
|
||||
[Obsolete("Use the Play() overload.")]
|
||||
public IPlayingAudioStream PlayAtCoords(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null, int range = AudioDistanceRange, IPlayerSession? excludedSession = null)
|
||||
{
|
||||
var id = CacheIdentifier();
|
||||
var msg = new PlayAudioPositionalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Coordinates = coordinates,
|
||||
AudioParams = audioParams ?? AudioParams.Default,
|
||||
Identifier = id
|
||||
};
|
||||
|
||||
if (range <= 0 && excludedSession == null)
|
||||
{
|
||||
RaiseNetworkEvent(msg);
|
||||
return new AudioSourceServer(this, id);
|
||||
}
|
||||
|
||||
List<IPlayerSession> players;
|
||||
|
||||
if (range > 0.0f)
|
||||
players = _playerManager.GetPlayersInRange(coordinates, range);
|
||||
else
|
||||
players = _playerManager.GetAllPlayers();
|
||||
|
||||
for (var i = players.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var player = players[i];
|
||||
if (player == excludedSession)
|
||||
{
|
||||
players.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(msg, player.ConnectedClient);
|
||||
}
|
||||
|
||||
return new AudioSourceServer(this, id, players);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int DefaultSoundRange => AudioDistanceRange;
|
||||
|
||||
@@ -214,7 +70,7 @@ namespace Robust.Server.GameObjects
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPlayingAudioStream? Play(Filter playerFilter, string filename, AudioParams? audioParams = null)
|
||||
public IPlayingAudioStream Play(Filter playerFilter, string filename, AudioParams? audioParams = null)
|
||||
{
|
||||
var id = CacheIdentifier();
|
||||
var msg = new PlayAudioGlobalMessage
|
||||
@@ -234,7 +90,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPlayingAudioStream? Play(Filter playerFilter, string filename, IEntity entity, AudioParams? audioParams = null)
|
||||
public IPlayingAudioStream Play(Filter playerFilter, string filename, IEntity entity, AudioParams? audioParams = null)
|
||||
{
|
||||
//TODO: Calculate this from PAS
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
|
||||
@@ -254,7 +110,7 @@ namespace Robust.Server.GameObjects
|
||||
var recipients = (playerFilter as IFilter).Recipients;
|
||||
|
||||
if (range > 0.0f)
|
||||
players = PASInRange(recipients, entity.Transform.MapPosition, range);
|
||||
players = PasInRange(recipients, entity.Transform.MapPosition, range);
|
||||
else
|
||||
players = recipients;
|
||||
|
||||
@@ -267,7 +123,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public IPlayingAudioStream Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
//TODO: Calculate this from PAS
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
|
||||
@@ -285,7 +141,7 @@ namespace Robust.Server.GameObjects
|
||||
var recipients = (playerFilter as IFilter).Recipients;
|
||||
|
||||
if (range > 0.0f)
|
||||
players = PASInRange(recipients, coordinates.ToMap(EntityManager), range);
|
||||
players = PasInRange(recipients, coordinates.ToMap(EntityManager), range);
|
||||
else
|
||||
players = recipients;
|
||||
|
||||
@@ -297,7 +153,7 @@ namespace Robust.Server.GameObjects
|
||||
return new AudioSourceServer(this, id, players);
|
||||
}
|
||||
|
||||
private static List<ICommonSession> PASInRange(IEnumerable<ICommonSession> players, MapCoordinates position, float range)
|
||||
private static List<ICommonSession> PasInRange(IEnumerable<ICommonSession> players, MapCoordinates position, float range)
|
||||
{
|
||||
return players.Where(x =>
|
||||
x.AttachedEntity != null &&
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
@@ -10,11 +12,19 @@ namespace Robust.Server.GameObjects
|
||||
public class PhysicsSystem : SharedPhysicsSystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_mapManager.OnGridCreated += HandleGridCreated;
|
||||
LoadMetricCVar();
|
||||
_configurationManager.OnValueChanged(CVars.MetricsEnabled, _ => LoadMetricCVar());
|
||||
}
|
||||
|
||||
private void LoadMetricCVar()
|
||||
{
|
||||
MetricsEnabled = _configurationManager.GetCVar(CVars.MetricsEnabled);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
||||
@@ -1,41 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
public interface IServerEntityManager : IEntityManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all entity states that have been modified after and including the provided tick.
|
||||
/// </summary>
|
||||
List<EntityState>? GetEntityStates(GameTick fromTick, IPlayerSession player);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all entity states within an AABB that have been modified after and including the provided tick.
|
||||
/// </summary>
|
||||
List<EntityState>? UpdatePlayerSeenEntityStates(GameTick fromTick, IPlayerSession player, float range);
|
||||
|
||||
// Keep track of deleted entities so we can sync deletions with the client.
|
||||
/// <summary>
|
||||
/// Gets a list of all entity UIDs that were deleted between <paramref name="fromTick" /> and now.
|
||||
/// </summary>
|
||||
List<EntityUid>? GetDeletedEntities(GameTick fromTick);
|
||||
|
||||
/// <summary>
|
||||
/// Remove deletion history.
|
||||
/// </summary>
|
||||
/// <param name="toTick">The last tick to delete the history for. Inclusive.</param>
|
||||
void CullDeletionHistory(GameTick toTick);
|
||||
|
||||
/// <summary>
|
||||
/// Removes entity state persistence information from the entity manager for a player.
|
||||
/// </summary>
|
||||
/// <param name="player"></param>
|
||||
void DropPlayerState(IPlayerSession player);
|
||||
|
||||
float MaxUpdateRange { get; }
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Server side version of the <see cref="IEntityManager"/>.
|
||||
/// </summary>
|
||||
public interface IServerEntityManager : IEntityManager { }
|
||||
}
|
||||
|
||||
@@ -1,55 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Prometheus;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.GameObjects.TransformComponent;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Manager for entities -- controls things like template loading and instantiation
|
||||
/// </summary>
|
||||
[UsedImplicitly] // DI Container
|
||||
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
|
||||
{
|
||||
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
|
||||
"robust_entities_count",
|
||||
"Amount of alive entities.");
|
||||
|
||||
private const float MinimumMotionForMovers = 1 / 128f;
|
||||
|
||||
#region IEntityManager Members
|
||||
|
||||
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Shared.IoC.Dependency] private readonly IPauseManager _pauseManager = default!;
|
||||
[Shared.IoC.Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
private float? _maxUpdateRangeCache;
|
||||
|
||||
public float MaxUpdateRange => _maxUpdateRangeCache
|
||||
??= _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPauseManager _pauseManager = default!;
|
||||
|
||||
private int _nextServerEntityUid = (int) EntityUid.FirstUid;
|
||||
|
||||
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
_maxUpdateRangeCache = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity CreateEntityUninitialized(string? prototypeName)
|
||||
{
|
||||
@@ -79,33 +53,6 @@ namespace Robust.Server.GameObjects
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
private Entity CreateEntityServer(string? prototypeName)
|
||||
{
|
||||
var entity = CreateEntity(prototypeName);
|
||||
|
||||
if (prototypeName != null)
|
||||
{
|
||||
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
|
||||
|
||||
// At this point in time, all data configure on the entity *should* be purely from the prototype.
|
||||
// As such, we can reset the modified ticks to Zero,
|
||||
// which indicates "not different from client's own deserialization".
|
||||
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
|
||||
foreach (var component in ComponentManager.GetNetComponents(entity.Uid))
|
||||
{
|
||||
// Make sure to ONLY get components that are defined in the prototype.
|
||||
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
|
||||
// And those aren't guaranteed to exist on the client, so don't clear them.
|
||||
if (prototype.Components.ContainsKey(component.Name))
|
||||
{
|
||||
((Component) component).ClearTicks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity SpawnEntity(string? protoName, EntityCoordinates coordinates)
|
||||
{
|
||||
@@ -116,10 +63,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
InitializeAndStartEntity((Entity) entity);
|
||||
|
||||
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this)))
|
||||
{
|
||||
entity.RunMapInit();
|
||||
}
|
||||
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this))) entity.RunMapInit();
|
||||
|
||||
return entity;
|
||||
}
|
||||
@@ -133,728 +77,26 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity SpawnEntityNoMapInit(string? protoName, EntityCoordinates coordinates)
|
||||
public override void Startup()
|
||||
{
|
||||
var newEnt = CreateEntityUninitialized(protoName, coordinates);
|
||||
InitializeAndStartEntity((Entity) newEnt);
|
||||
return newEnt;
|
||||
base.Startup();
|
||||
EntitySystemManager.Initialize();
|
||||
Started = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<EntityState>? GetEntityStates(GameTick fromTick, IPlayerSession player)
|
||||
public override void TickUpdate(float frameTime, Histogram? histogram)
|
||||
{
|
||||
var stateEntities = new List<EntityState>();
|
||||
foreach (var entity in AllEntities)
|
||||
{
|
||||
if (entity.Deleted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
base.TickUpdate(frameTime, histogram);
|
||||
|
||||
DebugTools.Assert(entity.Initialized);
|
||||
|
||||
if (entity.LastModifiedTick <= fromTick)
|
||||
continue;
|
||||
|
||||
stateEntities.Add(GetEntityState(ComponentManager, entity.Uid, fromTick, player));
|
||||
}
|
||||
|
||||
// no point sending an empty collection
|
||||
return stateEntities.Count == 0 ? default : stateEntities;
|
||||
EntitiesCount.Set(AllEntities.Count);
|
||||
}
|
||||
|
||||
private readonly Dictionary<IPlayerSession, SortedSet<EntityUid>> _seenMovers
|
||||
= new();
|
||||
|
||||
// Is thread safe.
|
||||
private SortedSet<EntityUid> GetSeenMovers(IPlayerSession player)
|
||||
{
|
||||
lock (_seenMovers)
|
||||
{
|
||||
return GetSeenMoversUnlocked(player);
|
||||
}
|
||||
}
|
||||
|
||||
private SortedSet<EntityUid> GetSeenMoversUnlocked(IPlayerSession player)
|
||||
{
|
||||
if (!_seenMovers.TryGetValue(player, out var movers))
|
||||
{
|
||||
movers = new SortedSet<EntityUid>();
|
||||
_seenMovers.Add(player, movers);
|
||||
}
|
||||
|
||||
return movers;
|
||||
}
|
||||
|
||||
private void AddToSeenMovers(IPlayerSession player, EntityUid entityUid)
|
||||
{
|
||||
var movers = GetSeenMoversUnlocked(player);
|
||||
|
||||
movers.Add(entityUid);
|
||||
}
|
||||
|
||||
private readonly Dictionary<IPlayerSession, Dictionary<EntityUid, GameTick>> _playerLastSeen
|
||||
= new();
|
||||
|
||||
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
|
||||
|
||||
private Dictionary<EntityUid, GameTick> GetLastSeen(IPlayerSession player)
|
||||
{
|
||||
lock (_playerLastSeen)
|
||||
{
|
||||
if (!_playerLastSeen.TryGetValue(player, out var lastSeen))
|
||||
{
|
||||
lastSeen = new Dictionary<EntityUid, GameTick>();
|
||||
_playerLastSeen.Add(player, lastSeen);
|
||||
}
|
||||
|
||||
return lastSeen;
|
||||
}
|
||||
}
|
||||
|
||||
private static GameTick GetLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid)
|
||||
{
|
||||
if (!lastSeen.TryGetValue(uid, out var tick))
|
||||
{
|
||||
tick = GameTick.First;
|
||||
}
|
||||
|
||||
return tick;
|
||||
}
|
||||
|
||||
private static GameTick UpdateLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid, GameTick newTick)
|
||||
{
|
||||
if (!lastSeen.TryGetValue(uid, out var oldTick))
|
||||
{
|
||||
oldTick = GameTick.First;
|
||||
}
|
||||
|
||||
lastSeen[uid] = newTick;
|
||||
|
||||
return oldTick;
|
||||
}
|
||||
|
||||
private static IEnumerable<EntityUid> GetLastSeenAfter(Dictionary<EntityUid, GameTick> lastSeen, GameTick fromTick)
|
||||
{
|
||||
foreach (var (uid, tick) in lastSeen)
|
||||
{
|
||||
if (tick > fromTick)
|
||||
{
|
||||
yield return uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<EntityUid> GetLastSeenOn(Dictionary<EntityUid, GameTick> lastSeen, GameTick fromTick)
|
||||
{
|
||||
foreach (var (uid, tick) in lastSeen)
|
||||
{
|
||||
if (tick == fromTick)
|
||||
{
|
||||
yield return uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid, GameTick tick)
|
||||
{
|
||||
lastSeen[uid] = tick;
|
||||
}
|
||||
|
||||
private static void ClearLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid)
|
||||
{
|
||||
lastSeen.Remove(uid);
|
||||
}
|
||||
|
||||
public void DropPlayerState(IPlayerSession player)
|
||||
{
|
||||
lock (_playerLastSeen)
|
||||
{
|
||||
_playerLastSeen.Remove(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void IncludeRelatives(IEnumerable<IEntity> children, HashSet<IEntity> set)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
var ent = child!;
|
||||
|
||||
while (ent != null && !ent.Deleted)
|
||||
{
|
||||
if (set.Add(ent))
|
||||
{
|
||||
AddContainedRecursive(ent, set);
|
||||
|
||||
ent = ent.Transform.Parent?.Owner!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Already processed this entity once.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddContainedRecursive(IEntity ent, HashSet<IEntity> set)
|
||||
{
|
||||
if (!ent.TryGetComponent(out ContainerManagerComponent? contMgr))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var container in contMgr.GetAllContainers())
|
||||
{
|
||||
// Manual for loop to cut out allocations.
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
for (var i = 0; i < container.ContainedEntities.Count; i++)
|
||||
{
|
||||
var contEnt = container.ContainedEntities[i];
|
||||
set.Add(contEnt);
|
||||
AddContainedRecursive(contEnt, set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerSeenEntityStatesResources
|
||||
{
|
||||
public readonly HashSet<EntityUid> IncludedEnts = new();
|
||||
|
||||
public readonly List<EntityState> EntityStates = new();
|
||||
|
||||
public readonly HashSet<EntityUid> NeededEnts = new();
|
||||
|
||||
public readonly HashSet<IEntity> Relatives = new();
|
||||
}
|
||||
|
||||
private readonly ThreadLocal<PlayerSeenEntityStatesResources> _playerSeenEntityStatesResources
|
||||
= new(() => new PlayerSeenEntityStatesResources());
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<EntityState>? UpdatePlayerSeenEntityStates(GameTick fromTick, IPlayerSession player, float range)
|
||||
{
|
||||
var playerEnt = player.AttachedEntity;
|
||||
if (playerEnt == null)
|
||||
{
|
||||
// super-observer?
|
||||
return GetEntityStates(fromTick, player);
|
||||
}
|
||||
|
||||
var playerUid = playerEnt.Uid;
|
||||
|
||||
var transform = playerEnt.Transform;
|
||||
var position = transform.WorldPosition;
|
||||
var mapId = transform.MapID;
|
||||
var viewbox = new Box2(position, position).Enlarged(MaxUpdateRange);
|
||||
|
||||
var seenMovers = GetSeenMovers(player);
|
||||
var lSeen = GetLastSeen(player);
|
||||
|
||||
var pseStateRes = _playerSeenEntityStatesResources.Value!;
|
||||
var checkedEnts = pseStateRes.IncludedEnts;
|
||||
var entityStates = pseStateRes.EntityStates;
|
||||
var neededEnts = pseStateRes.NeededEnts;
|
||||
var relatives = pseStateRes.Relatives;
|
||||
checkedEnts.Clear();
|
||||
entityStates.Clear();
|
||||
neededEnts.Clear();
|
||||
relatives.Clear();
|
||||
|
||||
foreach (var uid in seenMovers.ToList())
|
||||
{
|
||||
if (!TryGetEntity(uid, out var entity) || entity.Deleted)
|
||||
{
|
||||
seenMovers.Remove(uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out IPhysBody? body))
|
||||
{
|
||||
if (body.LinearVelocity.EqualsApprox(Vector2.Zero, MinimumMotionForMovers))
|
||||
{
|
||||
if (AnyParentInSet(uid, seenMovers))
|
||||
{
|
||||
// parent is moving
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MathF.Abs(body.AngularVelocity) > 0)
|
||||
{
|
||||
if (entity.TryGetComponent(out TransformComponent? txf) && txf.ChildCount > 0)
|
||||
{
|
||||
// has children spinning
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
seenMovers.Remove(uid);
|
||||
}
|
||||
}
|
||||
|
||||
var state = GetEntityState(ComponentManager, uid, fromTick, player);
|
||||
|
||||
if (checkedEnts.Add(uid))
|
||||
{
|
||||
entityStates.Add(state);
|
||||
|
||||
// mover did not change
|
||||
if (state.ComponentStates != null)
|
||||
{
|
||||
// mover can be seen
|
||||
if (!viewbox.Intersects(GetWorldAabbFromEntity(entity)))
|
||||
{
|
||||
// mover changed and can't be seen
|
||||
var idx = Array.FindIndex(state.ComponentStates,
|
||||
x => x is TransformComponentState);
|
||||
|
||||
if (idx != -1)
|
||||
{
|
||||
// mover changed positional data and can't be seen
|
||||
var oldState =
|
||||
(TransformComponentState) state.ComponentStates[idx];
|
||||
var newState = new TransformComponentState(Vector2NaN,
|
||||
oldState.Rotation, oldState.ParentID, oldState.NoLocalRotation);
|
||||
state.ComponentStates[idx] = newState;
|
||||
seenMovers.Remove(uid);
|
||||
ClearLastSeenTick(lSeen, uid);
|
||||
|
||||
checkedEnts.Add(uid);
|
||||
|
||||
var needed = oldState.ParentID;
|
||||
if (!needed.IsValid() || checkedEnts.Contains(needed))
|
||||
{
|
||||
// either no parent attached or parent already included
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetLastSeenTick(lSeen, needed) == GameTick.Zero)
|
||||
{
|
||||
neededEnts.Add(needed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// mover already added?
|
||||
if (!viewbox.Intersects(GetWorldAabbFromEntity(entity)))
|
||||
{
|
||||
// mover can't be seen
|
||||
var oldState =
|
||||
(TransformComponentState) entity.Transform.GetComponentState(player);
|
||||
entityStates.Add(new EntityState(uid,
|
||||
new ComponentChanged[]
|
||||
{
|
||||
new(false, NetIDs.TRANSFORM, "Transform")
|
||||
},
|
||||
new ComponentState[]
|
||||
{
|
||||
new TransformComponentState(Vector2NaN, oldState.Rotation,
|
||||
oldState.ParentID, oldState.NoLocalRotation)
|
||||
}));
|
||||
|
||||
seenMovers.Remove(uid);
|
||||
ClearLastSeenTick(lSeen, uid);
|
||||
checkedEnts.Add(uid);
|
||||
|
||||
var needed = oldState.ParentID;
|
||||
if (!needed.IsValid() || checkedEnts.Contains(needed))
|
||||
{
|
||||
// either no parent attached or parent already included
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetLastSeenTick(lSeen, needed) == GameTick.Zero)
|
||||
{
|
||||
neededEnts.Add(needed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentTick = CurrentTick;
|
||||
|
||||
// scan pvs box and include children and parents recursively
|
||||
IncludeRelatives(GetEntitiesInRange(mapId, position, range, true), relatives);
|
||||
|
||||
// Exclude any entities that are currently invisible to the player.
|
||||
ExcludeInvisible(relatives, player.VisibilityMask);
|
||||
|
||||
// Always send updates for all grid and map entities.
|
||||
// If we don't, the client-side game state manager WILL blow up.
|
||||
// TODO: Make map manager netcode aware of PVS to avoid the need for this workaround.
|
||||
IncludeMapCriticalEntities(relatives);
|
||||
|
||||
foreach (var entity in relatives)
|
||||
{
|
||||
DebugTools.Assert(entity.Initialized && !entity.Deleted);
|
||||
|
||||
var lastChange = entity.LastModifiedTick;
|
||||
|
||||
var uid = entity.Uid;
|
||||
|
||||
var lastSeen = UpdateLastSeenTick(lSeen, uid, currentTick);
|
||||
|
||||
DebugTools.Assert(lastSeen != currentTick);
|
||||
|
||||
/*
|
||||
if (uid != playerUid && entity.Prototype == playerEnt.Prototype && lastSeen < fromTick)
|
||||
{
|
||||
Logger.DebugS("pvs", $"Player {playerUid} is seeing player {uid}.");
|
||||
}
|
||||
*/
|
||||
|
||||
if (checkedEnts.Contains(uid))
|
||||
{
|
||||
// already have it
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastChange <= lastSeen)
|
||||
{
|
||||
// hasn't changed since last seen
|
||||
continue;
|
||||
}
|
||||
|
||||
// should this be lastSeen or fromTick?
|
||||
var entityState = GetEntityState(ComponentManager, uid, lastSeen, player);
|
||||
|
||||
checkedEnts.Add(uid);
|
||||
|
||||
if (entityState.ComponentStates == null)
|
||||
{
|
||||
// no changes
|
||||
continue;
|
||||
}
|
||||
|
||||
entityStates.Add(entityState);
|
||||
|
||||
if (uid == playerUid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entity.TryGetComponent(out IPhysBody? body))
|
||||
{
|
||||
// can't be a mover w/o physics
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!body.LinearVelocity.EqualsApprox(Vector2.Zero, MinimumMotionForMovers))
|
||||
{
|
||||
// has motion
|
||||
seenMovers.Add(uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// not moving
|
||||
seenMovers.Remove(uid);
|
||||
}
|
||||
}
|
||||
|
||||
var priorTick = new GameTick(fromTick.Value - 1);
|
||||
foreach (var uid in GetLastSeenOn(lSeen, priorTick))
|
||||
{
|
||||
if (checkedEnts.Contains(uid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (uid == playerUid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetEntity(uid, out var entity) || entity.Deleted)
|
||||
{
|
||||
// TODO: remove from states list being sent?
|
||||
continue;
|
||||
}
|
||||
|
||||
if (viewbox.Intersects(GetWorldAabbFromEntity(entity)))
|
||||
{
|
||||
// can be seen
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = GetEntityState(ComponentManager, uid, fromTick, player);
|
||||
|
||||
if (state.ComponentStates == null)
|
||||
{
|
||||
// nothing changed
|
||||
continue;
|
||||
}
|
||||
|
||||
checkedEnts.Add(uid);
|
||||
entityStates.Add(state);
|
||||
|
||||
seenMovers.Remove(uid);
|
||||
ClearLastSeenTick(lSeen, uid);
|
||||
|
||||
var idx = Array.FindIndex(state.ComponentStates, x => x is TransformComponentState);
|
||||
|
||||
if (idx == -1)
|
||||
{
|
||||
// no transform changes
|
||||
continue;
|
||||
}
|
||||
|
||||
var oldState = (TransformComponentState) state.ComponentStates[idx];
|
||||
var newState =
|
||||
new TransformComponentState(Vector2NaN, oldState.Rotation, oldState.ParentID, oldState.NoLocalRotation);
|
||||
state.ComponentStates[idx] = newState;
|
||||
|
||||
|
||||
var needed = oldState.ParentID;
|
||||
if (!needed.IsValid() || checkedEnts.Contains(needed))
|
||||
{
|
||||
// don't need to include parent or already included
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetLastSeenTick(lSeen, needed) == GameTick.First)
|
||||
{
|
||||
neededEnts.Add(needed);
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
var moreNeededEnts = new HashSet<EntityUid>();
|
||||
|
||||
foreach (var uid in moreNeededEnts)
|
||||
{
|
||||
if (checkedEnts.Contains(uid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entity = GetEntity(uid);
|
||||
var state = GetEntityState(ComponentManager, uid, fromTick, player);
|
||||
|
||||
if (state.ComponentStates == null || viewbox.Intersects(GetWorldAabbFromEntity(entity)))
|
||||
{
|
||||
// no states or should already be seen
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
checkedEnts.Add(uid);
|
||||
entityStates.Add(state);
|
||||
|
||||
var idx = Array.FindIndex(state.ComponentStates,
|
||||
x => x is TransformComponentState);
|
||||
|
||||
if (idx == -1)
|
||||
{
|
||||
// no transform state
|
||||
continue;
|
||||
}
|
||||
|
||||
var oldState = (TransformComponentState) state.ComponentStates[idx];
|
||||
var newState =
|
||||
new TransformComponentState(Vector2NaN, oldState.Rotation,
|
||||
oldState.ParentID, oldState.NoLocalRotation);
|
||||
state.ComponentStates[idx] = newState;
|
||||
seenMovers.Remove(uid);
|
||||
|
||||
ClearLastSeenTick(lSeen, uid);
|
||||
var needed = oldState.ParentID;
|
||||
|
||||
if (!needed.IsValid() || checkedEnts.Contains(needed))
|
||||
{
|
||||
// done here
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if further needed
|
||||
if (!checkedEnts.Contains(uid) && GetLastSeenTick(lSeen, needed) == GameTick.Zero)
|
||||
{
|
||||
moreNeededEnts.Add(needed);
|
||||
}
|
||||
}
|
||||
|
||||
neededEnts = moreNeededEnts;
|
||||
} while (neededEnts.Count > 0);
|
||||
|
||||
// help the client out
|
||||
entityStates.Sort((a, b) => a.Uid.CompareTo(b.Uid));
|
||||
|
||||
#if DEBUG_NULL_ENTITY_STATES
|
||||
foreach ( var state in entityStates ) {
|
||||
if (state.ComponentStates == null)
|
||||
{
|
||||
throw new NotImplementedException("Shouldn't send null states.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// no point sending an empty collection
|
||||
return entityStates.Count == 0 ? default : entityStates;
|
||||
}
|
||||
|
||||
public override void DeleteEntity(IEntity e)
|
||||
{
|
||||
base.DeleteEntity(e);
|
||||
EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(e));
|
||||
_deletionHistory.Add((CurrentTick, e.Uid));
|
||||
}
|
||||
|
||||
public List<EntityUid>? GetDeletedEntities(GameTick fromTick)
|
||||
{
|
||||
var list = new List<EntityUid>();
|
||||
foreach (var (tick, id) in _deletionHistory)
|
||||
{
|
||||
if (tick >= fromTick)
|
||||
{
|
||||
list.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
// no point sending an empty collection
|
||||
return list.Count == 0 ? default : list;
|
||||
}
|
||||
|
||||
public void CullDeletionHistory(GameTick toTick)
|
||||
{
|
||||
_deletionHistory.RemoveAll(hist => hist.tick <= toTick);
|
||||
}
|
||||
|
||||
public override bool UpdateEntityTree(IEntity entity, Box2? worldAABB = null)
|
||||
{
|
||||
var currentTick = CurrentTick;
|
||||
var updated = base.UpdateEntityTree(entity, worldAABB);
|
||||
|
||||
if (entity.Deleted
|
||||
|| !entity.Initialized
|
||||
|| !Entities.ContainsKey(entity.Uid))
|
||||
{
|
||||
return updated;
|
||||
}
|
||||
|
||||
DebugTools.Assert(entity.Transform.Initialized);
|
||||
|
||||
// note: updated can be false even if something moved a bit
|
||||
worldAABB ??= GetWorldAabbFromEntity(entity);
|
||||
|
||||
foreach (var (player, lastSeen) in _playerLastSeen)
|
||||
{
|
||||
var playerEnt = player.AttachedEntity;
|
||||
if (playerEnt == null)
|
||||
{
|
||||
// player has no entity, gaf?
|
||||
continue;
|
||||
}
|
||||
|
||||
var playerUid = playerEnt.Uid;
|
||||
|
||||
var entityUid = entity.Uid;
|
||||
|
||||
if (entityUid == playerUid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lastSeen.TryGetValue(playerUid, out var playerTick))
|
||||
{
|
||||
// player can't "see" itself, gaf?
|
||||
continue;
|
||||
}
|
||||
|
||||
var playerPos = playerEnt.Transform.WorldPosition;
|
||||
|
||||
var viewbox = new Box2(playerPos, playerPos).Enlarged(MaxUpdateRange);
|
||||
|
||||
if (!lastSeen.TryGetValue(entityUid, out var tick))
|
||||
{
|
||||
// never saw it other than first tick or was cleared
|
||||
if (!AnyParentMoving(player, entityUid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (tick >= currentTick)
|
||||
{
|
||||
// currently seeing it
|
||||
continue;
|
||||
}
|
||||
// saw it previously
|
||||
|
||||
// player can't see it now
|
||||
if (!viewbox.Intersects(worldAABB.Value))
|
||||
{
|
||||
var addToMovers = false;
|
||||
if (entity.Transform.LastModifiedTick >= currentTick)
|
||||
{
|
||||
addToMovers = true;
|
||||
}
|
||||
else if (entity.TryGetComponent(out IPhysBody? physics)
|
||||
&& physics.LastModifiedTick >= currentTick)
|
||||
{
|
||||
addToMovers = true;
|
||||
}
|
||||
|
||||
if (addToMovers)
|
||||
{
|
||||
AddToSeenMovers(player, entityUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private bool AnyParentMoving(IPlayerSession player, EntityUid entityUid)
|
||||
{
|
||||
var seenMovers = GetSeenMoversUnlocked(player);
|
||||
if (seenMovers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return AnyParentInSet(entityUid, seenMovers);
|
||||
}
|
||||
|
||||
private bool AnyParentInSet(EntityUid entityUid, SortedSet<EntityUid> set)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (!TryGetEntity(entityUid, out var ent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var txf = ent.Transform;
|
||||
|
||||
entityUid = txf.ParentUid;
|
||||
|
||||
if (entityUid == EntityUid.Invalid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (set.Contains(entityUid))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IEntityManager Members
|
||||
|
||||
IEntity IServerEntityManagerInternal.AllocEntity(string? prototypeName, EntityUid? uid)
|
||||
{
|
||||
return AllocEntity(prototypeName, uid);
|
||||
}
|
||||
|
||||
protected override EntityUid GenerateEntityUid()
|
||||
{
|
||||
return new(_nextServerEntityUid++);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityLoad(IEntity entity, IEntityLoadContext? context)
|
||||
{
|
||||
LoadEntity((Entity) entity, context);
|
||||
@@ -871,95 +113,33 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Startup()
|
||||
protected override EntityUid GenerateEntityUid()
|
||||
{
|
||||
base.Startup();
|
||||
EntitySystemManager.Initialize();
|
||||
Started = true;
|
||||
return new(_nextServerEntityUid++);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a network entity state for the given entity.
|
||||
/// </summary>
|
||||
/// <param name="compMan">ComponentManager that contains the components for the entity.</param>
|
||||
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
|
||||
/// <param name="fromTick">Only provide delta changes from this tick.</param>
|
||||
/// <param name="player">The player to generate this state for.</param>
|
||||
/// <returns>New entity State for the given entity.</returns>
|
||||
private static EntityState GetEntityState(IComponentManager compMan, EntityUid entityUid, GameTick fromTick, IPlayerSession player)
|
||||
private Entity CreateEntityServer(string? prototypeName)
|
||||
{
|
||||
var compStates = new List<ComponentState>();
|
||||
var changed = new List<ComponentChanged>();
|
||||
var entity = CreateEntity(prototypeName);
|
||||
|
||||
foreach (var comp in compMan.GetNetComponents(entityUid))
|
||||
if (prototypeName != null)
|
||||
{
|
||||
DebugTools.Assert(comp.Initialized);
|
||||
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
|
||||
|
||||
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
|
||||
// "not different from entity creation".
|
||||
// i.e. when the client spawns the entity and loads the entity prototype,
|
||||
// the data it deserializes from the prototype SHOULD be equal
|
||||
// to what the component state / ComponentChanged would send.
|
||||
// As such, we can avoid sending this data in this case since the client "already has it".
|
||||
|
||||
if (comp.NetSyncEnabled && comp.LastModifiedTick != GameTick.Zero && comp.LastModifiedTick >= fromTick)
|
||||
compStates.Add(comp.GetComponentState(player));
|
||||
|
||||
if (comp.CreationTick != GameTick.Zero && comp.CreationTick >= fromTick && !comp.Deleted)
|
||||
// At this point in time, all data configure on the entity *should* be purely from the prototype.
|
||||
// As such, we can reset the modified ticks to Zero,
|
||||
// which indicates "not different from client's own deserialization".
|
||||
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
|
||||
foreach (var component in ComponentManager.GetNetComponents(entity.Uid))
|
||||
{
|
||||
// Can't be null since it's returned by GetNetComponents
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
changed.Add(ComponentChanged.Added(comp.NetID!.Value, comp.Name));
|
||||
}
|
||||
else if (comp.Deleted && comp.LastModifiedTick >= fromTick)
|
||||
{
|
||||
// Can't be null since it's returned by GetNetComponents
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
changed.Add(ComponentChanged.Removed(comp.NetID!.Value));
|
||||
// Make sure to ONLY get components that are defined in the prototype.
|
||||
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
|
||||
// And those aren't guaranteed to exist on the client, so don't clear them.
|
||||
if (prototype.Components.ContainsKey(component.Name)) ((Component) component).ClearTicks();
|
||||
}
|
||||
}
|
||||
|
||||
return new EntityState(entityUid, changed.ToArray(), compStates.ToArray());
|
||||
}
|
||||
|
||||
|
||||
private void IncludeMapCriticalEntities(HashSet<IEntity> set)
|
||||
{
|
||||
foreach (var mapId in _mapManager.GetAllMapIds())
|
||||
{
|
||||
if (_mapManager.HasMapEntity(mapId))
|
||||
{
|
||||
set.Add(_mapManager.GetMapEntity(mapId));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var grid in _mapManager.GetAllGrids())
|
||||
{
|
||||
if (grid.GridEntityId != EntityUid.Invalid)
|
||||
{
|
||||
set.Add(GetEntity(grid.GridEntityId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExcludeInvisible(HashSet<IEntity> set, int visibilityMask)
|
||||
{
|
||||
set.RemoveWhere(e =>
|
||||
{
|
||||
if (!e.TryGetComponent(out VisibilityComponent? visibility))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (visibilityMask & visibility.Layer) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
public override void Update(float frameTime, Histogram? histogram)
|
||||
{
|
||||
base.Update(frameTime, histogram);
|
||||
|
||||
EntitiesCount.Set(AllEntities.Count);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Server.GameObjects
|
||||
_configurationManager.OnValueChanged(CVars.NetLogLateMsg, b => _logLateMsgs = b, true);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
public void TickUpdate()
|
||||
{
|
||||
while (_queue.Count != 0 && _queue.Peek().SourceTick <= _gameTiming.CurTick)
|
||||
{
|
||||
@@ -87,7 +87,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntitySystemMessage message)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message)
|
||||
{
|
||||
var newMsg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
newMsg.Type = EntityMessageType.SystemMessage;
|
||||
@@ -98,7 +98,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntitySystemMessage message, INetChannel targetConnection)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel targetConnection)
|
||||
{
|
||||
var newMsg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
newMsg.Type = EntityMessageType.SystemMessage;
|
||||
|
||||
324
Robust.Server/GameStates/EntityViewCulling.cs
Normal file
324
Robust.Server/GameStates/EntityViewCulling.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
internal class EntityViewCulling
|
||||
{
|
||||
private const int ViewSetCapacity = 128; // starting number of entities that are in view
|
||||
private const int PlayerSetSize = 64; // Starting number of players
|
||||
private const int MaxVisPoolSize = 1024; // Maximum number of pooled objects
|
||||
|
||||
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
|
||||
|
||||
private readonly IServerEntityManager _entMan;
|
||||
private readonly IComponentManager _compMan;
|
||||
private readonly IMapManager _mapManager;
|
||||
|
||||
private readonly Dictionary<ICommonSession, HashSet<EntityUid>> _playerVisibleSets = new(PlayerSetSize);
|
||||
|
||||
private readonly ConcurrentDictionary<ICommonSession, GameTick> _playerLastFullMap = new();
|
||||
|
||||
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
|
||||
|
||||
private readonly ObjectPool<HashSet<EntityUid>> _visSetPool
|
||||
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
|
||||
|
||||
private readonly ObjectPool<HashSet<EntityUid>> _viewerEntsPool
|
||||
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
|
||||
|
||||
/// <summary>
|
||||
/// Is view culling enabled, or will we send the whole map?
|
||||
/// </summary>
|
||||
public bool CullingEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the side of the view bounds square.
|
||||
/// </summary>
|
||||
public float ViewSize { get; set; }
|
||||
|
||||
public EntityViewCulling(IServerEntityManager entMan, IMapManager mapManager)
|
||||
{
|
||||
_entMan = entMan;
|
||||
_compMan = entMan.ComponentManager;
|
||||
_mapManager = mapManager;
|
||||
_compMan = _entMan.ComponentManager;
|
||||
}
|
||||
|
||||
// Not thread safe
|
||||
public void EntityDeleted(EntityUid e)
|
||||
{
|
||||
// Not aware of prediction
|
||||
_deletionHistory.Add((_entMan.CurrentTick, e));
|
||||
}
|
||||
|
||||
// Not thread safe
|
||||
public void CullDeletionHistory(GameTick oldestAck)
|
||||
{
|
||||
_deletionHistory.RemoveAll(hist => hist.tick < oldestAck);
|
||||
}
|
||||
|
||||
private List<EntityUid> GetDeletedEntities(GameTick fromTick)
|
||||
{
|
||||
var list = new List<EntityUid>();
|
||||
foreach (var (tick, id) in _deletionHistory)
|
||||
{
|
||||
if (tick >= fromTick) list.Add(id);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// Not thread safe
|
||||
public void AddPlayer(ICommonSession session)
|
||||
{
|
||||
_playerVisibleSets.Add(session, new HashSet<EntityUid>(ViewSetCapacity));
|
||||
}
|
||||
|
||||
// Not thread safe
|
||||
public void RemovePlayer(ICommonSession session)
|
||||
{
|
||||
_playerVisibleSets.Remove(session);
|
||||
_playerLastFullMap.Remove(session, out _);
|
||||
}
|
||||
|
||||
// thread safe
|
||||
public bool IsPointVisible(ICommonSession session, in MapCoordinates position)
|
||||
{
|
||||
var viewables = GetSessionViewers(session);
|
||||
|
||||
bool CheckInView(MapCoordinates mapCoordinates, HashSet<EntityUid> entityUids)
|
||||
{
|
||||
foreach (var euid in entityUids)
|
||||
{
|
||||
var (viewBox, mapId) = CalcViewBounds(in euid);
|
||||
|
||||
if (mapId != mapCoordinates.MapId)
|
||||
continue;
|
||||
|
||||
if (!CullingEnabled)
|
||||
return true;
|
||||
|
||||
if (viewBox.Contains(mapCoordinates.Position))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = CheckInView(position, viewables);
|
||||
|
||||
viewables.Clear();
|
||||
_viewerEntsPool.Return(viewables);
|
||||
return result;
|
||||
}
|
||||
|
||||
private HashSet<EntityUid> GetSessionViewers(ICommonSession session)
|
||||
{
|
||||
var viewers = _viewerEntsPool.Get();
|
||||
if (session.Status != SessionStatus.InGame || session.AttachedEntityUid is null)
|
||||
return viewers;
|
||||
|
||||
var query = _compMan.EntityQuery<BasicActorComponent>();
|
||||
|
||||
foreach (var actorComp in query)
|
||||
{
|
||||
if (actorComp.playerSession == session)
|
||||
viewers.Add(actorComp.Owner.Uid);
|
||||
}
|
||||
|
||||
return viewers;
|
||||
}
|
||||
|
||||
// thread safe
|
||||
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(ICommonSession session, GameTick fromTick, GameTick toTick)
|
||||
{
|
||||
DebugTools.Assert(session.Status == SessionStatus.InGame);
|
||||
|
||||
//TODO: Stop sending all entities to every player first tick
|
||||
List<EntityUid>? deletions;
|
||||
if (!CullingEnabled || fromTick == GameTick.Zero)
|
||||
{
|
||||
var allStates = ServerGameStateManager.GetAllEntityStates(_entMan, session, fromTick);
|
||||
deletions = GetDeletedEntities(fromTick);
|
||||
_playerLastFullMap.AddOrUpdate(session, toTick, (_, _) => toTick);
|
||||
return (allStates, deletions);
|
||||
}
|
||||
|
||||
var lastMapUpdate = _playerLastFullMap.GetValueOrDefault(session);
|
||||
var currentSet = CalcCurrentViewSet(session);
|
||||
|
||||
// If they don't have a usable eye, nothing to send, and map remove will deal with ent removal
|
||||
if (currentSet is null)
|
||||
return (null, null);
|
||||
|
||||
deletions = GetDeletedEntities(fromTick);
|
||||
|
||||
// pretty big allocations :(
|
||||
List<EntityState> entityStates = new(currentSet.Count);
|
||||
var previousSet = _playerVisibleSets[session];
|
||||
|
||||
// complement set
|
||||
foreach (var entityUid in previousSet)
|
||||
{
|
||||
if (!currentSet.Contains(entityUid) && !deletions.Contains(entityUid))
|
||||
{
|
||||
if(_compMan.HasComponent<SnapGridComponent>(entityUid))
|
||||
continue;
|
||||
|
||||
// PVS leave message
|
||||
//TODO: Remove NaN as the signal to leave PVS
|
||||
var xform = _compMan.GetComponent<ITransformComponent>(entityUid);
|
||||
var oldState = (TransformComponent.TransformComponentState)xform.GetComponentState(session);
|
||||
entityStates.Add(new EntityState(entityUid,
|
||||
new ComponentChanged[]
|
||||
{
|
||||
new(false, NetIDs.TRANSFORM, "Transform")
|
||||
},
|
||||
new ComponentState[]
|
||||
{
|
||||
new TransformComponent.TransformComponentState(Vector2NaN, oldState.Rotation,
|
||||
oldState.ParentID, oldState.NoLocalRotation)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entityUid in currentSet)
|
||||
{
|
||||
if (previousSet.Contains(entityUid))
|
||||
{
|
||||
//Still Visible
|
||||
// only send new changes
|
||||
var newState = ServerGameStateManager.GetEntityState(_entMan.ComponentManager, session, entityUid, fromTick);
|
||||
|
||||
if (!newState.Empty)
|
||||
entityStates.Add(newState);
|
||||
}
|
||||
else
|
||||
{
|
||||
// PVS enter message
|
||||
|
||||
// skip sending anchored entities (walls)
|
||||
if (_compMan.HasComponent<SnapGridComponent>(entityUid) && _entMan.GetEntity(entityUid).LastModifiedTick <= lastMapUpdate)
|
||||
continue;
|
||||
|
||||
// don't assume the client knows anything about us
|
||||
var newState = ServerGameStateManager.GetEntityState(_entMan.ComponentManager, session, entityUid, GameTick.Zero);
|
||||
entityStates.Add(newState);
|
||||
}
|
||||
}
|
||||
|
||||
// swap out vis sets
|
||||
_playerVisibleSets[session] = currentSet;
|
||||
previousSet.Clear();
|
||||
_visSetPool.Return(previousSet);
|
||||
|
||||
// no point sending an empty collection
|
||||
deletions = deletions?.Count == 0 ? default : deletions;
|
||||
|
||||
return (entityStates, deletions);
|
||||
}
|
||||
|
||||
private HashSet<EntityUid>? CalcCurrentViewSet(ICommonSession session)
|
||||
{
|
||||
if (!CullingEnabled)
|
||||
return null;
|
||||
|
||||
// if you don't have an attached entity, you don't see the world.
|
||||
if (session.AttachedEntityUid is null)
|
||||
return null;
|
||||
|
||||
var visibleEnts = _visSetPool.Get();
|
||||
var viewers = GetSessionViewers(session);
|
||||
|
||||
foreach (var eyeEuid in viewers)
|
||||
{
|
||||
var (viewBox, mapId) = CalcViewBounds(in eyeEuid);
|
||||
|
||||
uint visMask = 0;
|
||||
if (_compMan.TryGetComponent<EyeComponent>(eyeEuid, out var eyeComp))
|
||||
visMask = eyeComp.VisibilityMask;
|
||||
|
||||
//Always include the map entity
|
||||
visibleEnts.Add(_mapManager.GetMapEntityId(mapId));
|
||||
|
||||
//Always include viewable ent itself
|
||||
visibleEnts.Add(eyeEuid);
|
||||
|
||||
// grid entity should be added through this
|
||||
// assume there are no deleted ents in here, cull them first in ent/comp manager
|
||||
_entMan.FastEntitiesIntersecting(in mapId, ref viewBox, entity => RecursiveAdd((TransformComponent)entity.Transform, visibleEnts, visMask));
|
||||
}
|
||||
|
||||
viewers.Clear();
|
||||
_viewerEntsPool.Return(viewers);
|
||||
|
||||
return visibleEnts;
|
||||
}
|
||||
|
||||
// Read Safe
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
private bool RecursiveAdd(TransformComponent xform, HashSet<EntityUid> visSet, uint visMask)
|
||||
{
|
||||
var xformUid = xform.Owner.Uid;
|
||||
|
||||
// we are done, this ent has already been checked and is visible
|
||||
if (visSet.Contains(xformUid))
|
||||
return true;
|
||||
|
||||
// if we are invisible, we are not going into the visSet, so don't worry about parents, and children are not going in
|
||||
if (_compMan.TryGetComponent<VisibilityComponent>(xformUid, out var visComp))
|
||||
{
|
||||
if ((visMask & visComp.Layer) == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
var xformParentUid = xform.ParentUid;
|
||||
|
||||
// this is the world entity, it is always visible
|
||||
if (!xformParentUid.IsValid())
|
||||
{
|
||||
visSet.Add(xformUid);
|
||||
return true;
|
||||
}
|
||||
|
||||
// parent is already in the set
|
||||
if (visSet.Contains(xformParentUid))
|
||||
{
|
||||
visSet.Add(xformUid);
|
||||
return true;
|
||||
}
|
||||
|
||||
// parent was not added, so we are not either
|
||||
var xformParent = _compMan.GetComponent<TransformComponent>(xformParentUid);
|
||||
if (!RecursiveAdd(xformParent, visSet, visMask))
|
||||
return false;
|
||||
|
||||
// add us
|
||||
visSet.Add(xformUid);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read Safe
|
||||
private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid)
|
||||
{
|
||||
var xform = _compMan.GetComponent<ITransformComponent>(euid);
|
||||
|
||||
var view = Box2.UnitCentered.Scale(ViewSize).Translated(xform.WorldPosition);
|
||||
var map = xform.MapID;
|
||||
|
||||
return (view, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
/// <summary>
|
||||
/// Engine service that provides creating and dispatching of game states.
|
||||
/// Engine service that provides creating and dispatching of game states.
|
||||
/// </summary>
|
||||
public interface IServerGameStateManager
|
||||
{
|
||||
/// <summary>
|
||||
/// One time initialization of the service.
|
||||
/// One time initialization of the service.
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Create and dispatch game states to all connected sessions.
|
||||
/// Create and dispatch game states to all connected sessions.
|
||||
/// </summary>
|
||||
void SendGameStateUpdate();
|
||||
|
||||
bool PvsEnabled { get; }
|
||||
float PvsRange { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
@@ -9,22 +10,24 @@ using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ServerGameStateManager : IServerGameStateManager
|
||||
/// <inheritdoc cref="IServerGameStateManager"/>
|
||||
public class ServerGameStateManager : IServerGameStateManager, IPostInjectInit
|
||||
{
|
||||
// Mapping of net UID of clients -> last known acked state.
|
||||
private readonly Dictionary<long, GameTick> _ackedStates = new();
|
||||
private GameTick _lastOldestAck = GameTick.Zero;
|
||||
|
||||
private EntityViewCulling _entityView = null!;
|
||||
|
||||
[Dependency] private readonly IServerEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IServerNetManager _networkManager = default!;
|
||||
@@ -35,6 +38,12 @@ namespace Robust.Server.GameStates
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
public bool PvsEnabled => _configurationManager.GetCVar(CVars.NetPVS);
|
||||
public float PvsRange => _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_entityView = new EntityViewCulling(_entityManager, _mapManager);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
@@ -44,6 +53,27 @@ namespace Robust.Server.GameStates
|
||||
|
||||
_networkManager.Connected += HandleClientConnected;
|
||||
_networkManager.Disconnect += HandleClientDisconnect;
|
||||
|
||||
_playerManager.PlayerStatusChanged += HandlePlayerStatusChanged;
|
||||
|
||||
_entityManager.EntityDeleted += HandleEntityDeleted;
|
||||
}
|
||||
|
||||
private void HandleEntityDeleted(object? sender, EntityUid e)
|
||||
{
|
||||
_entityView.EntityDeleted(e);
|
||||
}
|
||||
|
||||
private void HandlePlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus == SessionStatus.InGame)
|
||||
{
|
||||
_entityView.AddPlayer(e.Session);
|
||||
}
|
||||
else if(e.OldStatus == SessionStatus.InGame)
|
||||
{
|
||||
_entityView.RemovePlayer(e.Session);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleClientConnected(object? sender, NetChannelArgs e)
|
||||
@@ -57,13 +87,6 @@ namespace Robust.Server.GameStates
|
||||
private void HandleClientDisconnect(object? sender, NetChannelArgs e)
|
||||
{
|
||||
_ackedStates.Remove(e.Channel.ConnectionId);
|
||||
|
||||
if (!_playerManager.TryGetSessionByChannel(e.Channel, out var session))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entityManager.DropPlayerState(session);
|
||||
}
|
||||
|
||||
private void HandleStateAck(MsgStateAck msg)
|
||||
@@ -98,12 +121,13 @@ namespace Robust.Server.GameStates
|
||||
{
|
||||
DebugTools.Assert(_networkManager.IsServer);
|
||||
|
||||
_entityManager.Update();
|
||||
_entityView.ViewSize = PvsRange * 2;
|
||||
_entityView.CullingEnabled = PvsEnabled;
|
||||
|
||||
if (!_networkManager.IsConnected)
|
||||
{
|
||||
// Prevent deletions piling up if we have no clients.
|
||||
_entityManager.CullDeletionHistory(GameTick.MaxValue);
|
||||
_entityView.CullDeletionHistory(GameTick.MaxValue);
|
||||
_mapManager.CullDeletionHistory(GameTick.MaxValue);
|
||||
return;
|
||||
}
|
||||
@@ -112,16 +136,14 @@ namespace Robust.Server.GameStates
|
||||
|
||||
var oldestAck = GameTick.MaxValue;
|
||||
|
||||
var oldDeps = IoCManager.Resolve<IDependencyCollection>();
|
||||
|
||||
var deps = new DependencyCollection();
|
||||
deps.RegisterInstance<ILogManager>(new ProxyLogManager(IoCManager.Resolve<ILogManager>()));
|
||||
deps.BuildGraph();
|
||||
|
||||
var mainThread = Thread.CurrentThread;
|
||||
(MsgState, INetChannel) GenerateMail(IPlayerSession session)
|
||||
{
|
||||
IoCManager.InitThread(deps, true);
|
||||
// KILL IT WITH FIRE
|
||||
if(mainThread != Thread.CurrentThread)
|
||||
IoCManager.InitThread(new DependencyCollection(), true);
|
||||
|
||||
// people not in the game don't get states
|
||||
if (session.Status != SessionStatus.InGame)
|
||||
{
|
||||
return default;
|
||||
@@ -134,11 +156,8 @@ namespace Robust.Server.GameStates
|
||||
DebugTools.Assert("Why does this channel not have an entry?");
|
||||
}
|
||||
|
||||
var entStates = lastAck == GameTick.Zero || !PvsEnabled
|
||||
? _entityManager.GetEntityStates(lastAck, session)
|
||||
: _entityManager.UpdatePlayerSeenEntityStates(lastAck, session, _entityManager.MaxUpdateRange);
|
||||
var (entStates, deletions) = _entityView.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
|
||||
var playerStates = _playerManager.GetPlayerStates(lastAck);
|
||||
var deletions = _entityManager.GetDeletedEntities(lastAck);
|
||||
var mapData = _mapManager.GetStateData(lastAck);
|
||||
|
||||
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
|
||||
@@ -167,15 +186,9 @@ namespace Robust.Server.GameStates
|
||||
}
|
||||
|
||||
var mailBag = _playerManager.GetAllPlayers()
|
||||
.AsParallel().Select(GenerateMail).ToList();
|
||||
|
||||
// TODO: oh god oh fuck kill it with fire.
|
||||
// PLINQ *seems* to be scheduling to the main thread partially (I guess that makes sense?)
|
||||
// Which causes that IoC "hack" up there to override IoC in the main thread, nuking the game.
|
||||
// At least, that's our running theory. I reproduced it once locally and can't reproduce it again.
|
||||
// Throwing shit at the wall to hope it fixes it.
|
||||
IoCManager.InitThread(oldDeps, true);
|
||||
|
||||
.AsParallel()
|
||||
.Where(s=>s.Status == SessionStatus.InGame).Select(GenerateMail);
|
||||
|
||||
foreach (var (msg, chan) in mailBag)
|
||||
{
|
||||
// see session.Status != SessionStatus.InGame above
|
||||
@@ -187,9 +200,78 @@ namespace Robust.Server.GameStates
|
||||
if (oldestAck > _lastOldestAck)
|
||||
{
|
||||
_lastOldestAck = oldestAck;
|
||||
_entityManager.CullDeletionHistory(oldestAck);
|
||||
_entityView.CullDeletionHistory(oldestAck);
|
||||
_mapManager.CullDeletionHistory(oldestAck);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a network entity state for the given entity.
|
||||
/// </summary>
|
||||
/// <param name="compMan">ComponentManager that contains the components for the entity.</param>
|
||||
/// <param name="player">The player to generate this state for.</param>
|
||||
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
|
||||
/// <param name="fromTick">Only provide delta changes from this tick.</param>
|
||||
/// <returns>New entity State for the given entity.</returns>
|
||||
internal static EntityState GetEntityState(IComponentManager compMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
|
||||
{
|
||||
var compStates = new List<ComponentState>();
|
||||
var changed = new List<ComponentChanged>();
|
||||
|
||||
foreach (var comp in compMan.GetNetComponents(entityUid))
|
||||
{
|
||||
DebugTools.Assert(comp.Initialized);
|
||||
|
||||
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
|
||||
// "not different from entity creation".
|
||||
// i.e. when the client spawns the entity and loads the entity prototype,
|
||||
// the data it deserializes from the prototype SHOULD be equal
|
||||
// to what the component state / ComponentChanged would send.
|
||||
// As such, we can avoid sending this data in this case since the client "already has it".
|
||||
|
||||
if (comp.NetSyncEnabled && comp.LastModifiedTick != GameTick.Zero && comp.LastModifiedTick >= fromTick)
|
||||
compStates.Add(comp.GetComponentState(player));
|
||||
|
||||
if (comp.CreationTick != GameTick.Zero && comp.CreationTick >= fromTick && !comp.Deleted)
|
||||
{
|
||||
// Can't be null since it's returned by GetNetComponents
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
changed.Add(ComponentChanged.Added(comp.NetID!.Value, comp.Name));
|
||||
}
|
||||
else if (comp.Deleted && comp.LastModifiedTick >= fromTick)
|
||||
{
|
||||
// Can't be null since it's returned by GetNetComponents
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
changed.Add(ComponentChanged.Removed(comp.NetID!.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return new EntityState(entityUid, changed.ToArray(), compStates.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all entity states that have been modified after and including the provided tick.
|
||||
/// </summary>
|
||||
internal static List<EntityState>? GetAllEntityStates(IEntityManager entityMan, ICommonSession player, GameTick fromTick)
|
||||
{
|
||||
var stateEntities = new List<EntityState>();
|
||||
foreach (var entity in entityMan.GetEntities())
|
||||
{
|
||||
if (entity.Deleted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(entity.Initialized);
|
||||
|
||||
if (entity.LastModifiedTick <= fromTick)
|
||||
continue;
|
||||
|
||||
stateEntities.Add(GetEntityState(entityMan.ComponentManager, player, entity.Uid, fromTick));
|
||||
}
|
||||
|
||||
// no point sending an empty collection
|
||||
return stateEntities.Count == 0 ? default : stateEntities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user