mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b215098e4 | ||
|
|
78d01fd25d | ||
|
|
8e8bfbe0cc | ||
|
|
45b0a49ffb | ||
|
|
58638c9109 | ||
|
|
2f2a397ecf | ||
|
|
78c551d854 | ||
|
|
679e07d9ea | ||
|
|
fa5d0235ec | ||
|
|
04d029b9a2 | ||
|
|
6e84821233 | ||
|
|
881cfeb9a9 | ||
|
|
0187e85700 | ||
|
|
dee8a87acd | ||
|
|
d4a7e8f3e0 | ||
|
|
dae6424667 | ||
|
|
3770149cfc | ||
|
|
341c279265 | ||
|
|
37c20723ab | ||
|
|
c3e4a64ad7 | ||
|
|
225446920a | ||
|
|
9b2a50b1a8 | ||
|
|
b4ed513e8c | ||
|
|
c28f2d77c3 | ||
|
|
3e344d00a8 | ||
|
|
c321400347 | ||
|
|
a4ff5d65ec | ||
|
|
97c70124a5 | ||
|
|
cdcc5239ab | ||
|
|
ba2f464249 | ||
|
|
4210f30460 | ||
|
|
9fc95591d9 | ||
|
|
0a59079a4a | ||
|
|
89f168c04d | ||
|
|
f11ac39cd5 | ||
|
|
380de8c4c3 | ||
|
|
539c161ea3 | ||
|
|
b07187459b | ||
|
|
97fb54b6d7 | ||
|
|
fe30d974ca | ||
|
|
884fade25a | ||
|
|
a3ab745121 | ||
|
|
4e0ad23272 | ||
|
|
b9b9cd0711 | ||
|
|
b05b1a7c86 | ||
|
|
4ced901358 | ||
|
|
898d5a3ba5 | ||
|
|
068c05c355 | ||
|
|
cd693875e6 | ||
|
|
e2723a83b3 | ||
|
|
f6ac8fbe1f | ||
|
|
a1e0f18bd6 | ||
|
|
1e7a481911 |
@@ -1,593 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xilium.CefGlue;
|
||||
using static Robust.Client.CEF.CefKeyCodes;
|
||||
using static Robust.Client.CEF.CefKeyCodes.ChromiumKeyboardCode;
|
||||
using static Robust.Client.Input.Keyboard;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
// Funny browser control to integrate in UI.
|
||||
public class BrowserControl : Control, IBrowserControl, IRawInputControl
|
||||
{
|
||||
private const int ScrollSpeed = 50;
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IInputManager _inputMgr = default!;
|
||||
[Dependency] private readonly CefManager _cef = default!;
|
||||
|
||||
private RobustRequestHandler _requestHandler = new RobustRequestHandler(Logger.GetSawmill("root"));
|
||||
private LiveData? _data;
|
||||
private string _startUrl = "about:blank";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Url
|
||||
{
|
||||
get => _data == null ? _startUrl : _data.Browser.GetMainFrame().Url;
|
||||
set
|
||||
{
|
||||
if (_data == null)
|
||||
_startUrl = value;
|
||||
else
|
||||
_data.Browser.GetMainFrame().LoadUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public bool IsLoading => _data?.Browser.IsLoading ?? false;
|
||||
|
||||
private readonly Dictionary<Key, ChromiumKeyboardCode> _keyMap = new()
|
||||
{
|
||||
[Key.A] = VKEY_A,
|
||||
[Key.B] = VKEY_B,
|
||||
[Key.C] = VKEY_C,
|
||||
[Key.D] = VKEY_D,
|
||||
[Key.E] = VKEY_E,
|
||||
[Key.F] = VKEY_F,
|
||||
[Key.G] = VKEY_G,
|
||||
[Key.H] = VKEY_H,
|
||||
[Key.I] = VKEY_I,
|
||||
[Key.J] = VKEY_J,
|
||||
[Key.K] = VKEY_K,
|
||||
[Key.L] = VKEY_L,
|
||||
[Key.M] = VKEY_M,
|
||||
[Key.N] = VKEY_N,
|
||||
[Key.O] = VKEY_O,
|
||||
[Key.P] = VKEY_P,
|
||||
[Key.Q] = VKEY_Q,
|
||||
[Key.R] = VKEY_R,
|
||||
[Key.S] = VKEY_S,
|
||||
[Key.T] = VKEY_T,
|
||||
[Key.U] = VKEY_U,
|
||||
[Key.V] = VKEY_V,
|
||||
[Key.W] = VKEY_W,
|
||||
[Key.X] = VKEY_X,
|
||||
[Key.Y] = VKEY_Y,
|
||||
[Key.Z] = VKEY_Z,
|
||||
[Key.Num0] = VKEY_0,
|
||||
[Key.Num1] = VKEY_1,
|
||||
[Key.Num2] = VKEY_2,
|
||||
[Key.Num3] = VKEY_3,
|
||||
[Key.Num4] = VKEY_4,
|
||||
[Key.Num5] = VKEY_5,
|
||||
[Key.Num6] = VKEY_6,
|
||||
[Key.Num7] = VKEY_7,
|
||||
[Key.Num8] = VKEY_8,
|
||||
[Key.Num9] = VKEY_9,
|
||||
[Key.NumpadNum0] = VKEY_NUMPAD0,
|
||||
[Key.NumpadNum1] = VKEY_NUMPAD1,
|
||||
[Key.NumpadNum2] = VKEY_NUMPAD2,
|
||||
[Key.NumpadNum3] = VKEY_NUMPAD3,
|
||||
[Key.NumpadNum4] = VKEY_NUMPAD4,
|
||||
[Key.NumpadNum5] = VKEY_NUMPAD5,
|
||||
[Key.NumpadNum6] = VKEY_NUMPAD6,
|
||||
[Key.NumpadNum7] = VKEY_NUMPAD7,
|
||||
[Key.NumpadNum8] = VKEY_NUMPAD8,
|
||||
[Key.NumpadNum9] = VKEY_NUMPAD9,
|
||||
[Key.Escape] = VKEY_ESCAPE,
|
||||
[Key.Control] = VKEY_CONTROL,
|
||||
[Key.Shift] = VKEY_SHIFT,
|
||||
[Key.Alt] = VKEY_MENU,
|
||||
[Key.LSystem] = VKEY_LWIN,
|
||||
[Key.RSystem] = VKEY_RWIN,
|
||||
[Key.LBracket] = VKEY_OEM_4,
|
||||
[Key.RBracket] = VKEY_OEM_6,
|
||||
[Key.SemiColon] = VKEY_OEM_1,
|
||||
[Key.Comma] = VKEY_OEM_COMMA,
|
||||
[Key.Period] = VKEY_OEM_PERIOD,
|
||||
[Key.Apostrophe] = VKEY_OEM_7,
|
||||
[Key.Slash] = VKEY_OEM_2,
|
||||
[Key.BackSlash] = VKEY_OEM_5,
|
||||
[Key.Tilde] = VKEY_OEM_3,
|
||||
[Key.Equal] = VKEY_OEM_PLUS,
|
||||
[Key.Space] = VKEY_SPACE,
|
||||
[Key.Return] = VKEY_RETURN,
|
||||
[Key.BackSpace] = VKEY_BACK,
|
||||
[Key.Tab] = VKEY_TAB,
|
||||
[Key.PageUp] = VKEY_PRIOR,
|
||||
[Key.PageDown] = VKEY_NEXT,
|
||||
[Key.End] = VKEY_END,
|
||||
[Key.Home] = VKEY_HOME,
|
||||
[Key.Insert] = VKEY_INSERT,
|
||||
[Key.Delete] = VKEY_DELETE,
|
||||
[Key.Minus] = VKEY_OEM_MINUS,
|
||||
[Key.NumpadAdd] = VKEY_ADD,
|
||||
[Key.NumpadSubtract] = VKEY_SUBTRACT,
|
||||
[Key.NumpadDivide] = VKEY_DIVIDE,
|
||||
[Key.NumpadMultiply] = VKEY_MULTIPLY,
|
||||
[Key.NumpadDecimal] = VKEY_DECIMAL,
|
||||
[Key.Left] = VKEY_LEFT,
|
||||
[Key.Right] = VKEY_RIGHT,
|
||||
[Key.Up] = VKEY_UP,
|
||||
[Key.Down] = VKEY_DOWN,
|
||||
[Key.F1] = VKEY_F1,
|
||||
[Key.F2] = VKEY_F2,
|
||||
[Key.F3] = VKEY_F3,
|
||||
[Key.F4] = VKEY_F4,
|
||||
[Key.F5] = VKEY_F5,
|
||||
[Key.F6] = VKEY_F6,
|
||||
[Key.F7] = VKEY_F7,
|
||||
[Key.F8] = VKEY_F8,
|
||||
[Key.F9] = VKEY_F9,
|
||||
[Key.F10] = VKEY_F10,
|
||||
[Key.F11] = VKEY_F11,
|
||||
[Key.F12] = VKEY_F12,
|
||||
[Key.F13] = VKEY_F13,
|
||||
[Key.F14] = VKEY_F14,
|
||||
[Key.F15] = VKEY_F15,
|
||||
[Key.Pause] = VKEY_PAUSE,
|
||||
};
|
||||
|
||||
public BrowserControl()
|
||||
{
|
||||
CanKeyboardFocus = true;
|
||||
KeyboardFocusOnClick = true;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_cef.CheckInitialized();
|
||||
|
||||
DebugTools.AssertNull(_data);
|
||||
|
||||
// A funny render handler that will allow us to render to the control.
|
||||
var renderer = new ControlRenderHandler(this);
|
||||
|
||||
// A funny web cef client. This can actually be shared by multiple browsers, but I'm not sure how the
|
||||
// rendering would work in that case? TODO CEF: Investigate a way to share the web client?
|
||||
var client = new RobustCefClient(renderer, _requestHandler, new RobustLoadHandler());
|
||||
|
||||
var info = CefWindowInfo.Create();
|
||||
|
||||
// FUNFACT: If you DO NOT set these below and set info.Width/info.Height instead, you get an external window
|
||||
// Good to know, huh? Setup is the same, except you can pass a dummy render handler to the CEF client.
|
||||
info.SetAsWindowless(IntPtr.Zero, false); // TODO CEF: Pass parent handle?
|
||||
info.WindowlessRenderingEnabled = true;
|
||||
|
||||
var settings = new CefBrowserSettings()
|
||||
{
|
||||
WindowlessFrameRate = 60
|
||||
};
|
||||
|
||||
// Create the web browser! And by default, we go to about:blank.
|
||||
var browser = CefBrowserHost.CreateBrowserSync(info, client, settings, _startUrl);
|
||||
|
||||
var texture = _clyde.CreateBlankTexture<Bgra32>(Vector2i.One);
|
||||
|
||||
_data = new LiveData(texture, client, browser, renderer);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
DebugTools.AssertNotNull(_data);
|
||||
|
||||
_data!.Texture.Dispose();
|
||||
_data.Browser.GetHost().CloseBrowser(true);
|
||||
_data = null;
|
||||
}
|
||||
|
||||
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
// Logger.Debug();
|
||||
var modifiers = CalcMouseModifiers();
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
(int) args.RelativePosition.X, (int) args.RelativePosition.Y,
|
||||
modifiers);
|
||||
|
||||
_data.Browser.GetHost().SendMouseMoveEvent(mouseEvent, false);
|
||||
}
|
||||
|
||||
protected internal override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var modifiers = CalcMouseModifiers();
|
||||
|
||||
_data.Browser.GetHost().SendMouseMoveEvent(new CefMouseEvent(0, 0, modifiers), true);
|
||||
}
|
||||
|
||||
protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
base.MouseWheel(args);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var modifiers = CalcMouseModifiers();
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
(int) args.RelativePosition.X, (int) args.RelativePosition.Y,
|
||||
modifiers);
|
||||
|
||||
_data.Browser.GetHost().SendMouseWheelEvent(
|
||||
mouseEvent,
|
||||
(int) args.Delta.X * ScrollSpeed,
|
||||
(int) args.Delta.Y * ScrollSpeed);
|
||||
}
|
||||
|
||||
bool IRawInputControl.RawKeyEvent(in GuiRawKeyEvent guiRawEvent)
|
||||
{
|
||||
if (_data == null)
|
||||
return false;
|
||||
|
||||
var host = _data.Browser.GetHost();
|
||||
|
||||
if (guiRawEvent.Key is Key.MouseLeft or Key.MouseMiddle or Key.MouseRight)
|
||||
{
|
||||
var key = guiRawEvent.Key switch
|
||||
{
|
||||
Key.MouseLeft => CefMouseButtonType.Left,
|
||||
Key.MouseMiddle => CefMouseButtonType.Middle,
|
||||
Key.MouseRight => CefMouseButtonType.Right,
|
||||
_ => default // not possible
|
||||
};
|
||||
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
guiRawEvent.MouseRelative.X, guiRawEvent.MouseRelative.Y,
|
||||
CefEventFlags.None);
|
||||
|
||||
// Logger.Debug($"MOUSE: {guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {key}");
|
||||
|
||||
// TODO: double click support?
|
||||
host.SendMouseClickEvent(mouseEvent, key, guiRawEvent.Action == RawKeyAction.Up, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Handle left/right modifier keys??
|
||||
if (!_keyMap.TryGetValue(guiRawEvent.Key, out var vkKey))
|
||||
vkKey = default;
|
||||
|
||||
// Logger.Debug($"{guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {vkKey}");
|
||||
|
||||
var lParam = 0;
|
||||
lParam |= (guiRawEvent.ScanCode & 0xFF) << 16;
|
||||
if (guiRawEvent.Action != RawKeyAction.Down)
|
||||
lParam |= 1 << 30;
|
||||
|
||||
if (guiRawEvent.Action == RawKeyAction.Up)
|
||||
lParam |= 1 << 31;
|
||||
|
||||
var modifiers = CalcModifiers(guiRawEvent.Key);
|
||||
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
// Repeats are sent as key downs, I guess?
|
||||
EventType = guiRawEvent.Action == RawKeyAction.Up
|
||||
? CefKeyEventType.KeyUp
|
||||
: CefKeyEventType.RawKeyDown,
|
||||
NativeKeyCode = lParam,
|
||||
// NativeKeyCode = guiRawEvent.ScanCode,
|
||||
WindowsKeyCode = (int) vkKey,
|
||||
IsSystemKey = false, // TODO
|
||||
Modifiers = modifiers
|
||||
});
|
||||
|
||||
if (guiRawEvent.Action != RawKeyAction.Up && guiRawEvent.Key == Key.Return)
|
||||
{
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
EventType = CefKeyEventType.Char,
|
||||
WindowsKeyCode = '\r',
|
||||
NativeKeyCode = lParam,
|
||||
Modifiers = modifiers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private CefEventFlags CalcModifiers(Key key)
|
||||
{
|
||||
CefEventFlags modifiers = default;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Control))
|
||||
modifiers |= CefEventFlags.ControlDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Alt))
|
||||
modifiers |= CefEventFlags.AltDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
private CefEventFlags CalcMouseModifiers()
|
||||
{
|
||||
CefEventFlags modifiers = default;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Control))
|
||||
modifiers |= CefEventFlags.ControlDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Alt))
|
||||
modifiers |= CefEventFlags.AltDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseLeft))
|
||||
modifiers |= CefEventFlags.LeftMouseButton;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseMiddle))
|
||||
modifiers |= CefEventFlags.MiddleMouseButton;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseRight))
|
||||
modifiers |= CefEventFlags.RightMouseButton;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
protected internal override void TextEntered(GUITextEventArgs args)
|
||||
{
|
||||
base.TextEntered(args);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var host = _data.Browser.GetHost();
|
||||
|
||||
Span<char> buf = stackalloc char[2];
|
||||
var written = args.AsRune.EncodeToUtf16(buf);
|
||||
|
||||
for (var i = 0; i < written; i++)
|
||||
{
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
EventType = CefKeyEventType.Char,
|
||||
WindowsKeyCode = buf[i],
|
||||
Character = buf[i],
|
||||
UnmodifiedCharacter = buf[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
_data.Browser.GetHost().NotifyMoveOrResizeStarted();
|
||||
_data.Browser.GetHost().WasResized();
|
||||
_data.Texture.Dispose();
|
||||
_data.Texture = _clyde.CreateBlankTexture<Bgra32>((PixelWidth, PixelHeight));
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var bufImg = _data.Renderer.Buffer.Buffer;
|
||||
|
||||
_data.Texture.SetSubImage(
|
||||
Vector2i.Zero,
|
||||
bufImg,
|
||||
new UIBox2i(
|
||||
0, 0,
|
||||
Math.Min(PixelWidth, bufImg.Width),
|
||||
Math.Min(PixelHeight, bufImg.Height)));
|
||||
|
||||
handle.DrawTexture(_data.Texture, Vector2.Zero);
|
||||
}
|
||||
|
||||
public void StopLoad()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.StopLoad();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.Reload();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!_data.Browser.CanGoBack)
|
||||
return false;
|
||||
|
||||
_data.Browser.GoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!_data.Browser.CanGoForward)
|
||||
return false;
|
||||
|
||||
_data.Browser.GoForward();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ExecuteJavaScript(string code)
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
// TODO: this should not run until the browser is done loading seriously does this even work?
|
||||
_data.Browser.GetMainFrame().ExecuteJavaScript(code, string.Empty, 1);
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
_requestHandler.AddResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
{
|
||||
_requestHandler.RemoveResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void AddBeforeBrowseHandler(Action<BeforeBrowseContext> handler)
|
||||
{
|
||||
_requestHandler.AddBeforeBrowseHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveBeforeBrowseHandler(Action<BeforeBrowseContext> handler)
|
||||
{
|
||||
_requestHandler.RemoveBeforeBrowseHandler(handler);
|
||||
}
|
||||
|
||||
private sealed class LiveData
|
||||
{
|
||||
public OwnedTexture Texture;
|
||||
public readonly RobustCefClient Client;
|
||||
public readonly CefBrowser Browser;
|
||||
public readonly ControlRenderHandler Renderer;
|
||||
|
||||
public LiveData(
|
||||
OwnedTexture texture,
|
||||
RobustCefClient client,
|
||||
CefBrowser browser,
|
||||
ControlRenderHandler renderer)
|
||||
{
|
||||
Texture = texture;
|
||||
Client = client;
|
||||
Browser = browser;
|
||||
Renderer = renderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ControlRenderHandler : CefRenderHandler
|
||||
{
|
||||
public ImageBuffer Buffer { get; }
|
||||
private Control _control;
|
||||
|
||||
internal ControlRenderHandler(Control control)
|
||||
{
|
||||
Buffer = new ImageBuffer(control);
|
||||
_control = control;
|
||||
}
|
||||
|
||||
protected override CefAccessibilityHandler? GetAccessibilityHandler() => null;
|
||||
|
||||
protected override void GetViewRect(CefBrowser browser, out CefRectangle rect)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
{
|
||||
rect = new CefRectangle();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO CEF: Do we need to pass real screen coords? Cause what we do already works...
|
||||
//var screenCoords = _control.ScreenCoordinates;
|
||||
//rect = new CefRectangle((int) screenCoords.X, (int) screenCoords.Y, (int)Math.Max(_control.Size.X, 1), (int)Math.Max(_control.Size.Y, 1));
|
||||
|
||||
// We do the max between size and 1 because it will LITERALLY CRASH WITHOUT AN ERROR otherwise.
|
||||
rect = new CefRectangle(0, 0, (int) Math.Max(_control.Size.X, 1), (int) Math.Max(_control.Size.Y, 1));
|
||||
}
|
||||
|
||||
protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return false;
|
||||
|
||||
// TODO CEF: Get actual scale factor?
|
||||
screenInfo.DeviceScaleFactor = 1.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects,
|
||||
IntPtr buffer, int width, int height)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
|
||||
foreach (var dirtyRect in dirtyRects)
|
||||
{
|
||||
Buffer.UpdateBuffer(width, height, buffer, dirtyRect);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects, IntPtr sharedHandle)
|
||||
{
|
||||
// Unused, but we're forced to implement it so.. NOOP.
|
||||
}
|
||||
|
||||
protected override void OnScrollOffsetChanged(CefBrowser browser, double x, double y)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectedRange,
|
||||
CefRectangle[] characterBounds)
|
||||
{
|
||||
if (_control.Disposed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
// The library we're using right now. TODO CEF: Do we want to use something else? We will need to ship it ourselves if so.
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
// Register this with IoC.
|
||||
// TODO CEF: think if making this inherit CefApp is a good idea...
|
||||
// TODO CEF: A way to handle external window browsers...
|
||||
[UsedImplicitly]
|
||||
public partial class CefManager
|
||||
{
|
||||
private CefApp _app = default!;
|
||||
private bool _initialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Call this to initialize CEF.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
DebugTools.Assert(!_initialized);
|
||||
|
||||
string subProcessName;
|
||||
if (OperatingSystem.IsWindows())
|
||||
subProcessName = "Robust.Client.CEF.exe";
|
||||
else if (OperatingSystem.IsLinux())
|
||||
subProcessName = "Robust.Client.CEF";
|
||||
else
|
||||
throw new NotSupportedException("Unsupported platform for CEF!");
|
||||
|
||||
var subProcessPath = PathHelpers.ExecutableRelativeFile(subProcessName);
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
|
||||
ExternalMessagePump = false, // Unsure, honestly. TODO CEF: Research this?
|
||||
NoSandbox = true, // Not disabling the sandbox crashes CEF.
|
||||
BrowserSubprocessPath = subProcessPath,
|
||||
LocalesDirPath = Path.Combine(PathHelpers.GetExecutableDirectory(), "locales"),
|
||||
ResourcesDirPath = PathHelpers.GetExecutableDirectory(),
|
||||
RemoteDebuggingPort = 9222
|
||||
};
|
||||
|
||||
Logger.Info($"CEF Version: {CefRuntime.ChromeVersion}");
|
||||
|
||||
// --------------------------- README --------------------------------------------------
|
||||
// By the way! You're gonna need the CEF binaries in your client's bin folder.
|
||||
// More specifically, version cef_binary_94.4.1+g4b61a8c+chromium-94.0.4606.54
|
||||
// Windows: https://cef-builds.spotifycdn.com/cef_binary_94.4.1%2Bg4b61a8c%2Bchromium-94.0.4606.54_windows64_minimal.tar.bz2
|
||||
// Linux: https://cef-builds.spotifycdn.com/cef_binary_94.4.1%2Bg4b61a8c%2Bchromium-94.0.4606.54_linux64_minimal.tar.bz2
|
||||
// Here's how to get it to work:
|
||||
// 1. Copy all the contents of "Release" to the bin/Content.Client folder.
|
||||
// 2. Copy all the contents of "Resources" to the bin/Content.Client folder.
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
_app = new RobustCefApp();
|
||||
|
||||
// We pass no main arguments...
|
||||
CefRuntime.Initialize(new CefMainArgs(null), settings, _app, IntPtr.Zero);
|
||||
|
||||
// TODO CEF: After this point, debugging breaks. No, literally. My client crashes but ONLY with the debugger.
|
||||
// I have tried using the DEBUG and RELEASE versions of libcef.so, stripped or non-stripped...
|
||||
// And nothing seemed to work. Odd.
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void CheckInitialized()
|
||||
{
|
||||
if (!_initialized)
|
||||
throw new InvalidOperationException("CefManager has not been initialized!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needs to be called regularly for CEF to keep working.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
DebugTools.Assert(_initialized);
|
||||
|
||||
// Calling this makes CEF do its work, without using its own update loop.
|
||||
CefRuntime.DoMessageLoopWork();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call before program shutdown.
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
DebugTools.Assert(_initialized);
|
||||
|
||||
CefRuntime.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public interface IBrowserWindow : IBrowserControl, IDisposable
|
||||
{
|
||||
bool Closed { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
// This was supposed to be the main entry for the subprocess program... It doesn't work.
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// This is a workaround for this to work on UNIX.
|
||||
var argv = args;
|
||||
if (CefRuntime.Platform != CefRuntimePlatform.Windows)
|
||||
{
|
||||
argv = new string[args.Length + 1];
|
||||
Array.Copy(args, 0, argv, 1, args.Length);
|
||||
argv[0] = "-";
|
||||
}
|
||||
|
||||
var mainArgs = new CefMainArgs(argv);
|
||||
|
||||
// This will block executing until the subprocess is shut down.
|
||||
var code = CefRuntime.ExecuteProcess(mainArgs, null, IntPtr.Zero);
|
||||
|
||||
if (code != 0)
|
||||
{
|
||||
System.Console.WriteLine($"CEF Subprocess exited unsuccessfully with exit code {code}! Arguments: {string.Join(' ', argv)}");
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
|
||||
<Target Name="RobustAfterBuild" AfterTargets="Build" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\cefglue\CefGlue\CefGlue.csproj" />
|
||||
<ProjectReference Include="..\Robust.Client\Robust.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -13,5 +13,6 @@
|
||||
<Compile Link="XamlX\filename" Include="../XamlX/src/XamlX/**/*.cs" />
|
||||
<Compile Remove="../XamlX/src/XamlX/**/SreTypeSystem.cs" />
|
||||
<Compile Remove="../XamlX/src/XamlX/obj/**" />
|
||||
<Compile Include="..\Robust.Client\UserInterface\ControlPropertyAccess.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Robust.Client.UserInterface;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
@@ -37,10 +38,10 @@ namespace Robust.Client.AutoGenerated
|
||||
|
||||
class NameVisitor : IXamlAstVisitor
|
||||
{
|
||||
private readonly List<(string name, string type, string access)> _names =
|
||||
new List<(string name, string type, string access)>();
|
||||
private readonly List<(string name, string type, AccessLevel access)> _names =
|
||||
new List<(string name, string type, AccessLevel access)>();
|
||||
|
||||
public static List<(string name, string type, string access)> GetNames(IXamlAstNode node)
|
||||
public static List<(string name, string type, AccessLevel access)> GetNames(IXamlAstNode node)
|
||||
{
|
||||
var visitor = new NameVisitor();
|
||||
node.Visit(visitor);
|
||||
@@ -88,7 +89,7 @@ namespace Robust.Client.AutoGenerated
|
||||
|
||||
var reg = (nameText.Text,
|
||||
$@"{clrtype.Namespace}.{clrtype.Name}",
|
||||
accessText?.Text ?? "Protected");
|
||||
accessText != null ? (AccessLevel) Enum.Parse(typeof(AccessLevel), accessText.Text) : AccessLevel.Protected);
|
||||
if (!_names.Contains(reg))
|
||||
{
|
||||
_names.Add(reg);
|
||||
@@ -137,36 +138,38 @@ namespace Robust.Client.AutoGenerated
|
||||
|
||||
var namedControls = names.Select(info =>
|
||||
{
|
||||
(string name, string type, string access) = info;
|
||||
(string name, string type, AccessLevel access) = info;
|
||||
|
||||
string accessStr;
|
||||
switch (access)
|
||||
{
|
||||
case "Public":
|
||||
access = "public";
|
||||
case AccessLevel.Public:
|
||||
accessStr = "public";
|
||||
break;
|
||||
case "Protected" when classSymbol.IsSealed:
|
||||
case "PrivateProtected" when classSymbol.IsSealed:
|
||||
case "Private":
|
||||
access = "private";
|
||||
case AccessLevel.Protected when classSymbol.IsSealed:
|
||||
case AccessLevel.PrivateProtected when classSymbol.IsSealed:
|
||||
case AccessLevel.Private:
|
||||
accessStr = "private";
|
||||
break;
|
||||
case "Protected":
|
||||
access = "protected";
|
||||
case AccessLevel.Protected:
|
||||
accessStr = "protected";
|
||||
break;
|
||||
case "Internal":
|
||||
case "ProtectedInternal" when classSymbol.IsSealed:
|
||||
access = "internal";
|
||||
case AccessLevel.PrivateProtected:
|
||||
accessStr = "private protected";
|
||||
break;
|
||||
case "ProtectedInternal":
|
||||
access = "protected internal";
|
||||
case AccessLevel.Internal:
|
||||
case AccessLevel.ProtectedInternal when classSymbol.IsSealed:
|
||||
accessStr = "internal";
|
||||
break;
|
||||
case "PrivateProtected":
|
||||
access = "private protected";
|
||||
case AccessLevel.ProtectedInternal:
|
||||
accessStr = "protected internal";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Invalid access level for control {name} in {fileName}: {access}.");
|
||||
throw new ArgumentException($"Invalid access level \"{Enum.GetName(typeof(AccessLevel), access)}\" " +
|
||||
$"for control {name} in file {fileName}.");
|
||||
}
|
||||
|
||||
return $" {access} global::{type} {name} => this.FindControl<global::{type}>(\"{name}\");";
|
||||
return $" {accessStr} global::{type} {name} => this.FindControl<global::{type}>(\"{name}\");";
|
||||
});
|
||||
|
||||
return $@"// <auto-generated />
|
||||
@@ -182,7 +185,6 @@ namespace {nameSpace}
|
||||
";
|
||||
}
|
||||
|
||||
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
var comp = (CSharpCompilation) context.Compilation;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
public sealed class BrowserWindowCreateParameters
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public sealed class BeforeBrowseContext
|
||||
internal sealed class CefBeforeBrowseContext : IBeforeBrowseContext
|
||||
{
|
||||
internal readonly CefRequest CefRequest;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Robust.Client.CEF
|
||||
|
||||
public bool IsCancelled { get; private set; }
|
||||
|
||||
internal BeforeBrowseContext(
|
||||
internal CefBeforeBrowseContext(
|
||||
bool isRedirect,
|
||||
bool userGesture,
|
||||
CefRequest cefRequest)
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
@@ -3,9 +3,9 @@ using System.IO;
|
||||
using System.Net;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public sealed class RequestHandlerContext
|
||||
internal sealed class CefRequestHandlerContext : IRequestHandlerContext
|
||||
{
|
||||
internal readonly CefRequest CefRequest;
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Robust.Client.CEF
|
||||
|
||||
internal IRequestResult? Result { get; private set; }
|
||||
|
||||
internal RequestHandlerContext(
|
||||
internal CefRequestHandlerContext(
|
||||
bool isNavigation,
|
||||
bool isDownload,
|
||||
string requestInitiator,
|
||||
@@ -1,22 +1,14 @@
|
||||
using System;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal sealed class ImageBuffer
|
||||
{
|
||||
private readonly Control _control;
|
||||
|
||||
public ImageBuffer(Control control)
|
||||
{
|
||||
_control = control;
|
||||
}
|
||||
|
||||
public Image<Bgra32> Buffer { get; private set; } = new(1, 1);
|
||||
|
||||
public unsafe void UpdateBuffer(int width, int height, IntPtr buffer, CefRectangle dirtyRect)
|
||||
88
Robust.Client.WebView/Cef/Program.cs
Normal file
88
Robust.Client.WebView/Cef/Program.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
// This was supposed to be the main entry for the subprocess program... It doesn't work.
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// This is a workaround for this to work on UNIX.
|
||||
var argv = args;
|
||||
if (CefRuntime.Platform != CefRuntimePlatform.Windows)
|
||||
{
|
||||
argv = new string[args.Length + 1];
|
||||
Array.Copy(args, 0, argv, 1, args.Length);
|
||||
argv[0] = "-";
|
||||
}
|
||||
/*
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// Chromium tries to load libEGL.so and libGLESv2.so relative to the process executable on Linux.
|
||||
// (Compared to Windows where it is relative to Chromium's *module*)
|
||||
// There is a TODO "is this correct?" in the Chromium code for this.
|
||||
// Great.
|
||||
|
||||
//CopyDllToExecutableDir("libEGL.so");
|
||||
//CopyDllToExecutableDir("libGLESv2.so");
|
||||
|
||||
// System.Threading.Thread.Sleep(200000);
|
||||
}
|
||||
*/
|
||||
var mainArgs = new CefMainArgs(argv);
|
||||
|
||||
// This will block executing until the subprocess is shut down.
|
||||
var code = CefRuntime.ExecuteProcess(mainArgs, null, IntPtr.Zero);
|
||||
|
||||
if (code != 0)
|
||||
{
|
||||
System.Console.WriteLine($"CEF Subprocess exited unsuccessfully with exit code {code}! Arguments: {string.Join(' ', argv)}");
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/* private static void CopyDllToExecutableDir(string dllName)
|
||||
{
|
||||
var executableDir = PathHelpers.GetExecutableDirectory();
|
||||
var targetPath = Path.Combine(executableDir, dllName);
|
||||
if (File.Exists(targetPath))
|
||||
return;
|
||||
|
||||
// Find source file.
|
||||
string? srcFile = null;
|
||||
foreach (var searchDir in WebViewManagerCef.NativeDllSearchDirectories())
|
||||
{
|
||||
var searchPath = Path.Combine(searchDir, dllName);
|
||||
if (File.Exists(searchPath))
|
||||
{
|
||||
srcFile = searchPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (srcFile == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(targetPath))
|
||||
return;
|
||||
|
||||
File.Copy(srcFile, targetPath);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Catching race condition lock errors and stuff I guess.
|
||||
}
|
||||
}
|
||||
} */
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.IO;
|
||||
using System.Net;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal interface IRequestResult
|
||||
{
|
||||
@@ -88,6 +88,13 @@ namespace Robust.Client.CEF
|
||||
protected override void Cancel()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_stream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal class RobustCefApp : CefApp
|
||||
{
|
||||
@@ -24,6 +24,13 @@ namespace Robust.Client.CEF
|
||||
{
|
||||
// Disable zygote on Linux.
|
||||
commandLine.AppendSwitch("--no-zygote");
|
||||
|
||||
// Work around https://bitbucket.org/chromiumembedded/cef/issues/3213/ozone-egl-initialization-does-not-have
|
||||
// Desktop GL force makes Chromium not try to load its own ANGLE/Swiftshader so load paths aren't problematic.
|
||||
if (OperatingSystem.IsLinux())
|
||||
commandLine.AppendSwitch("--use-gl", "desktop");
|
||||
|
||||
// commandLine.AppendSwitch("--single-process");
|
||||
|
||||
//commandLine.AppendSwitch("--disable-gpu");
|
||||
//commandLine.AppendSwitch("--disable-gpu-compositing");
|
||||
@@ -1,6 +1,6 @@
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
// Simple CEF client.
|
||||
internal class RobustCefClient : CefClient
|
||||
@@ -1,6 +1,6 @@
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public sealed class RobustLoadHandler : CefLoadHandler
|
||||
{
|
||||
@@ -3,20 +3,20 @@ using System.Collections.Generic;
|
||||
using Robust.Shared.Log;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal sealed class RobustRequestHandler : CefRequestHandler
|
||||
{
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly List<Action<RequestHandlerContext>> _resourceRequestHandlers = new();
|
||||
private readonly List<Action<BeforeBrowseContext>> _beforeBrowseHandlers = new();
|
||||
private readonly List<Action<IRequestHandlerContext>> _resourceRequestHandlers = new();
|
||||
private readonly List<Action<IBeforeBrowseContext>> _beforeBrowseHandlers = new();
|
||||
|
||||
public RobustRequestHandler(ISawmill sawmill)
|
||||
{
|
||||
_sawmill = sawmill;
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
public void AddResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
lock (_resourceRequestHandlers)
|
||||
{
|
||||
@@ -24,7 +24,7 @@ namespace Robust.Client.CEF
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
public void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
lock (_resourceRequestHandlers)
|
||||
{
|
||||
@@ -32,7 +32,7 @@ namespace Robust.Client.CEF
|
||||
}
|
||||
}
|
||||
|
||||
public void AddBeforeBrowseHandler(Action<BeforeBrowseContext> handler)
|
||||
public void AddBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
lock (_beforeBrowseHandlers)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace Robust.Client.CEF
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveBeforeBrowseHandler(Action<BeforeBrowseContext> handler)
|
||||
public void RemoveBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
lock (_beforeBrowseHandlers)
|
||||
{
|
||||
@@ -61,7 +61,7 @@ namespace Robust.Client.CEF
|
||||
{
|
||||
_sawmill.Debug($"HANDLING REQUEST: {request.Url}");
|
||||
|
||||
var context = new RequestHandlerContext(isNavigation, isDownload, requestInitiator, request);
|
||||
var context = new CefRequestHandlerContext(isNavigation, isDownload, requestInitiator, request);
|
||||
|
||||
foreach (var handler in _resourceRequestHandlers)
|
||||
{
|
||||
@@ -85,7 +85,7 @@ namespace Robust.Client.CEF
|
||||
{
|
||||
lock (_beforeBrowseHandlers)
|
||||
{
|
||||
var context = new BeforeBrowseContext(isRedirect, userGesture, request);
|
||||
var context = new CefBeforeBrowseContext(isRedirect, userGesture, request);
|
||||
|
||||
foreach (var handler in _beforeBrowseHandlers)
|
||||
{
|
||||
@@ -6,26 +6,25 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public partial class CefManager
|
||||
internal partial class WebViewManagerCef
|
||||
{
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
|
||||
private readonly List<BrowserWindowImpl> _browserWindows = new();
|
||||
private readonly List<WebViewWindowImpl> _browserWindows = new();
|
||||
|
||||
public IEnumerable<IBrowserWindow> AllBrowserWindows => _browserWindows;
|
||||
public IEnumerable<IWebViewWindow> AllBrowserWindows => _browserWindows;
|
||||
|
||||
public IBrowserWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams)
|
||||
public IWebViewWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams)
|
||||
{
|
||||
var mainHWnd = (_clyde.MainWindow as IClydeWindowInternal)?.WindowsHWnd ?? 0;
|
||||
|
||||
var info = CefWindowInfo.Create();
|
||||
info.Width = createParams.Width;
|
||||
info.Height = createParams.Height;
|
||||
info.Bounds = new CefRectangle(0, 0, createParams.Width, createParams.Height);
|
||||
info.SetAsPopup(mainHWnd, "ss14cef");
|
||||
|
||||
var impl = new BrowserWindowImpl(this);
|
||||
var impl = new WebViewWindowImpl(this);
|
||||
|
||||
var lifeSpanHandler = new WindowLifeSpanHandler(impl);
|
||||
var reqHandler = new RobustRequestHandler(Logger.GetSawmill("root"));
|
||||
@@ -40,13 +39,13 @@ namespace Robust.Client.CEF
|
||||
return impl;
|
||||
}
|
||||
|
||||
private sealed class BrowserWindowImpl : IBrowserWindow
|
||||
private sealed class WebViewWindowImpl : IWebViewWindow
|
||||
{
|
||||
private readonly CefManager _manager;
|
||||
private readonly WebViewManagerCef _manager;
|
||||
internal CefBrowser Browser = default!;
|
||||
internal RobustRequestHandler RequestHandler = default!;
|
||||
|
||||
public Action<RequestHandlerContext>? OnResourceRequest { get; set; }
|
||||
public Action<CefRequestHandlerContext>? OnResourceRequest { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Url
|
||||
@@ -73,7 +72,7 @@ namespace Robust.Client.CEF
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserWindowImpl(CefManager manager)
|
||||
public WebViewWindowImpl(WebViewManagerCef manager)
|
||||
{
|
||||
_manager = manager;
|
||||
}
|
||||
@@ -116,12 +115,12 @@ namespace Robust.Client.CEF
|
||||
Browser.GetMainFrame().ExecuteJavaScript(code, string.Empty, 1);
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
public void AddResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
RequestHandler.AddResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler)
|
||||
public void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
RequestHandler.RemoveResourceRequestHandler(handler);
|
||||
}
|
||||
@@ -168,9 +167,9 @@ namespace Robust.Client.CEF
|
||||
|
||||
private sealed class WindowLifeSpanHandler : CefLifeSpanHandler
|
||||
{
|
||||
private readonly BrowserWindowImpl _windowImpl;
|
||||
private readonly WebViewWindowImpl _windowImpl;
|
||||
|
||||
public WindowLifeSpanHandler(BrowserWindowImpl windowImpl)
|
||||
public WindowLifeSpanHandler(WebViewWindowImpl windowImpl)
|
||||
{
|
||||
_windowImpl = windowImpl;
|
||||
}
|
||||
580
Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs
Normal file
580
Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs
Normal file
@@ -0,0 +1,580 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xilium.CefGlue;
|
||||
using static Robust.Client.WebView.Cef.CefKeyCodes.ChromiumKeyboardCode;
|
||||
using static Robust.Client.Input.Keyboard;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal partial class WebViewManagerCef
|
||||
{
|
||||
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
|
||||
{
|
||||
var impl = new ControlImpl(owner);
|
||||
_dependencyCollection.InjectDependencies(impl);
|
||||
return impl;
|
||||
}
|
||||
|
||||
private sealed class ControlImpl : IWebViewControlImpl
|
||||
{
|
||||
private static readonly Dictionary<Key, CefKeyCodes.ChromiumKeyboardCode> KeyMap = new()
|
||||
{
|
||||
[Key.A] = VKEY_A,
|
||||
[Key.B] = VKEY_B,
|
||||
[Key.C] = VKEY_C,
|
||||
[Key.D] = VKEY_D,
|
||||
[Key.E] = VKEY_E,
|
||||
[Key.F] = VKEY_F,
|
||||
[Key.G] = VKEY_G,
|
||||
[Key.H] = VKEY_H,
|
||||
[Key.I] = VKEY_I,
|
||||
[Key.J] = VKEY_J,
|
||||
[Key.K] = VKEY_K,
|
||||
[Key.L] = VKEY_L,
|
||||
[Key.M] = VKEY_M,
|
||||
[Key.N] = VKEY_N,
|
||||
[Key.O] = VKEY_O,
|
||||
[Key.P] = VKEY_P,
|
||||
[Key.Q] = VKEY_Q,
|
||||
[Key.R] = VKEY_R,
|
||||
[Key.S] = VKEY_S,
|
||||
[Key.T] = VKEY_T,
|
||||
[Key.U] = VKEY_U,
|
||||
[Key.V] = VKEY_V,
|
||||
[Key.W] = VKEY_W,
|
||||
[Key.X] = VKEY_X,
|
||||
[Key.Y] = VKEY_Y,
|
||||
[Key.Z] = VKEY_Z,
|
||||
[Key.Num0] = VKEY_0,
|
||||
[Key.Num1] = VKEY_1,
|
||||
[Key.Num2] = VKEY_2,
|
||||
[Key.Num3] = VKEY_3,
|
||||
[Key.Num4] = VKEY_4,
|
||||
[Key.Num5] = VKEY_5,
|
||||
[Key.Num6] = VKEY_6,
|
||||
[Key.Num7] = VKEY_7,
|
||||
[Key.Num8] = VKEY_8,
|
||||
[Key.Num9] = VKEY_9,
|
||||
[Key.NumpadNum0] = VKEY_NUMPAD0,
|
||||
[Key.NumpadNum1] = VKEY_NUMPAD1,
|
||||
[Key.NumpadNum2] = VKEY_NUMPAD2,
|
||||
[Key.NumpadNum3] = VKEY_NUMPAD3,
|
||||
[Key.NumpadNum4] = VKEY_NUMPAD4,
|
||||
[Key.NumpadNum5] = VKEY_NUMPAD5,
|
||||
[Key.NumpadNum6] = VKEY_NUMPAD6,
|
||||
[Key.NumpadNum7] = VKEY_NUMPAD7,
|
||||
[Key.NumpadNum8] = VKEY_NUMPAD8,
|
||||
[Key.NumpadNum9] = VKEY_NUMPAD9,
|
||||
[Key.Escape] = VKEY_ESCAPE,
|
||||
[Key.Control] = VKEY_CONTROL,
|
||||
[Key.Shift] = VKEY_SHIFT,
|
||||
[Key.Alt] = VKEY_MENU,
|
||||
[Key.LSystem] = VKEY_LWIN,
|
||||
[Key.RSystem] = VKEY_RWIN,
|
||||
[Key.LBracket] = VKEY_OEM_4,
|
||||
[Key.RBracket] = VKEY_OEM_6,
|
||||
[Key.SemiColon] = VKEY_OEM_1,
|
||||
[Key.Comma] = VKEY_OEM_COMMA,
|
||||
[Key.Period] = VKEY_OEM_PERIOD,
|
||||
[Key.Apostrophe] = VKEY_OEM_7,
|
||||
[Key.Slash] = VKEY_OEM_2,
|
||||
[Key.BackSlash] = VKEY_OEM_5,
|
||||
[Key.Tilde] = VKEY_OEM_3,
|
||||
[Key.Equal] = VKEY_OEM_PLUS,
|
||||
[Key.Space] = VKEY_SPACE,
|
||||
[Key.Return] = VKEY_RETURN,
|
||||
[Key.BackSpace] = VKEY_BACK,
|
||||
[Key.Tab] = VKEY_TAB,
|
||||
[Key.PageUp] = VKEY_PRIOR,
|
||||
[Key.PageDown] = VKEY_NEXT,
|
||||
[Key.End] = VKEY_END,
|
||||
[Key.Home] = VKEY_HOME,
|
||||
[Key.Insert] = VKEY_INSERT,
|
||||
[Key.Delete] = VKEY_DELETE,
|
||||
[Key.Minus] = VKEY_OEM_MINUS,
|
||||
[Key.NumpadAdd] = VKEY_ADD,
|
||||
[Key.NumpadSubtract] = VKEY_SUBTRACT,
|
||||
[Key.NumpadDivide] = VKEY_DIVIDE,
|
||||
[Key.NumpadMultiply] = VKEY_MULTIPLY,
|
||||
[Key.NumpadDecimal] = VKEY_DECIMAL,
|
||||
[Key.Left] = VKEY_LEFT,
|
||||
[Key.Right] = VKEY_RIGHT,
|
||||
[Key.Up] = VKEY_UP,
|
||||
[Key.Down] = VKEY_DOWN,
|
||||
[Key.F1] = VKEY_F1,
|
||||
[Key.F2] = VKEY_F2,
|
||||
[Key.F3] = VKEY_F3,
|
||||
[Key.F4] = VKEY_F4,
|
||||
[Key.F5] = VKEY_F5,
|
||||
[Key.F6] = VKEY_F6,
|
||||
[Key.F7] = VKEY_F7,
|
||||
[Key.F8] = VKEY_F8,
|
||||
[Key.F9] = VKEY_F9,
|
||||
[Key.F10] = VKEY_F10,
|
||||
[Key.F11] = VKEY_F11,
|
||||
[Key.F12] = VKEY_F12,
|
||||
[Key.F13] = VKEY_F13,
|
||||
[Key.F14] = VKEY_F14,
|
||||
[Key.F15] = VKEY_F15,
|
||||
[Key.Pause] = VKEY_PAUSE,
|
||||
};
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IInputManager _inputMgr = default!;
|
||||
|
||||
public readonly WebViewControl Owner;
|
||||
|
||||
public ControlImpl(WebViewControl owner)
|
||||
{
|
||||
Owner = owner;
|
||||
}
|
||||
|
||||
private const int ScrollSpeed = 50;
|
||||
|
||||
private readonly RobustRequestHandler _requestHandler = new(Logger.GetSawmill("root"));
|
||||
private LiveData? _data;
|
||||
private string _startUrl = "about:blank";
|
||||
|
||||
public string Url
|
||||
{
|
||||
get => _data == null ? _startUrl : _data.Browser.GetMainFrame().Url;
|
||||
set
|
||||
{
|
||||
if (_data == null)
|
||||
_startUrl = value;
|
||||
else
|
||||
_data.Browser.GetMainFrame().LoadUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoading => _data?.Browser.IsLoading ?? false;
|
||||
|
||||
public void EnteredTree()
|
||||
{
|
||||
DebugTools.AssertNull(_data);
|
||||
|
||||
// A funny render handler that will allow us to render to the control.
|
||||
var renderer = new ControlRenderHandler(this);
|
||||
|
||||
// A funny web cef client. This can actually be shared by multiple browsers, but I'm not sure how the
|
||||
// rendering would work in that case? TODO CEF: Investigate a way to share the web client?
|
||||
var client = new RobustCefClient(renderer, _requestHandler, new RobustLoadHandler());
|
||||
|
||||
var info = CefWindowInfo.Create();
|
||||
|
||||
// FUNFACT: If you DO NOT set these below and set info.Width/info.Height instead, you get an external window
|
||||
// Good to know, huh? Setup is the same, except you can pass a dummy render handler to the CEF client.
|
||||
info.SetAsWindowless(IntPtr.Zero, false); // TODO CEF: Pass parent handle?
|
||||
info.WindowlessRenderingEnabled = true;
|
||||
|
||||
var settings = new CefBrowserSettings()
|
||||
{
|
||||
WindowlessFrameRate = 60
|
||||
};
|
||||
|
||||
// Create the web browser! And by default, we go to about:blank.
|
||||
var browser = CefBrowserHost.CreateBrowserSync(info, client, settings, _startUrl);
|
||||
|
||||
var texture = _clyde.CreateBlankTexture<Bgra32>(Vector2i.One);
|
||||
|
||||
_data = new LiveData(texture, client, browser, renderer);
|
||||
}
|
||||
|
||||
public void ExitedTree()
|
||||
{
|
||||
DebugTools.AssertNotNull(_data);
|
||||
|
||||
_data!.Texture.Dispose();
|
||||
_data.Browser.GetHost().CloseBrowser(true);
|
||||
_data = null;
|
||||
}
|
||||
|
||||
public void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
// Logger.Debug();
|
||||
var modifiers = CalcMouseModifiers();
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
(int)args.RelativePosition.X, (int)args.RelativePosition.Y,
|
||||
modifiers);
|
||||
|
||||
_data.Browser.GetHost().SendMouseMoveEvent(mouseEvent, false);
|
||||
}
|
||||
|
||||
public void MouseExited()
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var modifiers = CalcMouseModifiers();
|
||||
|
||||
_data.Browser.GetHost().SendMouseMoveEvent(new CefMouseEvent(0, 0, modifiers), true);
|
||||
}
|
||||
|
||||
public void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var modifiers = CalcMouseModifiers();
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
(int)args.RelativePosition.X, (int)args.RelativePosition.Y,
|
||||
modifiers);
|
||||
|
||||
_data.Browser.GetHost().SendMouseWheelEvent(
|
||||
mouseEvent,
|
||||
(int)args.Delta.X * ScrollSpeed,
|
||||
(int)args.Delta.Y * ScrollSpeed);
|
||||
}
|
||||
|
||||
public bool RawKeyEvent(in GuiRawKeyEvent guiRawEvent)
|
||||
{
|
||||
if (_data == null)
|
||||
return false;
|
||||
|
||||
var host = _data.Browser.GetHost();
|
||||
|
||||
if (guiRawEvent.Key is Key.MouseLeft or Key.MouseMiddle or Key.MouseRight)
|
||||
{
|
||||
var key = guiRawEvent.Key switch
|
||||
{
|
||||
Key.MouseLeft => CefMouseButtonType.Left,
|
||||
Key.MouseMiddle => CefMouseButtonType.Middle,
|
||||
Key.MouseRight => CefMouseButtonType.Right,
|
||||
_ => default // not possible
|
||||
};
|
||||
|
||||
var mouseEvent = new CefMouseEvent(
|
||||
guiRawEvent.MouseRelative.X, guiRawEvent.MouseRelative.Y,
|
||||
CefEventFlags.None);
|
||||
|
||||
// Logger.Debug($"MOUSE: {guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {key}");
|
||||
|
||||
// TODO: double click support?
|
||||
host.SendMouseClickEvent(mouseEvent, key, guiRawEvent.Action == RawKeyAction.Up, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Handle left/right modifier keys??
|
||||
if (!KeyMap.TryGetValue(guiRawEvent.Key, out var vkKey))
|
||||
vkKey = default;
|
||||
|
||||
// Logger.Debug($"{guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {vkKey}");
|
||||
|
||||
var lParam = 0;
|
||||
lParam |= (guiRawEvent.ScanCode & 0xFF) << 16;
|
||||
if (guiRawEvent.Action != RawKeyAction.Down)
|
||||
lParam |= 1 << 30;
|
||||
|
||||
if (guiRawEvent.Action == RawKeyAction.Up)
|
||||
lParam |= 1 << 31;
|
||||
|
||||
var modifiers = CalcModifiers(guiRawEvent.Key);
|
||||
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
// Repeats are sent as key downs, I guess?
|
||||
EventType = guiRawEvent.Action == RawKeyAction.Up
|
||||
? CefKeyEventType.KeyUp
|
||||
: CefKeyEventType.RawKeyDown,
|
||||
NativeKeyCode = lParam,
|
||||
// NativeKeyCode = guiRawEvent.ScanCode,
|
||||
WindowsKeyCode = (int)vkKey,
|
||||
IsSystemKey = false, // TODO
|
||||
Modifiers = modifiers
|
||||
});
|
||||
|
||||
if (guiRawEvent.Action != RawKeyAction.Up && guiRawEvent.Key == Key.Return)
|
||||
{
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
EventType = CefKeyEventType.Char,
|
||||
WindowsKeyCode = '\r',
|
||||
NativeKeyCode = lParam,
|
||||
Modifiers = modifiers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private CefEventFlags CalcModifiers(Key key)
|
||||
{
|
||||
CefEventFlags modifiers = default;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Control))
|
||||
modifiers |= CefEventFlags.ControlDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Alt))
|
||||
modifiers |= CefEventFlags.AltDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
private CefEventFlags CalcMouseModifiers()
|
||||
{
|
||||
CefEventFlags modifiers = default;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Control))
|
||||
modifiers |= CefEventFlags.ControlDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Alt))
|
||||
modifiers |= CefEventFlags.AltDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.Shift))
|
||||
modifiers |= CefEventFlags.ShiftDown;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseLeft))
|
||||
modifiers |= CefEventFlags.LeftMouseButton;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseMiddle))
|
||||
modifiers |= CefEventFlags.MiddleMouseButton;
|
||||
|
||||
if (_inputMgr.IsKeyDown(Key.MouseRight))
|
||||
modifiers |= CefEventFlags.RightMouseButton;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
public void TextEntered(GUITextEventArgs args)
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var host = _data.Browser.GetHost();
|
||||
|
||||
Span<char> buf = stackalloc char[2];
|
||||
var written = args.AsRune.EncodeToUtf16(buf);
|
||||
|
||||
for (var i = 0; i < written; i++)
|
||||
{
|
||||
host.SendKeyEvent(new CefKeyEvent
|
||||
{
|
||||
EventType = CefKeyEventType.Char,
|
||||
WindowsKeyCode = buf[i],
|
||||
Character = buf[i],
|
||||
UnmodifiedCharacter = buf[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Resized()
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
_data.Browser.GetHost().NotifyMoveOrResizeStarted();
|
||||
_data.Browser.GetHost().WasResized();
|
||||
_data.Texture.Dispose();
|
||||
_data.Texture = _clyde.CreateBlankTexture<Bgra32>((Owner.PixelWidth, Owner.PixelHeight));
|
||||
}
|
||||
|
||||
public void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
var bufImg = _data.Renderer.Buffer.Buffer;
|
||||
|
||||
_data.Texture.SetSubImage(
|
||||
Vector2i.Zero,
|
||||
bufImg,
|
||||
new UIBox2i(
|
||||
0, 0,
|
||||
Math.Min(Owner.PixelWidth, bufImg.Width),
|
||||
Math.Min(Owner.PixelHeight, bufImg.Height)));
|
||||
|
||||
handle.DrawTexture(_data.Texture, Vector2.Zero);
|
||||
}
|
||||
|
||||
public void StopLoad()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.StopLoad();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
_data.Browser.Reload();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!_data.Browser.CanGoBack)
|
||||
return false;
|
||||
|
||||
_data.Browser.GoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!_data.Browser.CanGoForward)
|
||||
return false;
|
||||
|
||||
_data.Browser.GoForward();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ExecuteJavaScript(string code)
|
||||
{
|
||||
if (_data == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
// TODO: this should not run until the browser is done loading seriously does this even work?
|
||||
_data.Browser.GetMainFrame().ExecuteJavaScript(code, string.Empty, 1);
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
_requestHandler.AddResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
_requestHandler.RemoveResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void AddBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
_requestHandler.AddBeforeBrowseHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
_requestHandler.RemoveBeforeBrowseHandler(handler);
|
||||
}
|
||||
|
||||
private sealed class LiveData
|
||||
{
|
||||
public OwnedTexture Texture;
|
||||
public readonly RobustCefClient Client;
|
||||
public readonly CefBrowser Browser;
|
||||
public readonly ControlRenderHandler Renderer;
|
||||
|
||||
public LiveData(
|
||||
OwnedTexture texture,
|
||||
RobustCefClient client,
|
||||
CefBrowser browser,
|
||||
ControlRenderHandler renderer)
|
||||
{
|
||||
Texture = texture;
|
||||
Client = client;
|
||||
Browser = browser;
|
||||
Renderer = renderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ControlRenderHandler : CefRenderHandler
|
||||
{
|
||||
public ImageBuffer Buffer { get; }
|
||||
private ControlImpl _control;
|
||||
|
||||
internal ControlRenderHandler(ControlImpl control)
|
||||
{
|
||||
Buffer = new ImageBuffer();
|
||||
_control = control;
|
||||
}
|
||||
|
||||
protected override CefAccessibilityHandler? GetAccessibilityHandler() => null;
|
||||
|
||||
protected override void GetViewRect(CefBrowser browser, out CefRectangle rect)
|
||||
{
|
||||
if (_control.Owner.Disposed)
|
||||
{
|
||||
rect = new CefRectangle();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO CEF: Do we need to pass real screen coords? Cause what we do already works...
|
||||
//var screenCoords = _control.ScreenCoordinates;
|
||||
//rect = new CefRectangle((int) screenCoords.X, (int) screenCoords.Y, (int)Math.Max(_control.Size.X, 1), (int)Math.Max(_control.Size.Y, 1));
|
||||
|
||||
// We do the max between size and 1 because it will LITERALLY CRASH WITHOUT AN ERROR otherwise.
|
||||
rect = new CefRectangle(
|
||||
0, 0,
|
||||
(int)Math.Max(_control.Owner.Size.X, 1), (int)Math.Max(_control.Owner.Size.Y, 1));
|
||||
}
|
||||
|
||||
protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
|
||||
{
|
||||
if (_control.Owner.Disposed)
|
||||
return false;
|
||||
|
||||
// TODO CEF: Get actual scale factor?
|
||||
screenInfo.DeviceScaleFactor = 1.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
|
||||
{
|
||||
if (_control.Owner.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects,
|
||||
IntPtr buffer, int width, int height)
|
||||
{
|
||||
if (_control.Owner.Disposed)
|
||||
return;
|
||||
|
||||
foreach (var dirtyRect in dirtyRects)
|
||||
{
|
||||
Buffer.UpdateBuffer(width, height, buffer, dirtyRect);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects, IntPtr sharedHandle)
|
||||
{
|
||||
// Unused, but we're forced to implement it so.. NOOP.
|
||||
}
|
||||
|
||||
protected override void OnScrollOffsetChanged(CefBrowser browser, double x, double y)
|
||||
{
|
||||
if (_control.Owner.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectedRange,
|
||||
CefRectangle[] characterBounds)
|
||||
{
|
||||
if (_control.Owner.Disposed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Robust.Client.WebView/Cef/WebViewManagerCef.cs
Normal file
101
Robust.Client.WebView/Cef/WebViewManagerCef.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal partial class WebViewManagerCef : IWebViewManagerImpl
|
||||
{
|
||||
private CefApp _app = default!;
|
||||
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
IoCManager.Instance!.InjectDependencies(this, oneOff: true);
|
||||
|
||||
string subProcessName;
|
||||
if (OperatingSystem.IsWindows())
|
||||
subProcessName = "Robust.Client.WebView.exe";
|
||||
else if (OperatingSystem.IsLinux())
|
||||
subProcessName = "Robust.Client.WebView";
|
||||
else
|
||||
throw new NotSupportedException("Unsupported platform for CEF!");
|
||||
|
||||
var subProcessPath = PathHelpers.ExecutableRelativeFile(subProcessName);
|
||||
var cefResourcesPath = LocateCefResources();
|
||||
|
||||
// System.Console.WriteLine(AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES"));
|
||||
|
||||
if (cefResourcesPath == null)
|
||||
throw new InvalidOperationException("Unable to locate cef_resources directory!");
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
|
||||
ExternalMessagePump = false, // Unsure, honestly. TODO CEF: Research this?
|
||||
NoSandbox = true, // Not disabling the sandbox crashes CEF.
|
||||
BrowserSubprocessPath = subProcessPath,
|
||||
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
|
||||
ResourcesDirPath = cefResourcesPath,
|
||||
RemoteDebuggingPort = 9222,
|
||||
};
|
||||
|
||||
Logger.Info($"CEF Version: {CefRuntime.ChromeVersion}");
|
||||
|
||||
_app = new RobustCefApp();
|
||||
|
||||
// We pass no main arguments...
|
||||
CefRuntime.Initialize(new CefMainArgs(null), settings, _app, IntPtr.Zero);
|
||||
|
||||
// TODO CEF: After this point, debugging breaks. No, literally. My client crashes but ONLY with the debugger.
|
||||
// I have tried using the DEBUG and RELEASE versions of libcef.so, stripped or non-stripped...
|
||||
// And nothing seemed to work. Odd.
|
||||
}
|
||||
|
||||
private static string? LocateCefResources()
|
||||
{
|
||||
if (ProbeDir(PathHelpers.GetExecutableDirectory(), out var path))
|
||||
return path;
|
||||
|
||||
|
||||
foreach (var searchDir in NativeDllSearchDirectories())
|
||||
{
|
||||
if (ProbeDir(searchDir, out path))
|
||||
return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
static bool ProbeDir(string dir, out string path)
|
||||
{
|
||||
path = Path.Combine(dir, "cef_resources");
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string[] NativeDllSearchDirectories()
|
||||
{
|
||||
var sepChar = OperatingSystem.IsWindows() ? ';' : ':';
|
||||
|
||||
var searchDirectories = ((string)AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES")!)
|
||||
.Split(sepChar, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return searchDirectories;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
// Calling this makes CEF do its work, without using its own update loop.
|
||||
CefRuntime.DoMessageLoopWork();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
CefRuntime.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Robust.Client.WebView/Headless/WebViewManagerHeadless.cs
Normal file
127
Robust.Client.WebView/Headless/WebViewManagerHeadless.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Robust.Client.WebView.Headless
|
||||
{
|
||||
internal sealed class WebViewManagerHeadless : IWebViewManagerImpl
|
||||
{
|
||||
public IWebViewWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams)
|
||||
{
|
||||
return new WebViewWindowDummy();
|
||||
}
|
||||
|
||||
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
|
||||
{
|
||||
return new WebViewControlImplDummy();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// Nop
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
// Nop
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Nop
|
||||
}
|
||||
|
||||
private abstract class DummyBase : IWebViewControl
|
||||
{
|
||||
public string Url { get; set; } = "about:blank";
|
||||
public bool IsLoading => true;
|
||||
|
||||
public void StopLoad()
|
||||
{
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ExecuteJavaScript(string code)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WebViewControlImplDummy : DummyBase, IWebViewControlImpl
|
||||
{
|
||||
public void EnteredTree()
|
||||
{
|
||||
}
|
||||
|
||||
public void ExitedTree()
|
||||
{
|
||||
}
|
||||
|
||||
public void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
}
|
||||
|
||||
public void MouseExited()
|
||||
{
|
||||
}
|
||||
|
||||
public void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
}
|
||||
|
||||
public bool RawKeyEvent(in GuiRawKeyEvent guiRawEvent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void TextEntered(GUITextEventArgs args)
|
||||
{
|
||||
}
|
||||
|
||||
public void Resized()
|
||||
{
|
||||
}
|
||||
|
||||
public void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
}
|
||||
|
||||
public void RemoveBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WebViewWindowDummy : DummyBase, IWebViewWindow
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool Closed => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Robust.Client.WebView/IBeforeBrowseContext.cs
Normal file
12
Robust.Client.WebView/IBeforeBrowseContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
public interface IBeforeBrowseContext
|
||||
{
|
||||
string Url { get; }
|
||||
string Method { get; }
|
||||
bool IsRedirect { get; }
|
||||
bool UserGesture { get; }
|
||||
bool IsCancelled { get; }
|
||||
void DoCancel();
|
||||
}
|
||||
}
|
||||
18
Robust.Client.WebView/IRequestHandlerContext.cs
Normal file
18
Robust.Client.WebView/IRequestHandlerContext.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
public interface IRequestHandlerContext
|
||||
{
|
||||
bool IsNavigation { get; }
|
||||
bool IsDownload { get; }
|
||||
string RequestInitiator { get; }
|
||||
string Url { get; }
|
||||
string Method { get; }
|
||||
bool IsHandled { get; }
|
||||
bool IsCancelled { get; }
|
||||
void DoCancel();
|
||||
void DoRespondStream(Stream stream, string contentType, HttpStatusCode code = HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using Robust.Client.WebView.Cef;
|
||||
|
||||
namespace Robust.Client.CEF
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
public interface IBrowserControl
|
||||
public interface IWebViewControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Current URL of the browser. Set to load a new page.
|
||||
@@ -42,7 +43,7 @@ namespace Robust.Client.CEF
|
||||
/// <param name="code">JavaScript code.</param>
|
||||
void ExecuteJavaScript(string code);
|
||||
|
||||
void AddResourceRequestHandler(Action<RequestHandlerContext> handler);
|
||||
void RemoveResourceRequestHandler(Action<RequestHandlerContext> handler);
|
||||
void AddResourceRequestHandler(Action<IRequestHandlerContext> handler);
|
||||
void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler);
|
||||
}
|
||||
}
|
||||
24
Robust.Client.WebView/IWebViewControlImpl.cs
Normal file
24
Robust.Client.WebView/IWebViewControlImpl.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal swappable implementation of <see cref="WebViewControl"/>.
|
||||
/// </summary>
|
||||
internal interface IWebViewControlImpl : IWebViewControl
|
||||
{
|
||||
void EnteredTree();
|
||||
void ExitedTree();
|
||||
void MouseMove(GUIMouseMoveEventArgs args);
|
||||
void MouseExited();
|
||||
void MouseWheel(GUIMouseWheelEventArgs args);
|
||||
bool RawKeyEvent(in GuiRawKeyEvent guiRawEvent);
|
||||
void TextEntered(GUITextEventArgs args);
|
||||
void Resized();
|
||||
void Draw(DrawingHandleScreen handle);
|
||||
void AddBeforeBrowseHandler(Action<IBeforeBrowseContext> handler);
|
||||
void RemoveBeforeBrowseHandler(Action<IBeforeBrowseContext> handler);
|
||||
}
|
||||
}
|
||||
7
Robust.Client.WebView/IWebViewManager.cs
Normal file
7
Robust.Client.WebView/IWebViewManager.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
public interface IWebViewManager
|
||||
{
|
||||
IWebViewWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams);
|
||||
}
|
||||
}
|
||||
14
Robust.Client.WebView/IWebViewManagerImpl.cs
Normal file
14
Robust.Client.WebView/IWebViewManagerImpl.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.WebViewHook;
|
||||
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal implementation of WebViewManager that is switched out by <see cref="IWebViewManagerHook"/>.
|
||||
/// </summary>
|
||||
internal interface IWebViewManagerImpl : IWebViewManagerInternal
|
||||
{
|
||||
void Initialize();
|
||||
void Update();
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
7
Robust.Client.WebView/IWebViewManagerInternal.cs
Normal file
7
Robust.Client.WebView/IWebViewManagerInternal.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
internal interface IWebViewManagerInternal : IWebViewManager
|
||||
{
|
||||
IWebViewControlImpl MakeControlImpl(WebViewControl owner);
|
||||
}
|
||||
}
|
||||
9
Robust.Client.WebView/IWebViewWindow.cs
Normal file
9
Robust.Client.WebView/IWebViewWindow.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
public interface IWebViewWindow : IWebViewControl, IDisposable
|
||||
{
|
||||
bool Closed { get; }
|
||||
}
|
||||
}
|
||||
26
Robust.Client.WebView/Robust.Client.WebView.csproj
Normal file
26
Robust.Client.WebView/Robust.Client.WebView.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
|
||||
<Target Name="RobustAfterBuild" AfterTargets="Build" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
<PackageReference Include="Robust.Natives.Cef" Version="95.7.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\cefglue\CefGlue\CefGlue.csproj" />
|
||||
<ProjectReference Include="..\Robust.Client\Robust.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
145
Robust.Client.WebView/WebViewControl.cs
Normal file
145
Robust.Client.WebView/WebViewControl.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.WebView.Cef;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
/// <summary>
|
||||
/// An UI control that presents web content.
|
||||
/// </summary>
|
||||
public sealed class WebViewControl : Control, IWebViewControl, IRawInputControl
|
||||
{
|
||||
[Dependency] private readonly IWebViewManagerInternal _webViewManager = default!;
|
||||
|
||||
private readonly IWebViewControlImpl _controlImpl;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Url
|
||||
{
|
||||
get => _controlImpl.Url;
|
||||
set => _controlImpl.Url = value;
|
||||
}
|
||||
|
||||
[ViewVariables] public bool IsLoading => _controlImpl.IsLoading;
|
||||
|
||||
public WebViewControl()
|
||||
{
|
||||
CanKeyboardFocus = true;
|
||||
KeyboardFocusOnClick = true;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_controlImpl = _webViewManager.MakeControlImpl(this);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_controlImpl.EnteredTree();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_controlImpl.ExitedTree();
|
||||
}
|
||||
|
||||
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
|
||||
_controlImpl.MouseMove(args);
|
||||
}
|
||||
|
||||
protected internal override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
_controlImpl.MouseExited();
|
||||
}
|
||||
|
||||
protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
base.MouseWheel(args);
|
||||
|
||||
_controlImpl.MouseWheel(args);
|
||||
}
|
||||
|
||||
bool IRawInputControl.RawKeyEvent(in GuiRawKeyEvent guiRawEvent)
|
||||
{
|
||||
return _controlImpl.RawKeyEvent(guiRawEvent);
|
||||
}
|
||||
|
||||
protected internal override void TextEntered(GUITextEventArgs args)
|
||||
{
|
||||
base.TextEntered(args);
|
||||
|
||||
_controlImpl.TextEntered(args);
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
|
||||
_controlImpl.Resized();
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
_controlImpl.Draw(handle);
|
||||
}
|
||||
|
||||
public void StopLoad()
|
||||
{
|
||||
_controlImpl.StopLoad();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
_controlImpl.Reload();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
return _controlImpl.GoBack();
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
return _controlImpl.GoForward();
|
||||
}
|
||||
|
||||
public void ExecuteJavaScript(string code)
|
||||
{
|
||||
_controlImpl.ExecuteJavaScript(code);
|
||||
}
|
||||
|
||||
public void AddResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
_controlImpl.AddResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler)
|
||||
{
|
||||
_controlImpl.RemoveResourceRequestHandler(handler);
|
||||
}
|
||||
|
||||
public void AddBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
_controlImpl.AddBeforeBrowseHandler(handler);
|
||||
}
|
||||
|
||||
public void RemoveBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
|
||||
{
|
||||
_controlImpl.RemoveBeforeBrowseHandler(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Robust.Client.WebView/WebViewManager.cs
Normal file
59
Robust.Client.WebView/WebViewManager.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Robust.Client.WebView;
|
||||
using Robust.Client.WebView.Cef;
|
||||
using Robust.Client.WebView.Headless;
|
||||
using Robust.Client.WebViewHook;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
[assembly: WebViewManagerImpl(typeof(WebViewManager))]
|
||||
|
||||
namespace Robust.Client.WebView
|
||||
{
|
||||
internal sealed class WebViewManager : IWebViewManagerInternal, IWebViewManagerHook
|
||||
{
|
||||
private IWebViewManagerImpl? _impl;
|
||||
|
||||
public void Initialize(GameController.DisplayMode mode)
|
||||
{
|
||||
DebugTools.Assert(_impl == null, "WebViewManager has already been initialized!");
|
||||
|
||||
IoCManager.RegisterInstance<IWebViewManager>(this);
|
||||
IoCManager.RegisterInstance<IWebViewManagerInternal>(this);
|
||||
|
||||
if (mode == GameController.DisplayMode.Headless)
|
||||
_impl = new WebViewManagerHeadless();
|
||||
else
|
||||
_impl = new WebViewManagerCef();
|
||||
|
||||
_impl.Initialize();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");
|
||||
|
||||
_impl!.Update();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");
|
||||
|
||||
_impl!.Shutdown();
|
||||
}
|
||||
|
||||
public IWebViewWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams)
|
||||
{
|
||||
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");
|
||||
|
||||
return _impl!.CreateBrowserWindow(createParams);
|
||||
}
|
||||
|
||||
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
|
||||
{
|
||||
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");
|
||||
|
||||
return _impl!.MakeControlImpl(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Robust.Client/Console/Completions.cs
Normal file
127
Robust.Client/Console/Completions.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using static Robust.Shared.Network.Messages.MsgScriptCompletionResponse;
|
||||
|
||||
namespace Robust.Client.Console
|
||||
{
|
||||
public class Completions : SS14Window
|
||||
{
|
||||
private HistoryLineEdit _textBar;
|
||||
private ScrollContainer _suggestPanel = new()
|
||||
{
|
||||
HScrollEnabled = false,
|
||||
};
|
||||
|
||||
private BoxContainer _suggestBox = new()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
};
|
||||
|
||||
public Completions(HistoryLineEdit textBar) : base()
|
||||
{
|
||||
Title = "Suggestions";
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
|
||||
_textBar = textBar;
|
||||
_suggestPanel.AddChild(_suggestBox);
|
||||
ContentsContainer.AddChild(_suggestPanel);
|
||||
}
|
||||
|
||||
private bool _firstopen = true;
|
||||
public void OpenAt(Vector2 position, Vector2 size)
|
||||
{
|
||||
if (_firstopen)
|
||||
{
|
||||
SetSize = size;
|
||||
LayoutContainer.SetPosition(this, position);
|
||||
_firstopen = false;
|
||||
}
|
||||
Open();
|
||||
}
|
||||
|
||||
private ImmutableArray<LiteResult> _results;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
_suggestBox.RemoveAllChildren();
|
||||
foreach (var res in _results)
|
||||
{
|
||||
var label = new Entry(res);
|
||||
|
||||
label.OnKeyBindDown += ev =>
|
||||
{
|
||||
if (ev.Function == EngineKeyFunctions.UIClick)
|
||||
_textBar.InsertAtCursor(label.Result.Properties["InsertionText"]);
|
||||
};
|
||||
|
||||
_suggestBox.AddChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
public void TextChanged() => Close();
|
||||
|
||||
public void SetSuggestions(MsgScriptCompletionResponse response)
|
||||
{
|
||||
_results = response.Results;
|
||||
Update();
|
||||
}
|
||||
|
||||
// Label and ghetto button.
|
||||
public class Entry : RichTextLabel
|
||||
{
|
||||
public readonly LiteResult Result;
|
||||
|
||||
public Entry(LiteResult result)
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
Result = result;
|
||||
var compl = new FormattedMessage();
|
||||
var dim = Color.FromHsl((0f, 0f, 0.8f, 1f));
|
||||
|
||||
// warning: ew ahead
|
||||
string basen = "default";
|
||||
if (Result.Tags.Contains("Interface"))
|
||||
basen = "interface name";
|
||||
else if (Result.Tags.Contains("Class"))
|
||||
basen = "class name";
|
||||
else if (Result.Tags.Contains("Struct"))
|
||||
basen = "struct name";
|
||||
else if (Result.Tags.Contains("Keyword"))
|
||||
basen = "keyword";
|
||||
else if (Result.Tags.Contains("Namespace"))
|
||||
basen = "namespace name";
|
||||
else if (Result.Tags.Contains("Method"))
|
||||
basen = "method name";
|
||||
else if (Result.Tags.Contains("Property"))
|
||||
basen = "property name";
|
||||
else if (Result.Tags.Contains("Field"))
|
||||
basen = "field name";
|
||||
|
||||
Color basec = ScriptingColorScheme.ColorScheme[basen];
|
||||
compl.PushColor(basec * dim);
|
||||
compl.AddText(Result.DisplayTextPrefix);
|
||||
compl.PushColor(basec);
|
||||
compl.AddText(Result.DisplayText);
|
||||
compl.PushColor(basec * dim);
|
||||
compl.AddText(Result.DisplayTextSuffix);
|
||||
compl.AddText(" [" + String.Join(", ", Result.Tags) + "]");
|
||||
if (Result.InlineDescription.Length != 0)
|
||||
{
|
||||
compl.PushNewline();
|
||||
compl.AddText(": ");
|
||||
compl.PushColor(Color.LightSlateGray);
|
||||
compl.AddText(Result.InlineDescription);
|
||||
}
|
||||
SetMessage(compl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,16 @@ namespace Robust.Client.Console
|
||||
|
||||
}
|
||||
|
||||
protected override void Complete()
|
||||
{
|
||||
var msg = _client._netManager.CreateNetMessage<MsgScriptCompletion>();
|
||||
msg.ScriptSession = _session;
|
||||
msg.Code = InputBar.Text;
|
||||
msg.Cursor = InputBar.CursorPosition;
|
||||
|
||||
_client._netManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
@@ -95,6 +105,12 @@ namespace Robust.Client.Console
|
||||
|
||||
OutputPanel.AddText(">");
|
||||
}
|
||||
|
||||
public void ReceiveCompletionResponse(MsgScriptCompletionResponse response)
|
||||
{
|
||||
Suggestions.SetSuggestions(response);
|
||||
Suggestions.OpenAt((Position.X + Size.X, Position.Y), (Size.X / 2, Size.Y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Robust.Client.Console
|
||||
_netManager.RegisterNetMessage<MsgScriptStop>();
|
||||
_netManager.RegisterNetMessage<MsgScriptEval>();
|
||||
_netManager.RegisterNetMessage<MsgScriptStart>();
|
||||
_netManager.RegisterNetMessage<MsgScriptCompletion>();
|
||||
_netManager.RegisterNetMessage<MsgScriptCompletionResponse>(ReceiveScriptCompletionResponse);
|
||||
_netManager.RegisterNetMessage<MsgScriptResponse>(ReceiveScriptResponse);
|
||||
_netManager.RegisterNetMessage<MsgScriptStartAck>(ReceiveScriptStartAckResponse);
|
||||
}
|
||||
@@ -44,6 +46,16 @@ namespace Robust.Client.Console
|
||||
console.ReceiveResponse(message);
|
||||
}
|
||||
|
||||
private void ReceiveScriptCompletionResponse(MsgScriptCompletionResponse message)
|
||||
{
|
||||
if (!_activeConsoles.TryGetValue(message.ScriptSession, out var console))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
console.ReceiveCompletionResponse(message);
|
||||
}
|
||||
|
||||
public bool CanScript => _conGroupController.CanScript();
|
||||
|
||||
public void StartSession()
|
||||
|
||||
@@ -53,6 +53,9 @@ namespace Robust.Client.Console
|
||||
OutputPanel.AddText(">");
|
||||
}
|
||||
|
||||
// No-op for now.
|
||||
protected override void Complete() { }
|
||||
|
||||
protected override async void Run()
|
||||
{
|
||||
var code = InputBar.Text;
|
||||
|
||||
52
Robust.Client/GameController/GameController.RobustModules.cs
Normal file
52
Robust.Client/GameController/GameController.RobustModules.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Robust.Client.WebViewHook;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
internal sealed partial class GameController
|
||||
{
|
||||
private void LoadOptionalRobustModules(GameController.DisplayMode mode)
|
||||
{
|
||||
// In the future, this manifest should be loaded somewhere else and used for more parts of init.
|
||||
// For now, this is fine.
|
||||
var manifest = LoadResourceManifest();
|
||||
|
||||
foreach (var module in manifest.Modules)
|
||||
{
|
||||
switch (module)
|
||||
{
|
||||
case "Robust.Client.WebView":
|
||||
LoadRobustWebView(mode);
|
||||
break;
|
||||
default:
|
||||
Logger.Error($"Unknown Robust module: {module}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadRobustWebView(GameController.DisplayMode mode)
|
||||
{
|
||||
Logger.Debug("Loading Robust.Client.WebView");
|
||||
|
||||
var assembly = LoadRobustModuleAssembly("Robust.Client.WebView");
|
||||
var attribute = assembly.GetCustomAttribute<WebViewManagerImplAttribute>()!;
|
||||
DebugTools.AssertNotNull(attribute);
|
||||
|
||||
var managerType = attribute.ImplementationType;
|
||||
_webViewHook = (IWebViewManagerHook)Activator.CreateInstance(managerType)!;
|
||||
_webViewHook.Initialize(mode);
|
||||
|
||||
Logger.Debug("Done initializing Robust.Client.WebView");
|
||||
}
|
||||
|
||||
private Assembly LoadRobustModuleAssembly(string assemblyName)
|
||||
{
|
||||
// TODO: Launcher distribution and all that stuff.
|
||||
return Assembly.Load(assemblyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime;
|
||||
@@ -11,12 +10,12 @@ using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Client.WebViewHook;
|
||||
using Robust.LoaderApi;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
@@ -32,6 +31,7 @@ using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
@@ -67,6 +67,8 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
private CommandLineArgs? _commandLineArgs;
|
||||
|
||||
// Arguments for loader-load. Not used otherwise.
|
||||
@@ -87,7 +89,7 @@ namespace Robust.Client
|
||||
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
|
||||
|
||||
_taskManager.Initialize();
|
||||
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
|
||||
// Disable load context usage on content start.
|
||||
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
|
||||
@@ -107,6 +109,9 @@ namespace Robust.Client
|
||||
|
||||
IoCManager.Resolve<ISerializationManager>().Initialize();
|
||||
|
||||
// Load optional Robust modules.
|
||||
LoadOptionalRobustModules(displayMode);
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
|
||||
@@ -196,6 +201,39 @@ namespace Robust.Client
|
||||
return true;
|
||||
}
|
||||
|
||||
private ResourceManifestData LoadResourceManifest()
|
||||
{
|
||||
// Parses /manifest.yml for game-specific settings that cannot be exclusively set up by content code.
|
||||
if (!_resourceCache.TryContentFileRead("/manifest.yml", out var stream))
|
||||
return new ResourceManifestData(Array.Empty<string>());
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
using (stream)
|
||||
{
|
||||
using var streamReader = new StreamReader(stream, EncodingHelpers.UTF8);
|
||||
yamlStream.Load(streamReader);
|
||||
}
|
||||
|
||||
if (yamlStream.Documents.Count != 1 || yamlStream.Documents[0].RootNode is not YamlMappingNode mapping)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Expected a single YAML document with root mapping for /manifest.yml");
|
||||
}
|
||||
|
||||
var modules = Array.Empty<string>();
|
||||
if (mapping.TryGetNode("modules", out var modulesMap))
|
||||
{
|
||||
var sequence = (YamlSequenceNode)modulesMap;
|
||||
modules = new string[sequence.Children.Count];
|
||||
for (var i = 0; i < modules.Length; i++)
|
||||
{
|
||||
modules[i] = sequence[i].AsString();
|
||||
}
|
||||
}
|
||||
|
||||
return new ResourceManifestData(modules);
|
||||
}
|
||||
|
||||
internal bool StartupSystemSplash(GameControllerOptions options, Func<ILogHandler>? logHandlerFactory)
|
||||
{
|
||||
Options = options;
|
||||
@@ -217,8 +255,10 @@ namespace Robust.Client
|
||||
System.Console.WriteLine($"LogLevel {level} does not exist!");
|
||||
continue;
|
||||
}
|
||||
|
||||
logLevel = result;
|
||||
}
|
||||
|
||||
_logManager.GetSawmill(sawmill).Level = logLevel;
|
||||
}
|
||||
}
|
||||
@@ -259,7 +299,7 @@ namespace Robust.Client
|
||||
|
||||
{
|
||||
// Handle GameControllerOptions implicit CVar overrides.
|
||||
_configurationManager.OverrideConVars(new []
|
||||
_configurationManager.OverrideConVars(new[]
|
||||
{
|
||||
(CVars.DisplayWindowIconSet.Name, options.WindowIconSet.ToString()),
|
||||
(CVars.DisplaySplashLogo.Name, options.SplashLogo.ToString())
|
||||
@@ -271,9 +311,11 @@ namespace Robust.Client
|
||||
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
: Options.MountOptions;
|
||||
|
||||
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory, Options.AssemblyDirectory,
|
||||
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
|
||||
Options.AssemblyDirectory,
|
||||
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
|
||||
|
||||
if (_loaderArgs != null)
|
||||
@@ -394,6 +436,7 @@ namespace Robust.Client
|
||||
|
||||
private void Update(FrameEventArgs frameEventArgs)
|
||||
{
|
||||
_webViewHook?.Update();
|
||||
_clyde.FrameProcess(frameEventArgs);
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
|
||||
_stateManager.FrameUpdate(frameEventArgs);
|
||||
@@ -442,7 +485,7 @@ namespace Robust.Client
|
||||
var uh = logManager.GetSawmill("unhandled");
|
||||
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
|
||||
{
|
||||
var message = ((Exception) args.ExceptionObject).ToString();
|
||||
var message = ((Exception)args.ExceptionObject).ToString();
|
||||
uh.Log(args.IsTerminating ? LogLevel.Fatal : LogLevel.Error, message);
|
||||
};
|
||||
|
||||
@@ -485,11 +528,15 @@ namespace Robust.Client
|
||||
{
|
||||
_modLoader.Shutdown();
|
||||
|
||||
_webViewHook?.Shutdown();
|
||||
|
||||
_networkManager.Shutdown("Client shutting down");
|
||||
_midiManager.Shutdown();
|
||||
IoCManager.Resolve<IEntityLookup>().Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
_clyde.Shutdown();
|
||||
}
|
||||
|
||||
private sealed record ResourceManifestData(string[] Modules);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
internal void AnimationComplete(string key)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
AnimationCompleted?.Invoke(key);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
[Obsolete("Use AnimationCompletedEvent instead")]
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace Robust.Client.GameObjects
|
||||
private bool _setDrawFovOnInitialize = true;
|
||||
[DataField("zoom")]
|
||||
private Vector2 _setZoomOnInitialize = Vector2.One;
|
||||
private Vector2 _offset = Vector2.Zero;
|
||||
|
||||
public IEye? Eye => _eye;
|
||||
|
||||
@@ -85,14 +84,11 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public override Vector2 Offset
|
||||
{
|
||||
get => _offset;
|
||||
get => _eye?.Offset ?? default;
|
||||
set
|
||||
{
|
||||
if(_offset.EqualsApprox(value))
|
||||
return;
|
||||
|
||||
_offset = value;
|
||||
UpdateEyePosition();
|
||||
if (_eye != null)
|
||||
_eye.Offset = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +167,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
if (_eye == null) return;
|
||||
var mapPos = Owner.Transform.MapPosition;
|
||||
_eye.Position = new MapCoordinates(mapPos.Position + _offset, mapPos.MapId);
|
||||
_eye.Position = new MapCoordinates(mapPos.Position, mapPos.MapId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,15 +43,6 @@ namespace Robust.Client.GameObjects
|
||||
[Animatable]
|
||||
Color Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether we use RSI directions to rotate, or just get angular rotation applied.
|
||||
/// If true, all rotation to this sprite component is negated (that is rotation from say the owner being rotated).
|
||||
/// Rotation transformations on individual layers still apply.
|
||||
/// If false, all layers get locked to south and rotation is a transformation.
|
||||
/// </summary>
|
||||
[Obsolete("Use NoRotation and/or DirectionOverride")]
|
||||
bool Directional { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All sprite rotation is locked, and will always be drawn upright on
|
||||
/// the screen, regardless of world or view orientation.
|
||||
|
||||
@@ -114,23 +114,6 @@ namespace Robust.Client.GameObjects
|
||||
set => color = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether we use RSI directions to rotate, or just get angular rotation applied.
|
||||
/// If true, all rotation to this sprite component is negated (that is rotation from say the owner being rotated).
|
||||
/// Rotation transformations on individual layers still apply.
|
||||
/// If false, all layers get locked to south and rotation is a transformation.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Obsolete("Use NoRotation and/or DirectionOverride")]
|
||||
public bool Directional
|
||||
{
|
||||
get => _directional;
|
||||
set => _directional = value;
|
||||
}
|
||||
|
||||
[DataField("directional")]
|
||||
private bool _directional = true;
|
||||
|
||||
[ViewVariables]
|
||||
internal RenderingTreeComponent? RenderTree { get; set; } = null;
|
||||
|
||||
@@ -389,6 +372,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
if (layerDatums.Count != 0)
|
||||
{
|
||||
LayerMap.Clear();
|
||||
LayerDatums = layerDatums;
|
||||
}
|
||||
}
|
||||
@@ -402,7 +386,6 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
//deep copying things to avoid entanglement
|
||||
_baseRsi = other._baseRsi;
|
||||
_directional = other._directional;
|
||||
_visible = other._visible;
|
||||
_layerMapShared = other._layerMapShared;
|
||||
color = other.color;
|
||||
@@ -2068,36 +2051,35 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
{
|
||||
return GetPrototypeTextures(prototype, resourceCache, out var _);
|
||||
}
|
||||
|
||||
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
|
||||
{
|
||||
var results = new List<IDirectionalTextureProvider>();
|
||||
noRot = false;
|
||||
var icon = IconComponent.GetPrototypeIcon(prototype, resourceCache);
|
||||
if (icon != null)
|
||||
{
|
||||
yield return icon;
|
||||
yield break;
|
||||
results.Add(icon);
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!prototype.Components.TryGetValue("Sprite", out _))
|
||||
{
|
||||
yield return resourceCache.GetFallback<TextureResource>().Texture;
|
||||
yield break;
|
||||
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
|
||||
return results;
|
||||
}
|
||||
|
||||
var dummy = new DummyIconEntity { Prototype = prototype };
|
||||
var spriteComponent = dummy.AddComponent<SpriteComponent>();
|
||||
|
||||
if (prototype.Components.TryGetValue("Appearance", out _))
|
||||
{
|
||||
var appearanceComponent = dummy.AddComponent<AppearanceComponent>();
|
||||
foreach (var layer in appearanceComponent.Visualizers)
|
||||
{
|
||||
layer.InitializeEntity(dummy);
|
||||
layer.OnChangeData(appearanceComponent);
|
||||
}
|
||||
}
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace).Uid;
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
|
||||
var anyTexture = false;
|
||||
foreach (var layer in spriteComponent.AllLayers)
|
||||
{
|
||||
if (layer.Texture != null) yield return layer.Texture;
|
||||
if (layer.Texture != null)
|
||||
results.Add(layer.Texture);
|
||||
if (!layer.RsiState.IsValid || !layer.Visible) continue;
|
||||
|
||||
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
|
||||
@@ -2105,14 +2087,17 @@ namespace Robust.Client.GameObjects
|
||||
!rsi.TryGetState(layer.RsiState, out var state))
|
||||
continue;
|
||||
|
||||
yield return state;
|
||||
results.Add(state);
|
||||
anyTexture = true;
|
||||
}
|
||||
|
||||
dummy.Delete();
|
||||
noRot = spriteComponent.NoRotation;
|
||||
|
||||
entityManager.DeleteEntity(dummy);
|
||||
|
||||
if (!anyTexture)
|
||||
yield return resourceCache.GetFallback<TextureResource>().Texture;
|
||||
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
|
||||
return results;
|
||||
}
|
||||
|
||||
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
@@ -2125,144 +2110,14 @@ namespace Robust.Client.GameObjects
|
||||
return GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
var dummy = new DummyIconEntity { Prototype = prototype };
|
||||
var spriteComponent = dummy.AddComponent<SpriteComponent>();
|
||||
dummy.Delete();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace).Uid;
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
var result = spriteComponent.Icon ?? GetFallbackState(resourceCache);
|
||||
entityManager.DeleteEntity(dummy);
|
||||
|
||||
return spriteComponent.Icon ?? GetFallbackState(resourceCache);
|
||||
return result;
|
||||
}
|
||||
|
||||
#region DummyIconEntity
|
||||
private class DummyIconEntity : IEntity
|
||||
{
|
||||
public GameTick LastModifiedTick { get; } = GameTick.Zero;
|
||||
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;
|
||||
public bool Paused { get; set; }
|
||||
public EntityPrototype? Prototype { get; set; }
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public bool IsValid()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public ITransformComponent Transform { get; } = null!;
|
||||
public MetaDataComponent MetaData { get; } = null!;
|
||||
|
||||
private Dictionary<Type, IComponent> _components = new();
|
||||
private EntityLifeStage _lifeStage;
|
||||
|
||||
public T AddComponent<T>() where T : Component, new()
|
||||
{
|
||||
var typeFactory = IoCManager.Resolve<IDynamicTypeFactoryInternal>();
|
||||
var serializationManager = IoCManager.Resolve<ISerializationManager>();
|
||||
var comp = (T)typeFactory.CreateInstanceUnchecked(typeof(T));
|
||||
_components[typeof(T)] = comp;
|
||||
comp.Owner = this;
|
||||
|
||||
if (typeof(ISpriteComponent).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
_components[typeof(ISpriteComponent)] = comp;
|
||||
}
|
||||
|
||||
if (Prototype != null && Prototype.TryGetComponent<T>(comp.Name, out var node))
|
||||
{
|
||||
comp = serializationManager.Copy(node, comp)!;
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
public void RemoveComponent<T>()
|
||||
{
|
||||
_components.Remove(typeof(T));
|
||||
}
|
||||
|
||||
public bool HasComponent<T>()
|
||||
{
|
||||
return _components.ContainsKey(typeof(T));
|
||||
}
|
||||
|
||||
public bool HasComponent(Type type)
|
||||
{
|
||||
return _components.ContainsKey(type);
|
||||
}
|
||||
|
||||
public T GetComponent<T>()
|
||||
{
|
||||
return (T)_components[typeof(T)];
|
||||
}
|
||||
|
||||
public IComponent GetComponent(Type type)
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component) where T : class
|
||||
{
|
||||
component = null;
|
||||
if (!_components.TryGetValue(typeof(T), out var value)) return false;
|
||||
component = (T)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public T? GetComponentOrNull<T>() where T : class
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryGetComponent(Type type, [NotNullWhen(true)] out IComponent? component)
|
||||
{
|
||||
component = null;
|
||||
if (!_components.TryGetValue(type, out var value)) return false;
|
||||
component = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IComponent? GetComponentOrNull(Type type)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public void QueueDelete()
|
||||
{
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<IComponent> GetAllComponents()
|
||||
{
|
||||
return Enumerable.Empty<IComponent>();
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetAllComponents<T>()
|
||||
{
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public void SendMessage(IComponent? owner, ComponentMessage message)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public void SendNetworkMessage(IComponent owner, ComponentMessage message, INetChannel? channel = null)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dirty()
|
||||
{
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal sealed class SpriteUpdateEvent : EntityEventArgs
|
||||
|
||||
@@ -21,18 +21,24 @@ namespace Robust.Client.GameObjects
|
||||
[UsedImplicitly]
|
||||
internal class EyeUpdateSystem : EntitySystem
|
||||
{
|
||||
// How fast the camera rotates in radians
|
||||
private const float CameraRotateSpeed = MathF.PI;
|
||||
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private bool _isLerping = false;
|
||||
private ITransformComponent? _lastParent;
|
||||
|
||||
private TransformComponent? _lastParent;
|
||||
private TransformComponent? _lerpTo;
|
||||
private Angle LerpStartRotation;
|
||||
private float _accumulator;
|
||||
|
||||
public bool IsLerping { get => _lerpTo != null; }
|
||||
|
||||
// How fast the camera rotates in radians / s
|
||||
private const float CameraRotateSpeed = MathF.PI;
|
||||
// PER THIS AMOUNT OF TIME MILLISECONDS
|
||||
private const float CameraRotateTimeUnit = 1.2f;
|
||||
// Safety override
|
||||
private const float _lerpTimeMax = CameraRotateTimeUnit + 0.4f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -74,27 +80,54 @@ namespace Robust.Client.GameObjects
|
||||
gridEnt.Transform
|
||||
: _mapManager.GetMapEntity(playerTransform.MapID).Transform;
|
||||
|
||||
if (parent != _lastParent)
|
||||
// Make sure that we don't fire the vomit carousel when we spawn in
|
||||
if (_lastParent is null)
|
||||
_lastParent = parent;
|
||||
|
||||
// Set a default for target rotation
|
||||
var parentRotation = -parent.WorldRotation;
|
||||
// Reuse current rotation when stepping into space
|
||||
if (parent.GridID == GridId.Invalid)
|
||||
parentRotation = currentEye.Rotation;
|
||||
|
||||
// Handle grid change in general
|
||||
if (_lastParent != parent)
|
||||
_lerpTo = parent;
|
||||
|
||||
// And we are up and running!
|
||||
if (_lerpTo is not null)
|
||||
{
|
||||
// Handle a case where we have beeing spinning around, but suddenly got off onto a different grid
|
||||
if (parent != _lerpTo) {
|
||||
LerpStartRotation = currentEye.Rotation;
|
||||
_lerpTo = parent;
|
||||
_accumulator = 0f;
|
||||
}
|
||||
|
||||
_accumulator += frameTime;
|
||||
|
||||
if (_accumulator >= 0.3f)
|
||||
var changeNeeded = (float) (LerpStartRotation - parentRotation).Theta;
|
||||
|
||||
var changeLerp = _accumulator / (Math.Abs(changeNeeded % MathF.PI) / CameraRotateSpeed * CameraRotateTimeUnit);
|
||||
|
||||
currentEye.Rotation = Angle.Lerp(LerpStartRotation, parentRotation, changeLerp);
|
||||
|
||||
// Either we have overshot, or we have taken way too long on this, emergency reset time
|
||||
if (changeLerp > 1.0f || _accumulator >= _lerpTimeMax)
|
||||
{
|
||||
_accumulator = 0f;
|
||||
_lastParent = parent;
|
||||
_lerpTo = null;
|
||||
_accumulator = 0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// We are just fine, or we finished a lerp (and probably overshot)
|
||||
if (_lerpTo is null)
|
||||
{
|
||||
_lastParent = parent;
|
||||
currentEye.Rotation = parentRotation;
|
||||
LerpStartRotation = parentRotation;
|
||||
}
|
||||
|
||||
if (_lastParent == parent)
|
||||
{
|
||||
// TODO: Detect parent change and start lerping
|
||||
var parentRotation = parent.WorldRotation;
|
||||
currentEye.Rotation = -parentRotation;
|
||||
}
|
||||
|
||||
foreach (var eyeComponent in EntityManager.EntityQuery<EyeComponent>(true))
|
||||
{
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Robust.Client.GameObjects
|
||||
AnythingMovedSubHandler(args.Sender.Transform);
|
||||
}
|
||||
|
||||
private void AnythingMovedSubHandler(ITransformComponent sender)
|
||||
private void AnythingMovedSubHandler(TransformComponent sender)
|
||||
{
|
||||
// To avoid doing redundant updates (and we don't need to update a grid's children ever)
|
||||
if (!_checkedChildren.Add(sender.Owner.Uid) ||
|
||||
@@ -125,7 +125,7 @@ namespace Robust.Client.GameObjects
|
||||
if (sender.Owner.TryGetComponent(out PointLightComponent? light))
|
||||
QueueLightUpdate(light);
|
||||
|
||||
foreach (ITransformComponent child in sender.Children)
|
||||
foreach (TransformComponent child in sender.Children)
|
||||
{
|
||||
AnythingMovedSubHandler(child);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly RenderingTreeSystem _treeSystem = default!;
|
||||
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
private HashSet<ISpriteComponent> _manualUpdate = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -38,6 +39,12 @@ namespace Robust.Client.GameObjects
|
||||
sprite.DoUpdateIsInert();
|
||||
}
|
||||
|
||||
foreach (var sprite in _manualUpdate)
|
||||
{
|
||||
if (!sprite.Deleted && !sprite.IsInert)
|
||||
sprite.FrameUpdate(frameTime);
|
||||
}
|
||||
|
||||
var pvsBounds = _eyeManager.GetWorldViewbounds();
|
||||
|
||||
var currentMap = _eyeManager.CurrentMap;
|
||||
@@ -57,10 +64,21 @@ namespace Robust.Client.GameObjects
|
||||
return true;
|
||||
}
|
||||
|
||||
value.FrameUpdate(state);
|
||||
if (!_manualUpdate.Contains(value))
|
||||
value.FrameUpdate(state);
|
||||
return true;
|
||||
}, bounds, true);
|
||||
}
|
||||
|
||||
_manualUpdate.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force update of the sprite component next frame
|
||||
/// </summary>
|
||||
public void ForceUpdate(ISpriteComponent sprite)
|
||||
{
|
||||
_manualUpdate.Add(sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ namespace Robust.Client.Graphics
|
||||
internal set => _coords = value;
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 Offset { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Angle Rotation
|
||||
@@ -51,6 +54,15 @@ namespace Robust.Client.Graphics
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GetViewMatrix(out Matrix3 viewMatrix, Vector2 renderScale)
|
||||
{
|
||||
var scaleMat = Matrix3.CreateScale(_scale.X * renderScale.X, _scale.Y * renderScale.Y);
|
||||
var rotMat = Matrix3.CreateRotation(_rotation);
|
||||
var transMat = Matrix3.CreateTranslation(-_coords.Position - Offset);
|
||||
|
||||
viewMatrix = transMat * rotMat * scaleMat;
|
||||
}
|
||||
|
||||
public void GetViewMatrixNoOffset(out Matrix3 viewMatrix, Vector2 renderScale)
|
||||
{
|
||||
var scaleMat = Matrix3.CreateScale(_scale.X * renderScale.X, _scale.Y * renderScale.Y);
|
||||
var rotMat = Matrix3.CreateRotation(_rotation);
|
||||
|
||||
@@ -21,6 +21,11 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
MapCoordinates Position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Translation offset from <see cref="Position"/>. Does not influence the center of FOV.
|
||||
/// </summary>
|
||||
Vector2 Offset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation of the camera around the Z axis.
|
||||
/// </summary>
|
||||
@@ -51,5 +56,7 @@ namespace Robust.Client.Graphics
|
||||
/// <param name="viewMatrixInv">Inverted view matrix for this camera.</param>
|
||||
/// <param name="renderScale"></param>
|
||||
void GetViewMatrixInv(out Matrix3 viewMatrixInv, Vector2 renderScale);
|
||||
|
||||
void GetViewMatrixNoOffset(out Matrix3 viewMatrix, Vector2 renderScale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.Audio.OpenAL;
|
||||
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenToolkit.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -155,6 +156,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var (x, y) = eye.Position.Position;
|
||||
AL.Listener(ALListener3f.Position, x, y, -5);
|
||||
var rot2d = eye.Rotation.ToVec();
|
||||
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
|
||||
|
||||
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
|
||||
var (rotX, rotY) = eye.Rotation.ToVec();
|
||||
var at = new Vector3(0f, 0f, -1f);
|
||||
var up = new Vector3(rotY, rotX, 0f);
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
|
||||
// Clear out finalized audio sources.
|
||||
while (_sourceDisposeQueue.TryDequeue(out var handles))
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
continue;
|
||||
}
|
||||
|
||||
var transform = _entityManager.GetComponent<ITransformComponent>(grid.GridEntityId);
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(grid.GridEntityId);
|
||||
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
|
||||
grid.GetMapChunks(worldBounds, out var enumerator);
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
SwapAllBuffers();
|
||||
}
|
||||
|
||||
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox)
|
||||
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
|
||||
{
|
||||
using (DebugGroup($"Overlays: {space}"))
|
||||
{
|
||||
@@ -116,7 +116,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
ClearFramebuffer(default);
|
||||
}
|
||||
|
||||
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox);
|
||||
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox, worldBounds);
|
||||
}
|
||||
|
||||
FlushRenderQueue();
|
||||
@@ -132,8 +132,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var list = GetOverlaysForSpace(space);
|
||||
|
||||
var worldBounds = CalcWorldAABB(vp);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldBounds);
|
||||
var worldAABB = CalcWorldAABB(vp);
|
||||
var worldBounds = CalcWorldBounds(vp);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldAABB, worldBounds);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
@@ -211,7 +212,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return;
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB);
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB, worldBounds);
|
||||
|
||||
var screenSize = viewport.Size;
|
||||
eye.GetViewMatrix(out var eyeMatrix, eye.Scale);
|
||||
@@ -264,7 +265,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
null,
|
||||
viewport,
|
||||
new UIBox2i((0, 0), viewport.Size),
|
||||
worldAABB);
|
||||
worldAABB,
|
||||
worldBounds);
|
||||
overlayIndex = j;
|
||||
continue;
|
||||
}
|
||||
@@ -471,7 +473,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
DrawLightsAndFov(viewport, worldBounds, worldAABB, eye);
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB);
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB, worldBounds);
|
||||
|
||||
using (DebugGroup("Grids"))
|
||||
{
|
||||
@@ -484,7 +486,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
DrawEntities(viewport, worldBounds, worldAABB, eye);
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB);
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
|
||||
{
|
||||
@@ -517,7 +519,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB);
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
|
||||
|
||||
_currentViewport = oldVp;
|
||||
});
|
||||
@@ -529,12 +531,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (eye == null)
|
||||
return default;
|
||||
|
||||
return GetAABB(eye, viewport);
|
||||
// Will be larger than the actual viewport due to rotation.
|
||||
return CalcWorldBounds(viewport).CalcBoundingBox();
|
||||
}
|
||||
|
||||
private static Box2 GetAABB(IEye eye, Viewport viewport)
|
||||
{
|
||||
return Box2.CenteredAround(eye.Position.Position,
|
||||
return Box2.CenteredAround(eye.Position.Position + eye.Offset,
|
||||
viewport.Size / viewport.RenderScale / EyeManager.PixelsPerMeter * eye.Zoom);
|
||||
}
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
var (lights, count, expandedBounds) = GetLightsToRender(mapId, worldBounds, worldAABB);
|
||||
eye.GetViewMatrix(out var eyeTransform, eye.Scale);
|
||||
eye.GetViewMatrixNoOffset(out var eyeTransform, eye.Scale);
|
||||
|
||||
UpdateOcclusionGeometry(mapId, expandedBounds, eyeTransform);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Text;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -143,6 +144,61 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StackedFont : Font
|
||||
{
|
||||
// _main is the "default" font; the top of the Stack.
|
||||
public readonly Font _main;
|
||||
public readonly Font[] Stack;
|
||||
|
||||
public StackedFont(params Font[] args)
|
||||
{
|
||||
if (args.Length < 1)
|
||||
throw new ArgumentException("At least one font is required");
|
||||
|
||||
Stack = args;
|
||||
_main = args[0];
|
||||
}
|
||||
|
||||
// All metrics methods use the default font (_main).
|
||||
// Technically these could vary between stacked fonts, but that is a case
|
||||
// that really should already be avoided for so many other reasons.
|
||||
public override int GetAscent(float scale) => _main.GetAscent(scale);
|
||||
public override int GetHeight(float scale) => _main.GetHeight(scale);
|
||||
public override int GetDescent(float scale) => _main.GetDescent(scale);
|
||||
public override int GetLineHeight(float scale) => _main.GetLineHeight(scale);
|
||||
|
||||
// DrawChar just proxies to the stack, or invokes _main's fallback.
|
||||
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
|
||||
{
|
||||
foreach (var f in Stack)
|
||||
{
|
||||
var w = f.DrawChar(handle, rune, baseline, scale, color, fallback: false);
|
||||
if (w != 0f)
|
||||
return w;
|
||||
}
|
||||
|
||||
if (fallback)
|
||||
return _main.DrawChar(handle, rune, baseline, scale, color, fallback: true);
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
|
||||
{
|
||||
foreach (var f in Stack)
|
||||
{
|
||||
var m = f.GetCharMetrics(rune, scale, fallback: false);
|
||||
if (m != null)
|
||||
return m;
|
||||
}
|
||||
|
||||
if (!Rune.IsWhiteSpace(rune) && fallback)
|
||||
return _main.GetCharMetrics(rune, scale, fallback: true);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DummyFont : Font
|
||||
{
|
||||
public override int GetAscent(float scale) => default;
|
||||
|
||||
@@ -84,7 +84,8 @@ namespace Robust.Client.Graphics
|
||||
IViewportControl? vpControl,
|
||||
IClydeViewport vp,
|
||||
in UIBox2i screenBox,
|
||||
in Box2 worldBox)
|
||||
in Box2 worldBox,
|
||||
in Box2Rotated worldBounds)
|
||||
{
|
||||
DrawingHandleBase handle;
|
||||
if (currentSpace == OverlaySpace.ScreenSpace || currentSpace == OverlaySpace.ScreenSpaceBelowWorld)
|
||||
@@ -97,7 +98,7 @@ namespace Robust.Client.Graphics
|
||||
handle = renderHandle.DrawingHandleWorld;
|
||||
}
|
||||
|
||||
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, worldBox);
|
||||
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, worldBox, worldBounds);
|
||||
|
||||
Draw(args);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,12 @@ namespace Robust.Client.Graphics
|
||||
/// <summary>
|
||||
/// AABB enclosing the area visible in the viewport.
|
||||
/// </summary>
|
||||
public readonly Box2 WorldBounds;
|
||||
public readonly Box2 WorldAABB;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Box2Rotated"/> of the area visible in the viewport.
|
||||
/// </summary>
|
||||
public readonly Box2Rotated WorldBounds;
|
||||
|
||||
public DrawingHandleScreen ScreenHandle => (DrawingHandleScreen) DrawingHandle;
|
||||
public DrawingHandleWorld WorldHandle => (DrawingHandleWorld) DrawingHandle;
|
||||
@@ -52,13 +57,15 @@ namespace Robust.Client.Graphics
|
||||
IClydeViewport viewport,
|
||||
DrawingHandleBase drawingHandle,
|
||||
in UIBox2i viewportBounds,
|
||||
in Box2 worldBounds)
|
||||
in Box2 worldAabb,
|
||||
in Box2Rotated worldBounds)
|
||||
{
|
||||
Space = space;
|
||||
ViewportControl = viewportControl;
|
||||
Viewport = viewport;
|
||||
DrawingHandle = drawingHandle;
|
||||
ViewportBounds = viewportBounds;
|
||||
WorldAABB = worldAabb;
|
||||
WorldBounds = worldBounds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ namespace Robust.Client.Graphics
|
||||
// 2D array for the texture to use for each animation frame at each direction.
|
||||
public readonly Texture[][] Icons;
|
||||
|
||||
internal State(Vector2i size, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons)
|
||||
internal State(Vector2i size, RSI rsi, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons)
|
||||
{
|
||||
DebugTools.Assert(size.X > 0);
|
||||
DebugTools.Assert(size.Y > 0);
|
||||
DebugTools.Assert(stateId.IsValid);
|
||||
|
||||
Size = size;
|
||||
RSI = rsi;
|
||||
StateId = stateId;
|
||||
Directions = direction;
|
||||
Delays = delays;
|
||||
@@ -47,6 +48,11 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
public Vector2i Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The source RSI of this state.
|
||||
/// </summary>
|
||||
public RSI RSI { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The identifier for this state inside an RSI.
|
||||
/// </summary>
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace Robust.Client.Input
|
||||
common.AddFunction(EngineKeyFunctions.TextReleaseFocus);
|
||||
common.AddFunction(EngineKeyFunctions.TextScrollToBottom);
|
||||
common.AddFunction(EngineKeyFunctions.TextDelete);
|
||||
common.AddFunction(EngineKeyFunctions.TextTabComplete);
|
||||
|
||||
var editor = contexts.New("editor", common);
|
||||
editor.AddFunction(EngineKeyFunctions.EditorLinePlace);
|
||||
|
||||
@@ -95,9 +95,21 @@ namespace Robust.Client.Placement
|
||||
private ShaderInstance? _drawingShader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The texture we use to show from our placement manager to represent the entity to place
|
||||
/// The entity for placement overlay.
|
||||
/// Colour of this gets swapped around in PlacementMode.
|
||||
/// This entity needs to stay in nullspace.
|
||||
/// </summary>
|
||||
public List<IDirectionalTextureProvider>? CurrentTextures { get; set; }
|
||||
public IEntity? CurrentPlacementOverlayEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A BAD way to explicitly control the icons used!!!
|
||||
/// Need to fix Content for this
|
||||
/// </summary>
|
||||
public List<IDirectionalTextureProvider>? CurrentTextures {
|
||||
set {
|
||||
PreparePlacementTexList(value, value != null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which of the placement orientations we are trying to place with
|
||||
@@ -330,7 +342,7 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
PlacementChanged?.Invoke(this, EventArgs.Empty);
|
||||
Hijack = null;
|
||||
CurrentTextures = null;
|
||||
EnsureNoPlacementOverlayEntity();
|
||||
CurrentPrototype = null;
|
||||
CurrentPermission = null;
|
||||
CurrentMode = null;
|
||||
@@ -362,8 +374,6 @@ namespace Robust.Client.Placement
|
||||
Direction = Direction.North;
|
||||
break;
|
||||
}
|
||||
|
||||
CurrentMode?.SetSprite();
|
||||
}
|
||||
|
||||
public void HandlePlacement()
|
||||
@@ -647,21 +657,64 @@ namespace Robust.Client.Placement
|
||||
BeginPlacing(CurrentPermission);
|
||||
}
|
||||
|
||||
private void EnsureNoPlacementOverlayEntity()
|
||||
{
|
||||
if (CurrentPlacementOverlayEntity != null)
|
||||
{
|
||||
if (!CurrentPlacementOverlayEntity.Deleted)
|
||||
CurrentPlacementOverlayEntity.Delete();
|
||||
CurrentPlacementOverlayEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
private SpriteComponent SetupPlacementOverlayEntity()
|
||||
{
|
||||
EnsureNoPlacementOverlayEntity();
|
||||
CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
return CurrentPlacementOverlayEntity.EnsureComponent<SpriteComponent>();
|
||||
}
|
||||
|
||||
private void PreparePlacement(string templateName)
|
||||
{
|
||||
var prototype = _prototypeManager.Index<EntityPrototype>(templateName);
|
||||
|
||||
CurrentTextures = SpriteComponent.GetPrototypeTextures(prototype, ResourceCache).ToList();
|
||||
CurrentPrototype = prototype;
|
||||
|
||||
IsActive = true;
|
||||
|
||||
var lst = SpriteComponent.GetPrototypeTextures(prototype, ResourceCache, out var noRot).ToList();
|
||||
PreparePlacementTexList(lst, noRot);
|
||||
}
|
||||
|
||||
public void PreparePlacementTexList(List<IDirectionalTextureProvider>? texs, bool noRot)
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
if (texs != null)
|
||||
{
|
||||
// This one covers most cases (including Construction)
|
||||
foreach (var v in texs)
|
||||
{
|
||||
if (v is RSI.State)
|
||||
{
|
||||
var st = (RSI.State) v;
|
||||
sc.AddLayer(st.StateId, st.RSI);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
sc.AddLayer(v.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sc.AddLayer(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png"));
|
||||
}
|
||||
sc.NoRotation = noRot;
|
||||
}
|
||||
|
||||
private void PreparePlacementTile()
|
||||
{
|
||||
CurrentTextures = new List<IDirectionalTextureProvider>
|
||||
{ResourceCache
|
||||
.GetResource<TextureResource>(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png")).Texture};
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.AddLayer(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png"));
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
@@ -85,14 +86,10 @@ namespace Robust.Client.Placement
|
||||
|
||||
public virtual void Render(DrawingHandleWorld handle)
|
||||
{
|
||||
if (TexturesToDraw == null)
|
||||
{
|
||||
SetSprite();
|
||||
DebugTools.AssertNotNull(TexturesToDraw);
|
||||
}
|
||||
|
||||
if (TexturesToDraw == null || TexturesToDraw.Count == 0)
|
||||
var sce = pManager.CurrentPlacementOverlayEntity;
|
||||
if (sce == null || sce.Deleted)
|
||||
return;
|
||||
var sc = sce.GetComponent<SpriteComponent>();
|
||||
|
||||
IEnumerable<EntityCoordinates> locationcollection;
|
||||
switch (pManager.PlacementType)
|
||||
@@ -111,17 +108,17 @@ namespace Robust.Client.Placement
|
||||
break;
|
||||
}
|
||||
|
||||
var size = TexturesToDraw[0].Size;
|
||||
var dirAng = pManager.Direction.ToAngle();
|
||||
foreach (var coordinate in locationcollection)
|
||||
{
|
||||
if (!coordinate.IsValid(pManager.EntityManager))
|
||||
return; // Just some paranoia just in case
|
||||
var entity = coordinate.GetEntity(pManager.EntityManager);
|
||||
var worldPos = coordinate.ToMapPos(pManager.EntityManager);
|
||||
var pos = worldPos - (size/(float)EyeManager.PixelsPerMeter) / 2f;
|
||||
var color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
var worldRot = entity.Transform.WorldRotation + dirAng;
|
||||
|
||||
foreach (var texture in TexturesToDraw)
|
||||
{
|
||||
handle.DrawTexture(texture, pos, color);
|
||||
}
|
||||
sc.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
sc.Render(handle, pManager.eyeManager.CurrentEye.Rotation, worldRot, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,14 +190,6 @@ namespace Robust.Client.Placement
|
||||
return pManager.ResourceCache.TryGetResource(new ResourcePath(@"/Textures/") / key, out sprite);
|
||||
}
|
||||
|
||||
public void SetSprite()
|
||||
{
|
||||
if (pManager.CurrentTextures == null)
|
||||
return;
|
||||
|
||||
TexturesToDraw = pManager.CurrentTextures.Select(o => o.TextureFor(pManager.Direction)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player is spawning within a certain range of his character if range is required on this mode
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client.CEF")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
|
||||
[assembly: InternalsVisibleTo("Robust.Lite")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Robust.Client.ResourceManagement
|
||||
reg.Indices = foldedIndices;
|
||||
reg.Offsets = callbackOffset;
|
||||
|
||||
var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays,
|
||||
var state = new RSI.State(frameSize, rsi, stateObject.StateId, stateObject.DirType, foldedDelays,
|
||||
textures);
|
||||
rsi.AddState(state);
|
||||
|
||||
|
||||
@@ -998,14 +998,4 @@ namespace Robust.Client.UserInterface
|
||||
public readonly int OldIndex;
|
||||
public readonly int NewIndex;
|
||||
}
|
||||
|
||||
public enum AccessLevel
|
||||
{
|
||||
Public,
|
||||
Protected,
|
||||
Internal,
|
||||
ProtectedInternal,
|
||||
Private,
|
||||
PrivateProtected,
|
||||
}
|
||||
}
|
||||
|
||||
12
Robust.Client/UserInterface/ControlPropertyAccess.cs
Normal file
12
Robust.Client/UserInterface/ControlPropertyAccess.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
public enum AccessLevel
|
||||
{
|
||||
Public,
|
||||
Protected,
|
||||
Internal,
|
||||
ProtectedInternal,
|
||||
Private,
|
||||
PrivateProtected,
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public event Action<LineEditEventArgs>? OnTextEntered;
|
||||
public event Action<LineEditEventArgs>? OnFocusEnter;
|
||||
public event Action<LineEditEventArgs>? OnFocusExit;
|
||||
public event Action<LineEditEventArgs>? OnTabComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the LineEdit text gets changed by the input text.
|
||||
@@ -523,6 +524,15 @@ namespace Robust.Client.UserInterface.Controls
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.TextTabComplete)
|
||||
{
|
||||
if (Editable)
|
||||
{
|
||||
OnTabComplete?.Invoke(new LineEditEventArgs(this, _text));
|
||||
}
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
@@ -48,6 +51,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
EntitySystem.Get<SpriteSystem>().ForceUpdate(Sprite);
|
||||
renderHandle.DrawEntity(Sprite.Owner, PixelSize / 2, Scale * UIScale, OverrideDirection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,22 +130,29 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
var (left, top) = Position;
|
||||
var (right, bottom) = Position + SetSize;
|
||||
|
||||
if (float.IsNaN(SetSize.X)) {
|
||||
right = Position.X + Size.X;
|
||||
}
|
||||
if (float.IsNaN(SetSize.Y)) {
|
||||
bottom = Position.Y + Size.Y;
|
||||
}
|
||||
|
||||
if ((CurrentDrag & DragMode.Top) == DragMode.Top)
|
||||
{
|
||||
top = Math.Min(args.GlobalPosition.Y - DragOffsetTopLeft.Y, bottom);
|
||||
top = Math.Min(args.GlobalPosition.Y - DragOffsetTopLeft.Y, Math.Min(bottom, bottom - MinSize.Y));
|
||||
}
|
||||
else if ((CurrentDrag & DragMode.Bottom) == DragMode.Bottom)
|
||||
{
|
||||
bottom = Math.Max(args.GlobalPosition.Y + DragOffsetBottomRight.Y, top);
|
||||
bottom = Math.Max(args.GlobalPosition.Y + DragOffsetBottomRight.Y, Math.Max(top, top + MinSize.Y));
|
||||
}
|
||||
|
||||
if ((CurrentDrag & DragMode.Left) == DragMode.Left)
|
||||
{
|
||||
left = Math.Min(args.GlobalPosition.X - DragOffsetTopLeft.X, right);
|
||||
left = Math.Min(args.GlobalPosition.X - DragOffsetTopLeft.X, Math.Min(right, right - MinSize.X));
|
||||
}
|
||||
else if ((CurrentDrag & DragMode.Right) == DragMode.Right)
|
||||
{
|
||||
right = Math.Max(args.GlobalPosition.X + DragOffsetBottomRight.X, left);
|
||||
right = Math.Max(args.GlobalPosition.X + DragOffsetBottomRight.X, Math.Max(left, left + MinSize.X));
|
||||
}
|
||||
|
||||
var rect = new UIBox2(left, top, right, bottom);
|
||||
|
||||
@@ -15,18 +15,26 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
public sealed partial class DropDownDebugConsole : Control
|
||||
{
|
||||
private bool _targetVisible;
|
||||
private float _curAnchorOffset = -ScreenRatio;
|
||||
|
||||
public const float ScreenRatio = 0.35f;
|
||||
|
||||
public DropDownDebugConsole()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
LayoutContainer.SetAnchorPreset(MainControl, LayoutContainer.LayoutPreset.TopWide);
|
||||
LayoutContainer.SetAnchorBottom(MainControl, 0.35f);
|
||||
|
||||
MainControl.CommandBar.OnKeyBindDown += CommandBarPubOnOnKeyBindDown;
|
||||
|
||||
}
|
||||
|
||||
private void UpdateAnchorOffset()
|
||||
{
|
||||
LayoutContainer.SetAnchorBottom(MainControl, _curAnchorOffset+ScreenRatio);
|
||||
LayoutContainer.SetAnchorTop(MainControl, _curAnchorOffset);
|
||||
}
|
||||
|
||||
private void CommandBarPubOnOnKeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.ShowDebugConsole)
|
||||
@@ -50,24 +58,23 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
return;
|
||||
}
|
||||
|
||||
var targetLocation = _targetVisible ? 0 : -MainControl.Height;
|
||||
var (posX, posY) = MainControl.Position;
|
||||
var targetOffset = _targetVisible ? 0 : -ScreenRatio;
|
||||
|
||||
if (Math.Abs(targetLocation - posY) <= 1)
|
||||
if (MathHelper.CloseTo(targetOffset, _curAnchorOffset))
|
||||
{
|
||||
if (!_targetVisible)
|
||||
{
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
posY = targetLocation;
|
||||
_curAnchorOffset = targetOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
posY = MathHelper.Lerp(posY, targetLocation, args.DeltaSeconds * 20);
|
||||
_curAnchorOffset = MathHelper.Lerp(_curAnchorOffset, targetOffset, args.DeltaSeconds * 20);
|
||||
}
|
||||
|
||||
LayoutContainer.SetPosition(MainControl, (posX, posY));
|
||||
UpdateAnchorOffset();
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Client.Console;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls
|
||||
@@ -10,6 +11,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
protected OutputPanel OutputPanel { get; }
|
||||
protected HistoryLineEdit InputBar { get; }
|
||||
protected Button RunButton { get; }
|
||||
protected Completions Suggestions { get; }
|
||||
|
||||
protected ScriptConsole()
|
||||
{
|
||||
@@ -47,11 +49,16 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
}
|
||||
});
|
||||
|
||||
Suggestions = new Completions(InputBar);
|
||||
InputBar.OnTabComplete += _ => Complete();
|
||||
InputBar.OnTextChanged += _ => Suggestions.TextChanged();
|
||||
InputBar.OnTextEntered += _ => Run();
|
||||
RunButton.OnPressed += _ => Run();
|
||||
MinSize = (550, 300);
|
||||
}
|
||||
|
||||
protected abstract void Complete();
|
||||
|
||||
protected abstract void Run();
|
||||
|
||||
protected override void Opened()
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
using System.Text;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows a control to listen for raw keyboard events. This allows bypassing the input binding system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Raw key events are raised *after* keybindings and focusing has been calculated,
|
||||
/// but before key bind events are actually raised.
|
||||
/// This is necessary to allow UI system stuff to actually work correctly.
|
||||
/// </remarks>
|
||||
internal interface IRawInputControl
|
||||
{
|
||||
/// <param name="guiRawEvent"></param>
|
||||
/// <returns>If true: all further key bind events should be blocked.</returns>
|
||||
bool RawKeyEvent(in GuiRawKeyEvent guiRawEvent) => false;
|
||||
// bool RawCharEvent(in GuiRawCharEvent guiRawCharEvent) => false;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
};
|
||||
top.HorizontalExpand = true;
|
||||
hBox.AddChild(top);
|
||||
hBox.AddChild(new SpriteView {Sprite = sprite});
|
||||
hBox.AddChild(new SpriteView {Sprite = sprite, OverrideDirection = Direction.South});
|
||||
vBoxContainer.AddChild(hBox);
|
||||
}
|
||||
else
|
||||
|
||||
25
Robust.Client/WebViewHook/IWebViewManagerHook.cs
Normal file
25
Robust.Client/WebViewHook/IWebViewManagerHook.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.WebViewHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Used so that the IWebViewManager can be loaded when loading Robust.Client.WebView.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
internal sealed class WebViewManagerImplAttribute : Attribute
|
||||
{
|
||||
public readonly Type ImplementationType;
|
||||
|
||||
public WebViewManagerImplAttribute(Type implementationType)
|
||||
{
|
||||
ImplementationType = implementationType;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IWebViewManagerHook
|
||||
{
|
||||
void Initialize(GameController.DisplayMode mode);
|
||||
void Update();
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
162
Robust.Server/Bql/BqlQueryManager.Parsers.cs
Normal file
162
Robust.Server/Bql/BqlQueryManager.Parsers.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Pidgin;
|
||||
using Robust.Shared.GameObjects;
|
||||
using static Pidgin.Parser;
|
||||
using static Pidgin.Parser<char>;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
public partial class BqlQueryManager
|
||||
{
|
||||
private readonly Dictionary<Type, Parser<char, BqlQuerySelectorParsed>> _parsers = new();
|
||||
private Parser<char, BqlQuerySelectorParsed> _allQuerySelectors = default!;
|
||||
private Parser<char, (IEnumerable<BqlQuerySelectorParsed>, string)> SimpleQuery => Parser.Map((en, _, rest) => (en, rest), SkipWhitespaces.Then(_allQuerySelectors).Many(), String("do").Then(SkipWhitespaces), Any.ManyString());
|
||||
|
||||
private static Parser<char, string> Word => OneOf(LetterOrDigit, Char('_')).ManyString();
|
||||
|
||||
private static Parser<char, object> Objectify<T>(Parser<char, T> inp)
|
||||
{
|
||||
return Parser.Map(x => (object) x!, inp);
|
||||
}
|
||||
|
||||
private struct SubstitutionData
|
||||
{
|
||||
public string Name;
|
||||
|
||||
public SubstitutionData(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
private static Parser<char, SubstitutionData> Substitution =>
|
||||
Try(Char('$').Then(OneOf(Uppercase, Char('_')).ManyString()))
|
||||
.MapWithInput((x, _) => new SubstitutionData(x.ToString()));
|
||||
|
||||
private static Parser<char, int> Integer =>
|
||||
Try(Int(10));
|
||||
|
||||
private static Parser<char, object> SubstitutableInteger =>
|
||||
Objectify(Integer).Or(Objectify(Try(Substitution)));
|
||||
|
||||
private static Parser<char, double> Float =>
|
||||
Try(Real);
|
||||
|
||||
private static Parser<char, object> SubstitutableFloat =>
|
||||
Objectify(Float).Or(Objectify(Try(Substitution)));
|
||||
|
||||
private static Parser<char, double> Percentage =>
|
||||
Try(Real).Before(Char('%'));
|
||||
|
||||
private static Parser<char, object> SubstitutablePercentage =>
|
||||
Objectify(Percentage).Or(Objectify(Try(Substitution)));
|
||||
|
||||
private static Parser<char, EntityUid> EntityId =>
|
||||
Try(Parser.Map(x => new EntityUid(x), Int(10)));
|
||||
|
||||
private static Parser<char, object> SubstitutableEntityId =>
|
||||
Objectify(EntityId).Or(Objectify(Try(Substitution)));
|
||||
|
||||
private Parser<char, Type> Component =>
|
||||
Try(Parser.Map(t => _componentFactory.GetRegistration(t).Type, Word));
|
||||
|
||||
private Parser<char, object> SubstitutableComponent =>
|
||||
Objectify(Component).Or(Objectify(Try(Substitution)));
|
||||
|
||||
private static Parser<char, string> QuotedString =>
|
||||
OneOf(Try(Char('"').Then(OneOf(new []
|
||||
{
|
||||
AnyCharExcept("\"")
|
||||
}).ManyString().Before(Char('"')))), Try(Word));
|
||||
|
||||
private static Parser<char, object> SubstitutableString =>
|
||||
Objectify(QuotedString).Or(Objectify(Try(Substitution)));
|
||||
|
||||
// thing to make sure it all compiles.
|
||||
[UsedImplicitly]
|
||||
private Parser<char, object> TypeSystemCheck =>
|
||||
OneOf(new[]
|
||||
{
|
||||
Objectify(Integer),
|
||||
Objectify(Percentage),
|
||||
Objectify(EntityId),
|
||||
Objectify(Component),
|
||||
Objectify(Float),
|
||||
Objectify(QuotedString)
|
||||
});
|
||||
|
||||
private Parser<char, BqlQuerySelectorParsed> BuildBqlQueryParser(BqlQuerySelector inst)
|
||||
{
|
||||
if (inst.Arguments.Length == 0)
|
||||
{
|
||||
return Parser.Map(_ => new BqlQuerySelectorParsed(new List<object>(), inst.Token, false), SkipWhitespaces);
|
||||
}
|
||||
|
||||
List<Parser<char, object>> argsParsers = new();
|
||||
|
||||
foreach (var (arg, _) in inst.Arguments.Select((x, i) => (x, i)))
|
||||
{
|
||||
List<Parser<char, object>> choices = new();
|
||||
if ((arg & QuerySelectorArgument.String) == QuerySelectorArgument.String)
|
||||
{
|
||||
choices.Add(Try(SubstitutableString.Before(SkipWhitespaces).Labelled("string argument")));
|
||||
}
|
||||
if ((arg & QuerySelectorArgument.Component) == QuerySelectorArgument.Component)
|
||||
{
|
||||
choices.Add(Try(SubstitutableComponent.Before(SkipWhitespaces).Labelled("component argument")));
|
||||
}
|
||||
if ((arg & QuerySelectorArgument.EntityId) == QuerySelectorArgument.EntityId)
|
||||
{
|
||||
choices.Add(Try(SubstitutableEntityId.Before(SkipWhitespaces).Labelled("entity ID argument")));
|
||||
}
|
||||
if ((arg & QuerySelectorArgument.Integer) == QuerySelectorArgument.Integer)
|
||||
{
|
||||
choices.Add(Try(SubstitutableInteger.Before(SkipWhitespaces).Labelled("integer argument")));
|
||||
}
|
||||
if ((arg & QuerySelectorArgument.Percentage) == QuerySelectorArgument.Percentage)
|
||||
{
|
||||
choices.Add(Try(SubstitutablePercentage.Before(SkipWhitespaces).Labelled("percentage argument")));
|
||||
}
|
||||
if ((arg & QuerySelectorArgument.Float) == QuerySelectorArgument.Float)
|
||||
{
|
||||
choices.Add(Try(SubstitutableFloat.Before(SkipWhitespaces).Labelled("float argument")));
|
||||
}
|
||||
|
||||
argsParsers.Add(OneOf(choices));
|
||||
}
|
||||
|
||||
Parser<char, List<object>> finalParser = argsParsers[0].Map(x => new List<object> { x });
|
||||
|
||||
for (var i = 1; i < argsParsers.Count; i++)
|
||||
{
|
||||
finalParser = finalParser.Then(argsParsers[i], (list, o) =>
|
||||
{
|
||||
list.Add(o);
|
||||
return list;
|
||||
}).Labelled("arguments");
|
||||
}
|
||||
|
||||
return Parser.Map(args => new BqlQuerySelectorParsed(args, inst.Token, false), finalParser);
|
||||
}
|
||||
|
||||
private void DoParserSetup()
|
||||
{
|
||||
foreach (var inst in _instances)
|
||||
{
|
||||
_parsers.Add(inst.GetType(), BuildBqlQueryParser(inst));
|
||||
}
|
||||
|
||||
_allQuerySelectors = Parser.Map((a,b) => (a,b), Try(String("not").Before(Char(' '))).Optional(), OneOf(_instances.Select(x =>
|
||||
Try(String(x.Token).Before(Char(' '))))).Then(tok =>
|
||||
_parsers[_queriesByToken[tok].GetType()])
|
||||
).Map(pair =>
|
||||
{
|
||||
pair.b.Inverted = pair.a.HasValue;
|
||||
return pair.b;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Robust.Server/Bql/BqlQueryManager.SimpleExecutor.cs
Normal file
40
Robust.Server/Bql/BqlQueryManager.SimpleExecutor.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Pidgin;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
public partial class BqlQueryManager
|
||||
{
|
||||
public (IEnumerable<EntityUid>, string) SimpleParseAndExecute(string query)
|
||||
{
|
||||
var parsed = SimpleQuery.Parse(query);
|
||||
if (parsed.Success)
|
||||
{
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var selectors = parsed.Value.Item1.ToArray();
|
||||
if (selectors.Length == 0)
|
||||
{
|
||||
return (entityManager.GetEntityUids(), parsed.Value.Item2);
|
||||
}
|
||||
|
||||
var entities = _queriesByToken[selectors[0].Token]
|
||||
.DoInitialSelection(selectors[0].Arguments, selectors[0].Inverted, entityManager);
|
||||
|
||||
foreach (var sel in selectors[1..])
|
||||
{
|
||||
entities = _queriesByToken[sel.Token].DoSelection(entities, sel.Arguments, sel.Inverted, entityManager);
|
||||
}
|
||||
|
||||
return (entities, parsed.Value.Item2);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(parsed.Error!.RenderErrorMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Robust.Server/Bql/BqlQueryManager.cs
Normal file
49
Robust.Server/Bql/BqlQueryManager.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
public partial class BqlQueryManager : IBqlQueryManager
|
||||
{
|
||||
private readonly IReflectionManager _reflectionManager;
|
||||
private readonly IComponentFactory _componentFactory;
|
||||
|
||||
private readonly List<BqlQuerySelector> _instances = new();
|
||||
private readonly Dictionary<string, BqlQuerySelector> _queriesByToken = new();
|
||||
|
||||
public BqlQueryManager()
|
||||
{
|
||||
_reflectionManager = IoCManager.Resolve<IReflectionManager>();
|
||||
_componentFactory = IoCManager.Resolve<IComponentFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically registers all query selectors with the parser/executor.
|
||||
/// </summary>
|
||||
public void DoAutoRegistrations()
|
||||
{
|
||||
foreach (var type in _reflectionManager.FindTypesWithAttribute<RegisterBqlQuerySelectorAttribute>())
|
||||
{
|
||||
RegisterClass(type);
|
||||
}
|
||||
|
||||
DoParserSetup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internally registers the given <see cref="BqlQuerySelector"/>.
|
||||
/// </summary>
|
||||
/// <param name="bqlQuerySelector">The selector to register</param>
|
||||
private void RegisterClass(Type bqlQuerySelector)
|
||||
{
|
||||
DebugTools.Assert(bqlQuerySelector.BaseType == typeof(BqlQuerySelector));
|
||||
var inst = (BqlQuerySelector)Activator.CreateInstance(bqlQuerySelector)!;
|
||||
_instances.Add(inst);
|
||||
_queriesByToken.Add(inst.Token, inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
329
Robust.Server/Bql/BqlQuerySelector.Builtin.cs
Normal file
329
Robust.Server/Bql/BqlQuerySelector.Builtin.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
[RegisterBqlQuerySelector]
|
||||
public class WithQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "with";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.Component };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var comp = (Type) arguments[0];
|
||||
return input.Where(x => entityManager.HasComponent(x, comp) ^ isInverted);
|
||||
}
|
||||
|
||||
public override IEnumerable<EntityUid> DoInitialSelection(IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
if (isInverted)
|
||||
{
|
||||
return base.DoInitialSelection(arguments, isInverted, entityManager);
|
||||
}
|
||||
|
||||
return entityManager.GetAllComponents((Type) arguments[0])
|
||||
.Select(x => x.OwnerUid);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class NamedQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "named";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.String };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var r = new Regex("^" + (string) arguments[0] + "$");
|
||||
return input.Where(e =>
|
||||
{
|
||||
if (entityManager.TryGetComponent<MetaDataComponent>(e, out var metaDataComponent))
|
||||
return r.IsMatch(metaDataComponent.EntityName) ^ isInverted;
|
||||
return isInverted;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class ParentedToQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "parentedto";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.EntityId };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var uid = (EntityUid) arguments[0];
|
||||
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) &&
|
||||
transform.Parent?.OwnerUid == uid) ^ isInverted);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class RecursiveParentedToQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "rparentedto";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.EntityId };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var uid = (EntityUid) arguments[0];
|
||||
return input.Where(e =>
|
||||
{
|
||||
if (!entityManager.TryGetComponent<TransformComponent>(e, out var transform))
|
||||
return isInverted;
|
||||
var cur = transform;
|
||||
while (cur.ParentUid != EntityUid.Invalid)
|
||||
{
|
||||
if ((cur.ParentUid == uid) ^ isInverted)
|
||||
return true;
|
||||
if (cur.Parent is null)
|
||||
return false;
|
||||
cur = cur.Parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class ChildrenQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "children";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
return input.SelectMany(e =>
|
||||
{
|
||||
return entityManager.TryGetComponent<TransformComponent>(e, out var transform)
|
||||
? transform.Children.Select(y => y.OwnerUid)
|
||||
: new List<EntityUid>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class RecursiveChildrenQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "rchildren";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments,
|
||||
bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
IEnumerable<EntityUid> toSearch = input;
|
||||
List<IEnumerable<EntityUid>> children = new List<IEnumerable<EntityUid>>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var doing = toSearch.Where(entityManager.HasComponent<TransformComponent>).Select(entityManager.GetComponent<TransformComponent>).ToArray();
|
||||
var search = doing.SelectMany(x => x.Children.Select(y => y.Owner));
|
||||
if (!search.Any())
|
||||
break;
|
||||
toSearch = doing.SelectMany(x => x.Children.Select(y => y.OwnerUid)).Where(x => x != EntityUid.Invalid);
|
||||
children.Add(doing.SelectMany(x => x.Children.Select(y => y.OwnerUid)).Where(x => x != EntityUid.Invalid));
|
||||
}
|
||||
|
||||
return children.SelectMany(x => x);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class ParentQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "parent";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
return input.Where(entityManager.HasComponent<TransformComponent>)
|
||||
.Select(e => entityManager.GetComponent<TransformComponent>(e).OwnerUid)
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
public override IEnumerable<EntityUid> DoInitialSelection(IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
return DoSelection(entityManager.EntityQuery<TransformComponent>().Select(x => x.OwnerUid), arguments,
|
||||
isInverted, entityManager);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class AboveQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "above";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.String };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
|
||||
var tileTy = tileDefinitionManager[(string) arguments[0]];
|
||||
var entity = IoCManager.Resolve<IEntityManager>();
|
||||
var map = IoCManager.Resolve<IMapManager>();
|
||||
if (tileTy.TileId == 0)
|
||||
{
|
||||
return input.Where(e => entityManager.TryGetComponent<TransformComponent>(e, out var transform) && (transform.Coordinates.GetGridId(entity) == GridId.Invalid) ^ isInverted);
|
||||
}
|
||||
else
|
||||
{
|
||||
return input.Where(e =>
|
||||
{
|
||||
if (!entityManager.TryGetComponent<TransformComponent>(e, out var transform)) return isInverted;
|
||||
|
||||
var gridId = transform.Coordinates.GetGridId(entity);
|
||||
if (gridId == GridId.Invalid)
|
||||
return isInverted;
|
||||
|
||||
var grid = map.GetGrid(gridId);
|
||||
return (grid.GetTileRef(transform.Coordinates).Tile.TypeId == tileTy.TileId) ^ isInverted;
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
// ReSharper disable once InconsistentNaming the name is correct shut up
|
||||
public class OnGridQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "ongrid";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.Integer };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var grid = new GridId((int) arguments[0]);
|
||||
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) && transform.GridID == grid) ^ isInverted);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
// ReSharper disable once InconsistentNaming the name is correct shut up
|
||||
public class OnMapQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "onmap";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.Integer };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var map = new MapId((int) arguments[0]);
|
||||
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) && transform.MapID == map) ^ isInverted);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class PrototypedQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "prototyped";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.String };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var name = (string) arguments[0];
|
||||
return input.Where(e => (entityManager.TryGetComponent<MetaDataComponent>(e, out var metaData) && metaData.EntityPrototype?.ID == name) ^ isInverted);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class RecursivePrototypedQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "rprototyped";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.String };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var name = (string) arguments[0];
|
||||
return input.Where(e =>
|
||||
{
|
||||
if (!entityManager.TryGetComponent<MetaDataComponent>(e, out var metaData))
|
||||
return isInverted;
|
||||
if ((metaData.EntityPrototype?.ID == name) ^ isInverted)
|
||||
return true;
|
||||
|
||||
return (metaData.EntityPrototype?.Parent == name) ^ isInverted; // Damn, can't actually do recursive check here.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class SelectQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "select";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.Integer | QuerySelectorArgument.Percentage };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
if (arguments[0] is int)
|
||||
{
|
||||
var inp = input.OrderBy(_ => Guid.NewGuid()).ToArray();
|
||||
var taken = (int) arguments[0];
|
||||
|
||||
if (isInverted)
|
||||
taken = Math.Max(0, inp.Length - taken);
|
||||
|
||||
return inp.Take(taken);
|
||||
}
|
||||
|
||||
var enumerable = input.OrderBy(_ => Guid.NewGuid()).ToArray();
|
||||
var amount = isInverted
|
||||
? (int) Math.Floor(enumerable.Length * Math.Clamp(1 - (double) arguments[0], 0, 1))
|
||||
: (int) Math.Floor(enumerable.Length * Math.Clamp((double) arguments[0], 0, 1));
|
||||
return enumerable.Take(amount);
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
public class NearQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "near";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.Float };
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
var radius = (float)(double)arguments[0];
|
||||
var entityLookup = IoCManager.Resolve<IEntityLookup>();
|
||||
|
||||
//BUG: GetEntitiesInRange effectively uses manhattan distance. This is not intended, near is supposed to be circular.
|
||||
return input.Where(entityManager.HasComponent<TransformComponent>)
|
||||
.SelectMany(e =>
|
||||
entityLookup.GetEntitiesInRange(entityManager.GetComponent<TransformComponent>(e).Coordinates,
|
||||
radius))
|
||||
.Select(x => x.Uid) // Sloth's fault.
|
||||
.Distinct();
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterBqlQuerySelector]
|
||||
// ReSharper disable once InconsistentNaming the name is correct shut up
|
||||
public class anchoredQuerySelector : BqlQuerySelector
|
||||
{
|
||||
public override string Token => "anchored";
|
||||
|
||||
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
|
||||
|
||||
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) && transform.Anchored) ^ isInverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Robust.Server/Bql/BqlQuerySelector.cs
Normal file
64
Robust.Server/Bql/BqlQuerySelector.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
[Flags]
|
||||
[PublicAPI]
|
||||
public enum QuerySelectorArgument
|
||||
{
|
||||
Integer = 0b00000001,
|
||||
Float = 0b00000010,
|
||||
String = 0b00000100,
|
||||
Percentage = 0b00001000,
|
||||
Component = 0b00010000,
|
||||
//SubQuery = 0b00100000,
|
||||
EntityId = 0b01000000,
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public abstract class BqlQuerySelector
|
||||
{
|
||||
/// <summary>
|
||||
/// The token name for the given QuerySelector, for example `when`.
|
||||
/// </summary>
|
||||
public virtual string Token => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for the given QuerySelector, presented as "what arguments are permitted in what spot".
|
||||
/// </summary>
|
||||
public virtual QuerySelectorArgument[] Arguments => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a transform over it's input entity list, whether that be filtering (selecting) or expanding the
|
||||
/// input on some criteria like what entities are nearby.
|
||||
/// </summary>
|
||||
/// <param name="input">Input entity list.</param>
|
||||
/// <param name="arguments">Parsed selector arguments.</param>
|
||||
/// <param name="isInverted">Whether the query is inverted.</param>
|
||||
/// <param name="entityManager">The entity manager.</param>
|
||||
/// <returns>New list of entities</returns>
|
||||
/// <exception cref="NotImplementedException">someone is a moron if this happens.</exception>
|
||||
public abstract IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input,
|
||||
IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager);
|
||||
|
||||
/// <summary>
|
||||
/// Performs selection as the first selector in the query. Allows for optimizing when you can be more efficient
|
||||
/// than just querying every entity.
|
||||
/// </summary>
|
||||
/// <param name="arguments"></param>
|
||||
/// <param name="isInverted"></param>
|
||||
/// <param name="entityManager"></param>
|
||||
/// <returns></returns>
|
||||
public virtual IEnumerable<EntityUid> DoInitialSelection(IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
|
||||
{
|
||||
return DoSelection(entityManager.GetEntityUids(), arguments, isInverted, entityManager);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
protected BqlQuerySelector() {}
|
||||
}
|
||||
}
|
||||
19
Robust.Server/Bql/BqlQuerySelectorParsed.cs
Normal file
19
Robust.Server/Bql/BqlQuerySelectorParsed.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
public struct BqlQuerySelectorParsed
|
||||
{
|
||||
public List<object> Arguments;
|
||||
public string Token;
|
||||
public bool Inverted;
|
||||
|
||||
public BqlQuerySelectorParsed(List<object> arguments, string token, bool inverted)
|
||||
{
|
||||
Arguments = arguments;
|
||||
Token = token;
|
||||
Inverted = inverted;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Robust.Server/Bql/ForAllCommand.cs
Normal file
73
Robust.Server/Bql/ForAllCommand.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
public class ForAllCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "forall";
|
||||
public string Description => "Runs a command over all entities with a given component";
|
||||
public string Help => "Usage: forall <bql query> do <command...>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var queryManager = IoCManager.Resolve<IBqlQueryManager>();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var (entities, rest) = queryManager.SimpleParseAndExecute(argStr[6..]);
|
||||
|
||||
foreach (var ent in entities.ToList())
|
||||
{
|
||||
var cmds = SubstituteEntityDetails(shell, entityManager.GetEntity(ent), rest).Split(";");
|
||||
foreach (var cmd in cmds)
|
||||
{
|
||||
shell.ExecuteCommand(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This will be refactored out soon.
|
||||
private static string SubstituteEntityDetails(IConsoleShell shell, IEntity ent, string ruleString)
|
||||
{
|
||||
// gross, is there a better way to do this?
|
||||
ruleString = ruleString.Replace("$ID", ent.Uid.ToString());
|
||||
ruleString = ruleString.Replace("$WX",
|
||||
ent.Transform.WorldPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$WY",
|
||||
ent.Transform.WorldPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$LX",
|
||||
ent.Transform.LocalPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$LY",
|
||||
ent.Transform.LocalPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$NAME", ent.Name);
|
||||
|
||||
if (shell.Player is IPlayerSession player)
|
||||
{
|
||||
if (player.AttachedEntity != null)
|
||||
{
|
||||
var p = player.AttachedEntity;
|
||||
ruleString = ruleString.Replace("$PID", ent.Uid.ToString());
|
||||
ruleString = ruleString.Replace("$PWX",
|
||||
p.Transform.WorldPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$PWY",
|
||||
p.Transform.WorldPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$PLX",
|
||||
p.Transform.LocalPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||
ruleString = ruleString.Replace("$PLY",
|
||||
p.Transform.LocalPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
return ruleString;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Robust.Server/Bql/IBqlQueryManager.cs
Normal file
11
Robust.Server/Bql/IBqlQueryManager.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
public interface IBqlQueryManager
|
||||
{
|
||||
public (IEnumerable<EntityUid>, string) SimpleParseAndExecute(string query);
|
||||
void DoAutoRegistrations();
|
||||
}
|
||||
}
|
||||
14
Robust.Server/Bql/RegisterBqlQuerySelectorAttribute.cs
Normal file
14
Robust.Server/Bql/RegisterBqlQuerySelectorAttribute.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Server.Bql
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
[BaseTypeRequired(typeof(BqlQuerySelector))]
|
||||
[MeansImplicitUse]
|
||||
[PublicAPI]
|
||||
public class RegisterBqlQuerySelectorAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,10 @@ namespace Robust.Server.Console.Commands
|
||||
class AddMapCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "addmap";
|
||||
public string Description => "Adds a new empty map to the round. If the mapID already exists, this command does nothing.";
|
||||
|
||||
public string Description =>
|
||||
"Adds a new empty map to the round. If the mapID already exists, this command does nothing.";
|
||||
|
||||
public string Help => "addmap <mapID> [initialize]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
@@ -35,11 +38,12 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
pauseMgr.AddUninitializedMap(mapId);
|
||||
}
|
||||
|
||||
shell.WriteLine($"Map with ID {mapId} created.");
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine($"Map with ID {mapId} already exists!");
|
||||
shell.WriteError($"Map with ID {mapId} already exists!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +52,12 @@ namespace Robust.Server.Console.Commands
|
||||
public string Command => "rmmap";
|
||||
public string Description => "Removes a map from the world. You cannot remove nullspace.";
|
||||
public string Help => "rmmap <mapId>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine("Wrong number of args.");
|
||||
shell.WriteError("Wrong number of args.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,7 +66,7 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
if (!mapManager.MapExists(mapId))
|
||||
{
|
||||
shell.WriteLine($"Map {mapId.Value} does not exist.");
|
||||
shell.WriteError($"Map {mapId.Value} does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,13 +85,13 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
shell.WriteLine("Not enough arguments.");
|
||||
shell.WriteError("Not enough arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out var intGridId))
|
||||
{
|
||||
shell.WriteLine("Not a valid grid ID.");
|
||||
shell.WriteError("Not a valid grid ID.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,7 +102,7 @@ namespace Robust.Server.Console.Commands
|
||||
// no saving default grid
|
||||
if (!mapManager.TryGetGrid(gridId, out var grid))
|
||||
{
|
||||
shell.WriteLine("That grid does not exist.");
|
||||
shell.WriteError("That grid does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,14 +134,14 @@ namespace Robust.Server.Console.Commands
|
||||
// no loading into null space
|
||||
if (mapId == MapId.Nullspace)
|
||||
{
|
||||
shell.WriteLine("Cannot load into nullspace.");
|
||||
shell.WriteError("Cannot load into nullspace.");
|
||||
return;
|
||||
}
|
||||
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
if (!mapManager.MapExists(mapId))
|
||||
{
|
||||
shell.WriteLine("Target map does not exist.");
|
||||
shell.WriteError("Target map does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,18 +170,21 @@ namespace Robust.Server.Console.Commands
|
||||
if (!int.TryParse(args[0], out var intMapId))
|
||||
return;
|
||||
|
||||
var mapID = new MapId(intMapId);
|
||||
var mapId = new MapId(intMapId);
|
||||
|
||||
// no saving null space
|
||||
if (mapID == MapId.Nullspace)
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
if (!mapManager.MapExists(mapID))
|
||||
if (!mapManager.MapExists(mapId))
|
||||
{
|
||||
shell.WriteError("Target map does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Parse path
|
||||
IoCManager.Resolve<IMapLoader>().SaveMap(mapID, "Maps/Demo/DemoMap.yaml");
|
||||
IoCManager.Resolve<IMapLoader>().SaveMap(mapId, args[1]);
|
||||
shell.WriteLine($"Map {mapId} has been saved to {args[1]}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,26 +202,24 @@ namespace Robust.Server.Console.Commands
|
||||
if (!int.TryParse(args[0], out var intMapId))
|
||||
return;
|
||||
|
||||
var mapID = new MapId(intMapId);
|
||||
var mapId = new MapId(intMapId);
|
||||
|
||||
// no loading null space
|
||||
if (mapID == MapId.Nullspace)
|
||||
if (mapId == MapId.Nullspace)
|
||||
{
|
||||
shell.WriteLine("You cannot load into map 0.");
|
||||
shell.WriteError("You cannot load into map 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
if (mapManager.MapExists(mapID))
|
||||
if (mapManager.MapExists(mapId))
|
||||
{
|
||||
shell.WriteLine($"Map {mapID} already exists.");
|
||||
shell.WriteError($"Map {mapId} already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Parse path
|
||||
var mapPath = "Maps/Demo/DemoMap.yaml";
|
||||
IoCManager.Resolve<IMapLoader>().LoadMap(mapID, mapPath);
|
||||
shell.WriteLine($"Map {mapID} has been loaded from {mapPath}.");
|
||||
IoCManager.Resolve<IMapLoader>().LoadMap(mapId, args[1]);
|
||||
shell.WriteLine($"Map {mapId} has been loaded from {args[1]}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +238,8 @@ namespace Robust.Server.Console.Commands
|
||||
var pos = player.AttachedEntity.Transform.Coordinates;
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
shell.WriteLine($"MapID:{pos.GetMapId(entityManager)} GridID:{pos.GetGridId(entityManager)} X:{pos.X:N2} Y:{pos.Y:N2}");
|
||||
shell.WriteLine(
|
||||
$"MapID:{pos.GetMapId(entityManager)} GridID:{pos.GetGridId(entityManager)} X:{pos.X:N2} Y:{pos.Y:N2}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +253,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
if (args.Length < 3 || args.Length > 4)
|
||||
{
|
||||
shell.WriteLine("Wrong number of args.");
|
||||
shell.WriteError("Wrong number of args.");
|
||||
}
|
||||
|
||||
var gridId = new GridId(int.Parse(args[0]));
|
||||
@@ -272,11 +279,12 @@ namespace Robust.Server.Console.Commands
|
||||
public string Command => "rmgrid";
|
||||
public string Description => "Removes a grid from a map. You cannot remove the default grid.";
|
||||
public string Help => "rmgrid <gridId>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine("Wrong number of args.");
|
||||
shell.WriteError("Wrong number of args.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,7 +293,7 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
if (!mapManager.GridExists(gridId))
|
||||
{
|
||||
shell.WriteLine($"Grid {gridId.Value} does not exist.");
|
||||
shell.WriteError($"Grid {gridId.Value} does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,7 +312,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine("Wrong number of args.");
|
||||
shell.WriteError("Wrong number of args.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -316,13 +324,13 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
if (!mapManager.MapExists(mapId))
|
||||
{
|
||||
shell.WriteLine("Map does not exist!");
|
||||
shell.WriteError("Map does not exist!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pauseManager.IsMapInitialized(mapId))
|
||||
{
|
||||
shell.WriteLine("Map is already initialized!");
|
||||
shell.WriteError("Map is already initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
public class SpawnCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "spawn";
|
||||
public string Description => "Spawns an entity with specific type at your feet.";
|
||||
public string Help => "spawn <entity type>";
|
||||
public string Description => "Spawns an entity with specific type.";
|
||||
public string Help => "spawn <prototype> OR spawn <prototype> <relative entity ID> OR spawn <prototype> <x> <y>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player as IPlayerSession;
|
||||
var ent = IoCManager.Resolve<IServerEntityManager>();
|
||||
if (player?.AttachedEntity != null)
|
||||
if (args.Length is < 1 or > 3)
|
||||
{
|
||||
shell.WriteError("Incorrect number of arguments. " + Help);
|
||||
}
|
||||
|
||||
if (args.Length == 1 && player?.AttachedEntity != null)
|
||||
{
|
||||
ent.SpawnEntity(args[0], player.AttachedEntity.Transform.Coordinates);
|
||||
}
|
||||
else if (args.Length == 2)
|
||||
{
|
||||
ent.SpawnEntity(args[0], ent.GetEntity(EntityUid.Parse(args[1])).Transform.Coordinates);
|
||||
}
|
||||
else if (player?.AttachedEntity != null)
|
||||
{
|
||||
var coords = new MapCoordinates(float.Parse(args[1]),
|
||||
float.Parse(args[2]), player.AttachedEntity.Transform.MapID);
|
||||
ent.SpawnEntity(args[0], coords);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ namespace Robust.Server.GameObjects
|
||||
[DataField("color")]
|
||||
private Color _color = Color.White;
|
||||
|
||||
[DataField("directional")]
|
||||
private bool _directional = true;
|
||||
|
||||
[DataField("sprite")]
|
||||
private string? _baseRSIPath;
|
||||
|
||||
@@ -119,17 +116,6 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Directional
|
||||
{
|
||||
get => _directional;
|
||||
set
|
||||
{
|
||||
_directional = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? BaseRSIPath
|
||||
{
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Robust.Server.GameObjects
|
||||
//TODO: Calculate this from PAS
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
|
||||
|
||||
if(!EntityManager.TryGetComponent<ITransformComponent>(uid, out var transform))
|
||||
if(!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
|
||||
return null;
|
||||
|
||||
var id = CacheIdentifier();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
internal sealed class TransformSystem : SharedTransformSystem
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Composition;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
@@ -15,7 +18,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
internal sealed class EntityViewCulling
|
||||
internal sealed class PVSSystem : EntitySystem
|
||||
{
|
||||
private const int ViewSetCapacity = 256; // starting number of entities that are in view
|
||||
private const int PlayerSetSize = 64; // Starting number of players
|
||||
@@ -23,12 +26,15 @@ namespace Robust.Server.GameStates
|
||||
|
||||
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
|
||||
|
||||
private readonly IServerEntityManager _entMan;
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly IEntityLookup _lookup;
|
||||
[Shared.IoC.Dependency] private readonly IServerEntityManager _entMan = default!;
|
||||
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Shared.IoC.Dependency] private readonly IEntityLookup _lookup = default!;
|
||||
[Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
|
||||
private readonly Dictionary<ICommonSession, HashSet<EntityUid>> _playerVisibleSets = new(PlayerSetSize);
|
||||
internal readonly Dictionary<ICommonSession, Dictionary<IMapChunkInternal, GameTick>> PlayerChunks = new(PlayerSetSize);
|
||||
private readonly Dictionary<ICommonSession, Dictionary<IMapChunkInternal, GameTick>> _playerChunks = new(PlayerSetSize);
|
||||
|
||||
private readonly Dictionary<ICommonSession, ChunkStreamingData>
|
||||
_streamingChunks = new();
|
||||
@@ -81,25 +87,11 @@ namespace Robust.Server.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
public EntityViewCulling(IServerEntityManager entMan, IMapManager mapManager, IEntityLookup lookup)
|
||||
{
|
||||
_entMan = entMan;
|
||||
_mapManager = mapManager;
|
||||
_lookup = lookup;
|
||||
}
|
||||
|
||||
public void SetTransformNetId(ushort value)
|
||||
{
|
||||
_transformNetId = value;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -117,54 +109,86 @@ namespace Robust.Server.GameStates
|
||||
return list;
|
||||
}
|
||||
|
||||
// Not thread safe
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
EntityManager.EntityDeleted += OnEntityDelete;
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChange;
|
||||
_mapManager.OnGridRemoved += OnGridRemoved;
|
||||
|
||||
// If you want to make this modifiable at runtime you need to subscribe to tickrate updates and streaming updates
|
||||
// plus invalidate any chunks currently being streamed as well.
|
||||
StreamingTilesPerTick = (int) (_configManager.GetCVar(CVars.StreamedTilesPerSecond) / _gameTiming.TickRate);
|
||||
_configManager.OnValueChanged(CVars.StreamedTileRange, SetStreamRange, true);
|
||||
}
|
||||
|
||||
private void SetStreamRange(float value)
|
||||
{
|
||||
StreamRange = value;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
EntityManager.EntityDeleted -= OnEntityDelete;
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChange;
|
||||
_mapManager.OnGridRemoved -= OnGridRemoved;
|
||||
_configManager.UnsubValueChanged(CVars.StreamedTileRange, SetStreamRange);
|
||||
}
|
||||
|
||||
private void OnGridRemoved(MapId mapid, GridId gridid)
|
||||
{
|
||||
// Remove any sort of tracking for when a chunk was sent.
|
||||
foreach (var (_, chunks) in _playerChunks)
|
||||
{
|
||||
foreach (var (chunk, _) in chunks.ToArray())
|
||||
{
|
||||
if (chunk is not MapChunk mapChunk ||
|
||||
mapChunk.GridId == gridid)
|
||||
{
|
||||
chunks.Remove(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Player Status
|
||||
|
||||
private void OnPlayerStatusChange(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus == SessionStatus.InGame)
|
||||
{
|
||||
AddPlayer(e.Session);
|
||||
}
|
||||
else if (e.OldStatus == SessionStatus.InGame)
|
||||
{
|
||||
RemovePlayer(e.Session);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPlayer(ICommonSession session)
|
||||
{
|
||||
var visSet = _visSetPool.Get();
|
||||
|
||||
_playerVisibleSets.Add(session, visSet);
|
||||
PlayerChunks.Add(session, new Dictionary<IMapChunkInternal, GameTick>(32));
|
||||
_playerChunks.Add(session, new Dictionary<IMapChunkInternal, GameTick>(32));
|
||||
_streamingChunks.Add(session, new ChunkStreamingData());
|
||||
}
|
||||
|
||||
// Not thread safe
|
||||
public void RemovePlayer(ICommonSession session)
|
||||
{
|
||||
_playerVisibleSets.Remove(session);
|
||||
PlayerChunks.Remove(session);
|
||||
_playerChunks.Remove(session);
|
||||
_playerLastFullMap.Remove(session, out _);
|
||||
_streamingChunks.Remove(session);
|
||||
}
|
||||
|
||||
// thread safe
|
||||
public bool IsPointVisible(ICommonSession session, in MapCoordinates position)
|
||||
#endregion
|
||||
|
||||
private void OnEntityDelete(object? sender, EntityUid e)
|
||||
{
|
||||
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;
|
||||
// Not aware of prediction
|
||||
_deletionHistory.Add((EntityManager.CurrentTick, e));
|
||||
}
|
||||
|
||||
private HashSet<EntityUid> GetSessionViewers(ICommonSession session)
|
||||
@@ -214,7 +238,7 @@ namespace Robust.Server.GameStates
|
||||
if (session.AttachedEntityUid is not null)
|
||||
{
|
||||
var viewers = GetSessionViewers(session);
|
||||
var chunksSeen = PlayerChunks[session];
|
||||
var chunksSeen = _playerChunks[session];
|
||||
|
||||
foreach (var eyeEuid in viewers)
|
||||
{
|
||||
@@ -449,10 +473,10 @@ namespace Robust.Server.GameStates
|
||||
|
||||
//TODO: HACK: somehow an entity left the view, transform does not exist (deleted?), but was not in the
|
||||
// deleted list. This seems to happen with the map entity on round restart.
|
||||
if (!_entMan.EntityExists(entityUid))
|
||||
if (!EntityManager.EntityExists(entityUid))
|
||||
continue;
|
||||
|
||||
var xform = _entMan.GetComponent<ITransformComponent>(entityUid);
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(entityUid);
|
||||
|
||||
// Anchored entities don't ever leave
|
||||
if (xform.Anchored) continue;
|
||||
@@ -477,14 +501,14 @@ namespace Robust.Server.GameStates
|
||||
{
|
||||
|
||||
// skip sending anchored entities (walls)
|
||||
DebugTools.Assert(!_entMan.GetComponent<ITransformComponent>(entityUid).Anchored);
|
||||
DebugTools.Assert(!EntityManager.GetComponent<TransformComponent>(entityUid).Anchored);
|
||||
|
||||
if (previousSet.Contains(entityUid))
|
||||
{
|
||||
//Still Visible
|
||||
|
||||
// Nothing new to send
|
||||
if(_entMan.GetEntity(entityUid).LastModifiedTick < fromTick)
|
||||
if (EntityManager.GetEntity(entityUid).LastModifiedTick < fromTick)
|
||||
continue;
|
||||
|
||||
// only send new changes
|
||||
@@ -537,13 +561,13 @@ namespace Robust.Server.GameStates
|
||||
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 (_entMan.TryGetComponent<VisibilityComponent>(uid, out var visComp))
|
||||
if (EntityManager.TryGetComponent<VisibilityComponent>(uid, out var visComp))
|
||||
{
|
||||
if ((visMask & visComp.Layer) == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
var xform = _entMan.GetComponent<TransformComponent>(uid);
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(uid);
|
||||
|
||||
var parentUid = xform.ParentUid;
|
||||
|
||||
@@ -606,7 +630,7 @@ namespace Robust.Server.GameStates
|
||||
// Read Safe
|
||||
private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid)
|
||||
{
|
||||
var xform = _entMan.GetComponent<ITransformComponent>(euid);
|
||||
var xform = _entMan.GetComponent<TransformComponent>(euid);
|
||||
|
||||
var view = Box2.UnitCentered.Scale(ViewSize).Translated(xform.WorldPosition);
|
||||
var map = xform.MapID;
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
@@ -30,10 +29,9 @@ namespace Robust.Server.GameStates
|
||||
private readonly Dictionary<long, GameTick> _ackedStates = new();
|
||||
private GameTick _lastOldestAck = GameTick.Zero;
|
||||
|
||||
private EntityViewCulling _entityView = null!;
|
||||
private PVSSystem _pvs = default!;
|
||||
|
||||
[Dependency] private readonly IServerEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityLookup _lookup = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IServerNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
@@ -58,13 +56,12 @@ namespace Robust.Server.GameStates
|
||||
|
||||
public void SetTransformNetId(ushort netId)
|
||||
{
|
||||
_entityView.SetTransformNetId(netId);
|
||||
_pvs.SetTransformNetId(netId);
|
||||
}
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_logger = Logger.GetSawmill("PVS");
|
||||
_entityView = new EntityViewCulling(_entityManager, _mapManager, _lookup);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -76,49 +73,7 @@ namespace Robust.Server.GameStates
|
||||
_networkManager.Connected += HandleClientConnected;
|
||||
_networkManager.Disconnect += HandleClientDisconnect;
|
||||
|
||||
_playerManager.PlayerStatusChanged += HandlePlayerStatusChanged;
|
||||
|
||||
_entityManager.EntityDeleted += HandleEntityDeleted;
|
||||
|
||||
_mapManager.OnGridRemoved += HandleGridRemove;
|
||||
|
||||
// If you want to make this modifiable at runtime you need to subscribe to tickrate updates and streaming updates
|
||||
// plus invalidate any chunks currently being streamed as well.
|
||||
_entityView.StreamingTilesPerTick = (int) (_configurationManager.GetCVar(CVars.StreamedTilesPerSecond) / _gameTiming.TickRate);
|
||||
_configurationManager.OnValueChanged(CVars.StreamedTileRange, value => _entityView.StreamRange = value, true);
|
||||
}
|
||||
|
||||
private void HandleGridRemove(MapId mapid, GridId gridid)
|
||||
{
|
||||
// Remove any sort of tracking for when a chunk was sent.
|
||||
foreach (var (_, chunks) in _entityView.PlayerChunks)
|
||||
{
|
||||
foreach (var (chunk, _) in chunks.ToArray())
|
||||
{
|
||||
if (chunk is not MapChunk mapChunk ||
|
||||
mapChunk.GridId == gridid)
|
||||
{
|
||||
chunks.Remove(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
_pvs = EntitySystem.Get<PVSSystem>();
|
||||
}
|
||||
|
||||
private void HandleClientConnected(object? sender, NetChannelArgs e)
|
||||
@@ -166,14 +121,14 @@ namespace Robust.Server.GameStates
|
||||
{
|
||||
DebugTools.Assert(_networkManager.IsServer);
|
||||
|
||||
_entityView.ViewSize = PvsRange * 2;
|
||||
_entityView.CullingEnabled = PvsEnabled;
|
||||
_pvs.ViewSize = PvsRange * 2;
|
||||
_pvs.CullingEnabled = PvsEnabled;
|
||||
|
||||
if (!_networkManager.IsConnected)
|
||||
{
|
||||
// Prevent deletions piling up if we have no clients.
|
||||
_entityManager.CullDeletionHistory(GameTick.MaxValue);
|
||||
_entityView.CullDeletionHistory(GameTick.MaxValue);
|
||||
_pvs.CullDeletionHistory(GameTick.MaxValue);
|
||||
_mapManager.CullDeletionHistory(GameTick.MaxValue);
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +157,7 @@ namespace Robust.Server.GameStates
|
||||
DebugTools.Assert("Why does this channel not have an entry?");
|
||||
}
|
||||
|
||||
var (entStates, deletions) = _entityView.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
|
||||
var (entStates, deletions) = _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
|
||||
var playerStates = _playerManager.GetPlayerStates(lastAck);
|
||||
var mapData = _mapManager.GetStateData(lastAck);
|
||||
|
||||
@@ -254,7 +209,7 @@ namespace Robust.Server.GameStates
|
||||
{
|
||||
_lastOldestAck = oldestAck;
|
||||
_entityManager.CullDeletionHistory(oldestAck);
|
||||
_entityView.CullDeletionHistory(oldestAck);
|
||||
_pvs.CullDeletionHistory(oldestAck);
|
||||
_mapManager.CullDeletionHistory(oldestAck);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Completion;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
|
||||
using Microsoft.CodeAnalysis.Host.Mef;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Robust.Server.Console;
|
||||
@@ -19,6 +23,7 @@ using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Scripting;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Network.Messages.MsgScriptCompletionResponse;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -39,6 +44,8 @@ namespace Robust.Server.Scripting
|
||||
_netManager.RegisterNetMessage<MsgScriptStop>(ReceiveScriptEnd);
|
||||
_netManager.RegisterNetMessage<MsgScriptEval>(ReceiveScriptEval);
|
||||
_netManager.RegisterNetMessage<MsgScriptStart>(ReceiveScriptStart);
|
||||
_netManager.RegisterNetMessage<MsgScriptCompletion>(ReceiveScriptCompletion);
|
||||
_netManager.RegisterNetMessage<MsgScriptCompletionResponse>();
|
||||
_netManager.RegisterNetMessage<MsgScriptResponse>();
|
||||
_netManager.RegisterNetMessage<MsgScriptStartAck>();
|
||||
|
||||
@@ -237,6 +244,75 @@ namespace Robust.Server.Scripting
|
||||
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
|
||||
}
|
||||
|
||||
private async void ReceiveScriptCompletion(MsgScriptCompletion message)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session))
|
||||
return;
|
||||
|
||||
if (!_conGroupController.CanScript(session))
|
||||
{
|
||||
Logger.WarningS("script", "Client {0} tried to access Scripting without permissions.", session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_instances.TryGetValue(session, out var instances) ||
|
||||
!instances.TryGetValue(message.ScriptSession, out var instance))
|
||||
return;
|
||||
|
||||
var replyMessage = _netManager.CreateNetMessage<MsgScriptCompletionResponse>();
|
||||
replyMessage.ScriptSession = message.ScriptSession;
|
||||
|
||||
// Everything below here cribbed from
|
||||
// https://www.strathweb.com/2018/12/using-roslyn-c-completion-service-programmatically/
|
||||
var workspace = new AdhocWorkspace(MefHostServices.Create(MefHostServices.DefaultAssemblies));
|
||||
|
||||
var scriptProject = workspace.AddProject(ProjectInfo.Create(
|
||||
ProjectId.CreateNewId(),
|
||||
VersionStamp.Create(),
|
||||
"Script", "Script",
|
||||
LanguageNames.CSharp,
|
||||
isSubmission: true
|
||||
)
|
||||
.WithMetadataReferences(
|
||||
_reflectionManager.Assemblies.Select(a => MetadataReference.CreateFromFile(a.Location))
|
||||
)
|
||||
.WithCompilationOptions(new CSharpCompilationOptions(
|
||||
OutputKind.DynamicallyLinkedLibrary,
|
||||
usings: ScriptInstanceShared.DefaultImports
|
||||
)));
|
||||
|
||||
var document = workspace.AddDocument(DocumentInfo.Create(
|
||||
DocumentId.CreateNewId(scriptProject.Id),
|
||||
"Script",
|
||||
sourceCodeKind: SourceCodeKind.Script,
|
||||
loader: TextLoader.From(TextAndVersion.Create(SourceText.From(message.Code), VersionStamp.Create()))
|
||||
));
|
||||
|
||||
var results = await CompletionService
|
||||
.GetService(document)
|
||||
.GetCompletionsAsync(document, message.Cursor);
|
||||
|
||||
if (results is not null)
|
||||
{
|
||||
var ires = ImmutableArray.CreateBuilder<LiteResult>();
|
||||
foreach (var r in results.Items)
|
||||
ires.Add(new LiteResult(
|
||||
displayText: r.DisplayText,
|
||||
displayTextPrefix: r.DisplayTextPrefix,
|
||||
displayTextSuffix: r.DisplayTextSuffix,
|
||||
inlineDescription: r.InlineDescription,
|
||||
tags: r.Tags,
|
||||
properties: r.Properties
|
||||
));
|
||||
|
||||
replyMessage.Results = ires.ToImmutable();
|
||||
}
|
||||
else
|
||||
replyMessage.Results = ImmutableArray<LiteResult>.Empty;
|
||||
|
||||
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
|
||||
}
|
||||
|
||||
private void PromptAutoImports(
|
||||
IEnumerable<Diagnostic> diags,
|
||||
string code,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Robust.Server.Bql;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.DataMetrics;
|
||||
using Robust.Server.Debugging;
|
||||
@@ -73,6 +74,7 @@ namespace Robust.Server
|
||||
IoCManager.Register<IMetricsManager, MetricsManager>();
|
||||
IoCManager.Register<IAuthManager, AuthManager>();
|
||||
IoCManager.Register<IPhysicsManager, PhysicsManager>();
|
||||
IoCManager.Register<IBqlQueryManager, BqlQueryManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,9 @@ namespace Robust.Shared.Maths
|
||||
public static Angle operator -(Angle a, Angle b)
|
||||
=> new(a.Theta - b.Theta);
|
||||
|
||||
public static Angle operator -(Angle orig)
|
||||
=> new(-orig.Theta);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Theta} rad";
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Robust.Shared.Scripting
|
||||
private static readonly Func<Script, bool> _hasReturnValue;
|
||||
private static readonly Func<Diagnostic, IReadOnlyList<object?>?> _getDiagnosticArguments;
|
||||
|
||||
private static readonly string[] _defaultImports =
|
||||
public static readonly string[] DefaultImports =
|
||||
{
|
||||
"System",
|
||||
"System.Linq",
|
||||
@@ -126,19 +126,8 @@ namespace Robust.Shared.Scripting
|
||||
// TODO: there are probably issues with multiple classifications overlapping the same text here.
|
||||
// Too lazy to fix.
|
||||
var src = code[span.TextSpan.Start..span.TextSpan.End];
|
||||
var color = span.ClassificationType switch
|
||||
{
|
||||
ClassificationTypeNames.Comment => Color.FromHex("#57A64A"),
|
||||
ClassificationTypeNames.NumericLiteral => Color.FromHex("#b5cea8"),
|
||||
ClassificationTypeNames.StringLiteral => Color.FromHex("#D69D85"),
|
||||
ClassificationTypeNames.Keyword => Color.FromHex("#569CD6"),
|
||||
ClassificationTypeNames.StaticSymbol => Color.FromHex("#4EC9B0"),
|
||||
ClassificationTypeNames.ClassName => Color.FromHex("#4EC9B0"),
|
||||
ClassificationTypeNames.StructName => Color.FromHex("#4EC9B0"),
|
||||
ClassificationTypeNames.InterfaceName => Color.FromHex("#B8D7A3"),
|
||||
ClassificationTypeNames.EnumName => Color.FromHex("#B8D7A3"),
|
||||
_ => Color.FromHex("#D4D4D4")
|
||||
};
|
||||
if (!ScriptingColorScheme.ColorScheme.TryGetValue(span.ClassificationType, out var color))
|
||||
color = ScriptingColorScheme.ColorScheme[ScriptingColorScheme.Default];
|
||||
|
||||
msg.PushColor(color);
|
||||
msg.AddText(src);
|
||||
@@ -218,7 +207,7 @@ namespace Robust.Shared.Scripting
|
||||
public static ScriptOptions GetScriptOptions(IReflectionManager reflectionManager)
|
||||
{
|
||||
return ScriptOptions.Default
|
||||
.AddImports(_defaultImports)
|
||||
.AddImports(DefaultImports)
|
||||
.AddReferences(GetDefaultReferences(reflectionManager));
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,15 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> NetTickrate =
|
||||
CVarDef.Create("net.tickrate", 60, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
public static readonly CVarDef<float> ConnectionTimeout =
|
||||
CVarDef.Create("net.connection_timeout", 25.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<float> ResendHandshakeInterval =
|
||||
CVarDef.Create("net.handshake_interval", 3.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<int> MaximumHandshakeAttempts =
|
||||
CVarDef.Create("net.handshake_attempts", 5, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/**
|
||||
* SUS
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Nett;
|
||||
using Robust.Shared.Log;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Nett;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Configuration
|
||||
@@ -412,6 +412,11 @@ namespace Robust.Shared.Configuration
|
||||
return float.Parse(value);
|
||||
}
|
||||
|
||||
if (type?.IsEnum ?? false)
|
||||
{
|
||||
return Enum.Parse(type, value);
|
||||
}
|
||||
|
||||
// Must be a string.
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace Robust.Shared.Containers
|
||||
}
|
||||
}
|
||||
|
||||
public static void AttachParentToContainerOrGrid(this ITransformComponent transform)
|
||||
public static void AttachParentToContainerOrGrid(this TransformComponent transform)
|
||||
{
|
||||
if (transform.Parent == null
|
||||
|| !TryGetContainer(transform.Parent.Owner, out var container)
|
||||
@@ -153,7 +153,7 @@ namespace Robust.Shared.Containers
|
||||
transform.AttachToGridOrMap();
|
||||
}
|
||||
|
||||
private static bool TryInsertIntoContainer(this ITransformComponent transform, IContainer container)
|
||||
private static bool TryInsertIntoContainer(this TransformComponent transform, IContainer container)
|
||||
{
|
||||
if (container.Insert(transform.Owner)) return true;
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Robust.Shared.Containers
|
||||
|
||||
#region Container Helpers
|
||||
|
||||
public bool TryGetContainingContainer(EntityUid uid, [NotNullWhen(true)] out IContainer? container, ITransformComponent? transform = null)
|
||||
public bool TryGetContainingContainer(EntityUid uid, [NotNullWhen(true)] out IContainer? container, TransformComponent? transform = null)
|
||||
{
|
||||
container = null;
|
||||
if (!Resolve(uid, ref transform, false))
|
||||
@@ -117,7 +117,7 @@ namespace Robust.Shared.Containers
|
||||
return TryGetContainingContainer(transform.ParentUid, uid, out container);
|
||||
}
|
||||
|
||||
public bool IsEntityInContainer(EntityUid uid, ITransformComponent? transform = null)
|
||||
public bool IsEntityInContainer(EntityUid uid, TransformComponent? transform = null)
|
||||
{
|
||||
return TryGetContainingContainer(uid, out _, transform);
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ Types:
|
||||
- "int get_Height()"
|
||||
- "int get_Width()"
|
||||
- "SixLabors.ImageSharp.Formats.PixelTypeInfo get_PixelType()"
|
||||
- "SixLabors.ImageSharp.Image`1<!!0> Load<>(System.IO.Stream)"
|
||||
- "void Dispose()"
|
||||
Image`1:
|
||||
Methods:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
@@ -28,6 +28,10 @@ namespace Robust.Shared.GameObjects
|
||||
[ViewVariables]
|
||||
public IEntity Owner { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables]
|
||||
public EntityUid OwnerUid => Owner.Uid;
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables]
|
||||
public bool Paused => Owner.Paused;
|
||||
@@ -232,8 +236,9 @@ namespace Robust.Shared.GameObjects
|
||||
if(Owner is null)
|
||||
return;
|
||||
|
||||
Owner.Dirty();
|
||||
LastModifiedTick = Owner.EntityManager.CurrentTick;
|
||||
var entManager = Owner.EntityManager;
|
||||
entManager.DirtyEntity(Owner.Uid);
|
||||
LastModifiedTick = entManager.CurrentTick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the position and orientation of the entity.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public interface ITransformComponent : IComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Defer updates to the EntityTree and MoveEvent calls if toggled.
|
||||
/// </summary>
|
||||
bool DeferUpdates { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// While updating did we actually defer anything?
|
||||
/// </summary>
|
||||
bool UpdatesDeferred { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Run MoveEvent, RotateEvent, and UpdateEntityTree updates.
|
||||
/// </summary>
|
||||
void RunDeferred(Box2 worldAABB);
|
||||
|
||||
/// <summary>
|
||||
/// Disables or enables to ability to locally rotate the entity. When set it removes any local rotation.
|
||||
/// </summary>
|
||||
bool NoLocalRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local offset of this entity relative to its parent
|
||||
/// (<see cref="Parent"/> if it's not null, to <see cref="GridID"/> otherwise).
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Vector2 LocalPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Position offset of this entity relative to its parent.
|
||||
/// </summary>
|
||||
EntityCoordinates Coordinates { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current position offset of the entity relative to the world.
|
||||
/// Can de-parent from its parent if the parent is a grid.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Vector2 WorldPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current position offset of the entity relative to the world.
|
||||
/// This is effectively a more complete version of <see cref="WorldPosition"/>
|
||||
/// </summary>
|
||||
MapCoordinates MapPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current rotation offset of the entity.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Angle LocalRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current world rotation of the entity.
|
||||
/// </summary>
|
||||
Angle WorldRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Matrix for transforming points from local to world space.
|
||||
/// </summary>
|
||||
Matrix3 WorldMatrix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Matrix for transforming points from world to local space.
|
||||
/// </summary>
|
||||
Matrix3 InvWorldMatrix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the transform of the container of this object if it exists, can be nested several times.
|
||||
/// </summary>
|
||||
ITransformComponent? Parent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The UID of the parent entity that this entity is attached to.
|
||||
/// </summary>
|
||||
public EntityUid ParentUid { get; set; }
|
||||
|
||||
Vector2? LerpDestination { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Finds the transform located on the map or in nullspace
|
||||
/// </summary>
|
||||
ITransformComponent GetMapTransform();
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the entity of this transform contains the entity argument
|
||||
/// </summary>
|
||||
bool ContainsEntity(ITransformComponent entityTransform);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the map which this object is on
|
||||
/// </summary>
|
||||
MapId MapID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the grid which this object is on
|
||||
/// </summary>
|
||||
GridId GridID { get; }
|
||||
|
||||
void AttachToGridOrMap();
|
||||
void AttachParent(ITransformComponent parent);
|
||||
void AttachParent(IEntity parent);
|
||||
|
||||
IEnumerable<ITransformComponent> Children { get; }
|
||||
int ChildCount { get; }
|
||||
IEnumerable<EntityUid> ChildEntityUids { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this transform anchored to a grid tile?
|
||||
/// </summary>
|
||||
bool Anchored { get; set; }
|
||||
|
||||
Matrix3 GetLocalMatrix();
|
||||
Matrix3 GetLocalMatrixInv();
|
||||
void DetachParentToNull();
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,9 @@ namespace Robust.Shared.GameObjects
|
||||
IMapGrid Grid { get; }
|
||||
void ClearGridId();
|
||||
|
||||
bool AnchorEntity(ITransformComponent transform);
|
||||
void UnanchorEntity(ITransformComponent transform);
|
||||
void AnchoredEntityDirty(ITransformComponent transform);
|
||||
bool AnchorEntity(TransformComponent transform);
|
||||
void UnanchorEntity(TransformComponent transform);
|
||||
void AnchoredEntityDirty(TransformComponent transform);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMapGridComponent"/>
|
||||
@@ -68,7 +68,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AnchorEntity(ITransformComponent transform)
|
||||
public bool AnchorEntity(TransformComponent transform)
|
||||
{
|
||||
var xform = (TransformComponent) transform;
|
||||
var tileIndices = Grid.TileIndicesFor(transform.Coordinates);
|
||||
@@ -79,7 +79,7 @@ namespace Robust.Shared.GameObjects
|
||||
xform.Parent = Owner.Transform;
|
||||
|
||||
// anchor snapping
|
||||
xform.LocalPosition = Grid.GridTileToLocal(Grid.TileIndicesFor(xform.WorldPosition)).Position;
|
||||
xform.LocalPosition = Grid.GridTileToLocal(tileIndices).Position;
|
||||
|
||||
xform.SetAnchored(result);
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnanchorEntity(ITransformComponent transform)
|
||||
public void UnanchorEntity(TransformComponent transform)
|
||||
{
|
||||
//HACK: Client grid pivot causes this.
|
||||
//TODO: make grid components the actual grid
|
||||
@@ -111,7 +111,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AnchoredEntityDirty(ITransformComponent transform)
|
||||
public void AnchoredEntityDirty(TransformComponent transform)
|
||||
{
|
||||
if (!transform.Anchored)
|
||||
return;
|
||||
|
||||
@@ -22,11 +22,9 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
/// <summary>
|
||||
/// Should this component be removed when no more timers are running?
|
||||
/// N.B. This is set to false because https://github.com/space-wizards/RobustToolbox/pull/2091 caused massive issues.
|
||||
/// Changing this to false won't cause issues but masks the underlying problem, while leaving the option to turn it on for testing available.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool RemoveOnEmpty { get; set; } = false;
|
||||
public bool RemoveOnEmpty { get; set; } = true;
|
||||
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user