mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* Move to Central Package Management. Allows us to store NuGet package versions all in one place. Yay! * Update NuGet packages and fix code for changes. Notable: Changes to ILVerify. Npgsql doesn't need hacks for inet anymore, now we need hacks to make the old code work with this new reality. NUnit's analyzers are already complaining and I didn't even update it to 4.x yet. TerraFX changed to GetLastSystemError so error handling had to be changed. Buncha APIs have more NRT annotations. * Remove dotnet-eng NuGet package source. I genuinely don't know what this was for, and Central Package Management starts throwing warnings about it, so YEET. * Fix double loading of assemblies due to ALC shenanigans. Due to how the "sideloading" code for the ModLoader was set up, it would first try to load Microsoft.Extensions.Primitives from next to the content dll. But we already have that library in Robust! Chaos ensues. We now try to forcibly prioritize loading from the default ALC first to avoid this. * Remove Robust.Physics project. Never used. * Remove erroneous NVorbis reference. Should be VorbisPizza and otherwise wasn't used. * Sandbox fixes * Remove unused unit test package references. Castle.Core and NUnit.ConsoleRunner. * Update NUnit to 4.0.1 This requires replacing all the old assertion methods because they removed them 🥲 * Mute CA1416 (platform check) errors TerraFX started annotating APIs with this and I can't be arsed to entertain this analyzer so out it goes. * Fine ya cranky, no more CPM for Robust.Client.Injectors * Changelog * Oh so that's what dotnet-eng was used for. Yeah ok that makes sense. * Central package management for remaining 2 robust projects * Ok that was a bad idea let's just use NUnit 3 on the analyzer test project * Oh right forgot to remove this one * Update to a newer version of RemoteExecutor * Disable RemoteExecutor test https://github.com/dotnet/arcade/issues/8483 Yeah this package is not well maintained and clearly we can't rely on it. * Fix immutable list serialization
609 lines
19 KiB
C#
609 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Numerics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Robust.Client.Input;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Shared;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Utility;
|
|
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using TerraFX.Interop.Windows;
|
|
using FrameEventArgs = Robust.Shared.Timing.FrameEventArgs;
|
|
using GL = OpenToolkit.Graphics.OpenGL4.GL;
|
|
|
|
namespace Robust.Client.Graphics.Clyde
|
|
{
|
|
internal partial class Clyde
|
|
{
|
|
private readonly List<WindowReg> _windows = new();
|
|
private readonly List<WindowHandle> _windowHandles = new();
|
|
private readonly Dictionary<int, MonitorHandle> _monitorHandles = new();
|
|
|
|
private int _primaryMonitorId;
|
|
private WindowReg? _mainWindow;
|
|
|
|
private IWindowingImpl? _windowing;
|
|
#pragma warning disable 414
|
|
// Keeping this for if/when we ever get a new renderer.
|
|
private Renderer _chosenRenderer;
|
|
#pragma warning restore 414
|
|
|
|
private ResPath? _windowIconPath;
|
|
private Thread? _windowingThread;
|
|
private bool _vSync;
|
|
private WindowMode _windowMode;
|
|
private WindowReg? _currentHoveredWindow;
|
|
private bool _threadWindowBlit;
|
|
private bool EffectiveThreadWindowBlit => _threadWindowBlit && !_isGLES;
|
|
|
|
public event Action<TextEnteredEventArgs>? TextEntered;
|
|
public event Action<TextEditingEventArgs>? TextEditing;
|
|
public event Action<MouseMoveEventArgs>? MouseMove;
|
|
public event Action<MouseEnterLeaveEventArgs>? MouseEnterLeave;
|
|
public event Action<KeyEventArgs>? KeyUp;
|
|
public event Action<KeyEventArgs>? KeyDown;
|
|
public event Action<MouseWheelEventArgs>? MouseWheel;
|
|
public event Action<WindowRequestClosedEventArgs>? CloseWindow;
|
|
public event Action<WindowDestroyedEventArgs>? DestroyWindow;
|
|
public event Action<WindowContentScaleEventArgs>? OnWindowScaleChanged;
|
|
public event Action<WindowResizedEventArgs>? OnWindowResized;
|
|
public event Action<WindowFocusedEventArgs>? OnWindowFocused;
|
|
|
|
// NOTE: in engine we pretend the framebuffer size is the screen size..
|
|
// For practical reasons like UI rendering.
|
|
public IClydeWindow MainWindow => _mainWindow?.Handle ??
|
|
throw new InvalidOperationException("Windowing is not initialized");
|
|
|
|
public Vector2i ScreenSize => _mainWindow?.FramebufferSize ??
|
|
throw new InvalidOperationException("Windowing is not initialized");
|
|
|
|
public bool IsFocused => _mainWindow?.IsFocused ??
|
|
throw new InvalidOperationException("Windowing is not initialized");
|
|
|
|
public IEnumerable<IClydeWindow> AllWindows => _windowHandles;
|
|
|
|
public Vector2 DefaultWindowScale => _mainWindow?.WindowScale ??
|
|
throw new InvalidOperationException("Windowing is not initialized");
|
|
|
|
public ScreenCoordinates MouseScreenPosition
|
|
{
|
|
get
|
|
{
|
|
var window = _currentHoveredWindow;
|
|
if (window == null)
|
|
return default;
|
|
|
|
return new ScreenCoordinates(window.LastMousePos, window.Id);
|
|
}
|
|
}
|
|
|
|
public uint? GetX11WindowId()
|
|
{
|
|
return _windowing?.WindowGetX11Id(_mainWindow!) ?? null;
|
|
}
|
|
|
|
private bool InitWindowing()
|
|
{
|
|
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngleEs3On10_0))
|
|
{
|
|
Environment.SetEnvironmentVariable("ANGLE_FEATURE_OVERRIDES_ENABLED", "allowES3OnFL10_0");
|
|
}
|
|
|
|
var iconPath = _cfg.GetCVar(CVars.DisplayWindowIconSet);
|
|
if (!string.IsNullOrWhiteSpace(iconPath))
|
|
_windowIconPath = new ResPath(iconPath);
|
|
|
|
_windowingThread = Thread.CurrentThread;
|
|
|
|
var windowingApi = _cfg.GetCVar(CVars.DisplayWindowingApi);
|
|
IWindowingImpl winImpl;
|
|
|
|
switch (windowingApi)
|
|
{
|
|
case "glfw":
|
|
winImpl = new GlfwWindowingImpl(this, _deps);
|
|
break;
|
|
case "sdl2":
|
|
winImpl = new Sdl2WindowingImpl(this, _deps);
|
|
break;
|
|
default:
|
|
_logManager.GetSawmill("clyde.win").Log(
|
|
LogLevel.Error, "Unknown windowing API: {name}. Falling back to GLFW.", windowingApi);
|
|
goto case "glfw";
|
|
}
|
|
|
|
_windowing = winImpl;
|
|
return _windowing.Init();
|
|
}
|
|
|
|
private bool TryInitMainWindow(GLContextSpec? glSpec, [NotNullWhen(false)] out string? error)
|
|
{
|
|
DebugTools.AssertNotNull(_glContext);
|
|
|
|
var width = _cfg.GetCVar(CVars.DisplayWidth);
|
|
var height = _cfg.GetCVar(CVars.DisplayHeight);
|
|
var prevWidth = width;
|
|
var prevHeight = height;
|
|
|
|
IClydeMonitor? monitor = null;
|
|
var fullscreen = false;
|
|
|
|
if (_windowMode == WindowMode.Fullscreen)
|
|
{
|
|
monitor = _monitorHandles[_primaryMonitorId];
|
|
width = monitor.Size.X;
|
|
height = monitor.Size.Y;
|
|
fullscreen = true;
|
|
}
|
|
|
|
var parameters = new WindowCreateParameters
|
|
{
|
|
Width = width,
|
|
Height = height,
|
|
Monitor = monitor,
|
|
Fullscreen = fullscreen
|
|
};
|
|
|
|
var (reg, err) = SharedWindowCreate(glSpec, parameters, null, isMain: true);
|
|
|
|
if (reg == null)
|
|
{
|
|
error = err!;
|
|
return false;
|
|
}
|
|
|
|
DebugTools.Assert(reg.Id == WindowId.Main);
|
|
|
|
if (fullscreen)
|
|
{
|
|
reg.PrevWindowSize = (prevWidth, prevHeight);
|
|
reg.PrevWindowPos = (50, 50);
|
|
}
|
|
|
|
error = null;
|
|
return true;
|
|
}
|
|
|
|
private unsafe bool InitMainWindowAndRenderer()
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
DebugTools.AssertNotNull(_glContext);
|
|
|
|
_chosenRenderer = Renderer.OpenGL;
|
|
|
|
var succeeded = false;
|
|
string? lastError = null;
|
|
|
|
if (_glContext!.RequireWindowGL)
|
|
{
|
|
var specs = _glContext!.SpecsToTry;
|
|
|
|
foreach (var glSpec in specs)
|
|
{
|
|
if (!TryInitMainWindow(glSpec, out lastError))
|
|
{
|
|
Logger.DebugS("clyde.win", $"OpenGL {glSpec.OpenGLVersion} unsupported: {lastError}");
|
|
continue;
|
|
}
|
|
|
|
succeeded = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!TryInitMainWindow(null, out lastError))
|
|
Logger.DebugS("clyde.win", $"Failed to create window: {lastError}");
|
|
else
|
|
succeeded = true;
|
|
}
|
|
|
|
// We should have a main window now.
|
|
DebugTools.AssertNotNull(_mainWindow);
|
|
|
|
// _openGLVersion must be set by _glContext.
|
|
DebugTools.Assert(_openGLVersion != RendererOpenGLVersion.Auto);
|
|
|
|
if (!succeeded)
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
var msgBoxContent = "Failed to create the game window. " +
|
|
"This probably means your GPU is too old to play the game. " +
|
|
"Try to update your graphics drivers, " +
|
|
"or enable compatibility mode in the launcher if that fails.\n" +
|
|
$"The exact error is: {lastError}";
|
|
|
|
fixed (char* pText = msgBoxContent)
|
|
fixed (char* pCaption = "RobustToolbox: Failed to create window")
|
|
{
|
|
Windows.MessageBoxW(HWND.NULL,
|
|
pText,
|
|
pCaption,
|
|
MB.MB_OK | MB.MB_ICONERROR);
|
|
}
|
|
}
|
|
|
|
Logger.FatalS("clyde.win",
|
|
"Failed to create main game window! " +
|
|
"This probably means your GPU is too old to run the game. " +
|
|
$"That or update your graphics drivers. {lastError}");
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!_earlyGLInit)
|
|
InitOpenGL();
|
|
|
|
_sawmillOgl.Debug("Setting viewport and rendering splash...");
|
|
|
|
GL.Viewport(0, 0, ScreenSize.X, ScreenSize.Y);
|
|
CheckGlError();
|
|
|
|
// Quickly do a render with _drawingSplash = true so the screen isn't blank.
|
|
Render();
|
|
|
|
return true;
|
|
}
|
|
|
|
private IEnumerable<Image<Rgba32>> LoadWindowIcons()
|
|
{
|
|
if (OperatingSystem.IsMacOS() || _windowIconPath == null)
|
|
{
|
|
// Does nothing on macOS so don't bother.
|
|
yield break;
|
|
}
|
|
|
|
foreach (var file in _resManager.ContentFindFiles(_windowIconPath))
|
|
{
|
|
if (file.Extension != "png")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
using var stream = _resManager.ContentFileRead(file);
|
|
yield return Image.Load<Rgba32>(stream);
|
|
}
|
|
}
|
|
|
|
private void ShutdownWindowing()
|
|
{
|
|
_windowing?.Shutdown();
|
|
}
|
|
|
|
public void SetWindowTitle(string title)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
DebugTools.AssertNotNull(_mainWindow);
|
|
|
|
_windowing!.WindowSetTitle(_mainWindow!, title);
|
|
}
|
|
|
|
public void SetWindowMonitor(IClydeMonitor monitor)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
DebugTools.AssertNotNull(_mainWindow);
|
|
|
|
_windowing!.WindowSetMonitor(_mainWindow!, monitor);
|
|
}
|
|
|
|
public void RequestWindowAttention()
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
DebugTools.AssertNotNull(_mainWindow);
|
|
|
|
_windowing!.WindowRequestAttention(_mainWindow!);
|
|
}
|
|
|
|
public IClydeWindow CreateWindow(WindowCreateParameters parameters)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
DebugTools.AssertNotNull(_glContext);
|
|
DebugTools.AssertNotNull(_mainWindow);
|
|
|
|
var glSpec = _glContext!.GetNewWindowSpec();
|
|
|
|
_glContext.BeforeSharedWindowCreateUnbind();
|
|
|
|
var (reg, error) = SharedWindowCreate(
|
|
glSpec,
|
|
parameters,
|
|
glSpec == null ? null : _mainWindow,
|
|
isMain: false);
|
|
|
|
// Rebinding is handed by WindowCreated in the GL context.
|
|
|
|
if (error != null)
|
|
throw new Exception(error);
|
|
|
|
return reg!.Handle;
|
|
}
|
|
|
|
private (WindowReg?, string? error) SharedWindowCreate(
|
|
GLContextSpec? glSpec,
|
|
WindowCreateParameters parameters,
|
|
WindowReg? share,
|
|
bool isMain)
|
|
{
|
|
WindowReg? owner = null;
|
|
if (parameters.Owner != null)
|
|
owner = ((WindowHandle)parameters.Owner).Reg;
|
|
|
|
var (reg, error) = _windowing!.WindowCreate(glSpec, parameters, share, owner);
|
|
|
|
if (reg != null)
|
|
{
|
|
// Window init succeeded, do setup.
|
|
reg.IsMainWindow = isMain;
|
|
if (isMain)
|
|
_mainWindow = reg;
|
|
|
|
_windows.Add(reg);
|
|
_windowHandles.Add(reg.Handle);
|
|
|
|
var rtId = AllocRid();
|
|
_renderTargets.Add(rtId, new LoadedRenderTarget
|
|
{
|
|
Size = reg.FramebufferSize,
|
|
IsWindow = true,
|
|
WindowId = reg.Id,
|
|
IsSrgb = true
|
|
});
|
|
|
|
reg.RenderTarget = new RenderWindow(this, rtId);
|
|
|
|
_glContext!.WindowCreated(glSpec, reg);
|
|
}
|
|
|
|
// Pass through result whether successful or not, caller handles it.
|
|
return (reg, error);
|
|
}
|
|
|
|
private void DoDestroyWindow(WindowReg reg)
|
|
{
|
|
if (reg.IsMainWindow)
|
|
throw new InvalidOperationException("Cannot destroy main window.");
|
|
|
|
if (reg.IsDisposed)
|
|
return;
|
|
|
|
reg.IsDisposed = true;
|
|
|
|
_glContext!.WindowDestroyed(reg);
|
|
_windowing!.WindowDestroy(reg);
|
|
|
|
_windows.Remove(reg);
|
|
_windowHandles.Remove(reg.Handle);
|
|
|
|
var destroyed = new WindowDestroyedEventArgs(reg.Handle);
|
|
DestroyWindow?.Invoke(destroyed);
|
|
reg.Closed?.Invoke(destroyed);
|
|
}
|
|
|
|
public void ProcessInput(FrameEventArgs frameEventArgs)
|
|
{
|
|
_windowing?.ProcessEvents();
|
|
DispatchEvents();
|
|
}
|
|
|
|
private void SwapAllBuffers()
|
|
{
|
|
_glContext?.SwapAllBuffers();
|
|
}
|
|
|
|
private void VSyncChanged(bool newValue)
|
|
{
|
|
_vSync = newValue;
|
|
_glContext?.UpdateVSync();
|
|
}
|
|
|
|
private void WindowModeChanged(int mode)
|
|
{
|
|
_windowMode = (WindowMode) mode;
|
|
_windowing?.UpdateMainWindowMode();
|
|
}
|
|
|
|
Task<string> IClipboardManager.GetText()
|
|
{
|
|
return _windowing?.ClipboardGetText(_mainWindow!) ?? Task.FromResult("");
|
|
}
|
|
|
|
void IClipboardManager.SetText(string text)
|
|
{
|
|
_windowing?.ClipboardSetText(_mainWindow!, text);
|
|
}
|
|
|
|
public IEnumerable<IClydeMonitor> EnumerateMonitors()
|
|
{
|
|
return _monitorHandles.Values;
|
|
}
|
|
|
|
public ICursor GetStandardCursor(StandardCursorShape shape)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
return _windowing!.CursorGetStandard(shape);
|
|
}
|
|
|
|
public ICursor CreateCursor(Image<Rgba32> image, Vector2i hotSpot)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
return _windowing!.CursorCreate(image, hotSpot);
|
|
}
|
|
|
|
public void SetCursor(ICursor? cursor)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
_windowing!.CursorSet(_mainWindow!, cursor);
|
|
}
|
|
|
|
|
|
private void SetWindowVisible(WindowReg reg, bool visible)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
_windowing!.WindowSetVisible(reg, visible);
|
|
}
|
|
|
|
public void RunOnWindowThread(Action a)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
_windowing!.RunOnWindowThread(a);
|
|
}
|
|
|
|
public void TextInputSetRect(UIBox2i rect)
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
_windowing!.TextInputSetRect(rect);
|
|
}
|
|
|
|
public void TextInputStart()
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
_windowing!.TextInputStart();
|
|
}
|
|
|
|
public void TextInputStop()
|
|
{
|
|
DebugTools.AssertNotNull(_windowing);
|
|
|
|
_windowing!.TextInputStop();
|
|
}
|
|
|
|
private abstract class WindowReg
|
|
{
|
|
public bool IsDisposed;
|
|
|
|
public WindowId Id;
|
|
public Vector2 WindowScale;
|
|
public Vector2 PixelRatio;
|
|
public Vector2i FramebufferSize;
|
|
public Vector2i WindowSize;
|
|
public Vector2i PrevWindowSize;
|
|
public Vector2i WindowPos;
|
|
public Vector2i PrevWindowPos;
|
|
public Vector2 LastMousePos;
|
|
public bool IsFocused;
|
|
public bool IsMinimized;
|
|
public string Title = "";
|
|
public bool IsVisible;
|
|
public IClydeWindow? Owner;
|
|
|
|
public bool DisposeOnClose;
|
|
|
|
public bool IsMainWindow;
|
|
public WindowHandle Handle = default!;
|
|
public RenderWindow RenderTarget = default!;
|
|
public Action<WindowRequestClosedEventArgs>? RequestClosed;
|
|
public Action<WindowDestroyedEventArgs>? Closed;
|
|
public Action<WindowResizedEventArgs>? Resized;
|
|
}
|
|
|
|
private sealed class WindowHandle : IClydeWindowInternal
|
|
{
|
|
// So funny story
|
|
// When this class was a record, the C# compiler on .NET 5 stack overflowed
|
|
// while compiling the Closed event.
|
|
// VERY funny.
|
|
|
|
private readonly Clyde _clyde;
|
|
public readonly WindowReg Reg;
|
|
|
|
public bool IsDisposed => Reg.IsDisposed;
|
|
public WindowId Id => Reg.Id;
|
|
|
|
public WindowHandle(Clyde clyde, WindowReg reg)
|
|
{
|
|
_clyde = clyde;
|
|
Reg = reg;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_clyde.DoDestroyWindow(Reg);
|
|
}
|
|
|
|
public Vector2i Size => Reg.FramebufferSize;
|
|
|
|
public IRenderTarget RenderTarget => Reg.RenderTarget;
|
|
|
|
public string Title
|
|
{
|
|
get => Reg.Title;
|
|
set => _clyde._windowing!.WindowSetTitle(Reg, value);
|
|
}
|
|
|
|
public bool IsFocused => Reg.IsFocused;
|
|
public bool IsMinimized => Reg.IsMinimized;
|
|
|
|
public bool IsVisible
|
|
{
|
|
get => Reg.IsVisible;
|
|
set => _clyde.SetWindowVisible(Reg, value);
|
|
}
|
|
|
|
public Vector2 ContentScale => Reg.WindowScale;
|
|
|
|
public bool DisposeOnClose
|
|
{
|
|
get => Reg.DisposeOnClose;
|
|
set => Reg.DisposeOnClose = value;
|
|
}
|
|
|
|
public event Action<WindowRequestClosedEventArgs> RequestClosed
|
|
{
|
|
add => Reg.RequestClosed += value;
|
|
remove => Reg.RequestClosed -= value;
|
|
}
|
|
|
|
public event Action<WindowDestroyedEventArgs>? Destroyed
|
|
{
|
|
add => Reg.Closed += value;
|
|
remove => Reg.Closed -= value;
|
|
}
|
|
|
|
public event Action<WindowResizedEventArgs>? Resized
|
|
{
|
|
add => Reg.Resized += value;
|
|
remove => Reg.Resized -= value;
|
|
}
|
|
|
|
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
|
|
}
|
|
|
|
private sealed class MonitorHandle : IClydeMonitor
|
|
{
|
|
public MonitorHandle(int id, string name, Vector2i size, int refreshRate, VideoMode[] videoModes)
|
|
{
|
|
Id = id;
|
|
Name = name;
|
|
Size = size;
|
|
RefreshRate = refreshRate;
|
|
VideoModes = videoModes;
|
|
}
|
|
|
|
public int Id { get; }
|
|
public string Name { get; }
|
|
public Vector2i Size { get; }
|
|
public int RefreshRate { get; }
|
|
public IEnumerable<VideoMode> VideoModes { get; }
|
|
}
|
|
|
|
private abstract class MonitorReg
|
|
{
|
|
public MonitorHandle Handle = default!;
|
|
}
|
|
}
|
|
}
|