Compare commits

...

53 Commits

Author SHA1 Message Date
Vera Aguilera Puerto
9b215098e4 Remove ITransformComponent. 2021-11-08 12:49:55 +01:00
Vera Aguilera Puerto
78d01fd25d Remove every ITransformComponent usage. 2021-11-08 12:44:03 +01:00
Vera Aguilera Puerto
8e8bfbe0cc Obsolete ITransformComponent 2021-11-08 12:28:49 +01:00
Vera Aguilera Puerto
45b0a49ffb Fix build 2021-11-08 12:23:22 +01:00
Vera Aguilera Puerto
58638c9109 Make TransformComponent public, IEntity keeps a reference to TransformComponent. 2021-11-08 12:21:34 +01:00
Vera Aguilera Puerto
2f2a397ecf Fix TransformComponent access modifiers (#2207)
Part 1 of ITransformComponent removal.
2021-11-08 12:18:53 +01:00
Vera Aguilera Puerto
78c551d854 Remove DummyIconEntity, use actual entities instead. (#2206) 2021-11-08 12:17:13 +01:00
Azzy
679e07d9ea Fixes ViewVariables sprite rotation bug (#2202) 2021-11-07 18:47:27 +11:00
DrSmugleaf
fa5d0235ec Prepend Client and Server to tests ran message 2021-11-06 11:42:01 +01:00
Javier Guardia Fernández
04d029b9a2 Add test pooling (#2146)
* Add test pooling and global test setup

* WIP test pooling changes

* Make asynchronous tests the default again

* Finish fixing tests, make test threads background threads

* Un-pool tests with custom cvars

* Fix not changing FailureLogLevel cvar on instance return

* Fix error when overriding already overriden cvar

* Don't pool some physics integration tests

* Unpool engine tests, the technology just isn't there yet

* Remove explicit Pool = false from physics tests

* Change default pooling setting to false

* Didn't need this anyway

* Remove ConfigurationManager.ResetOverrides

* Bring back enum cvar override parsing

* Make integrationInstances name clearer > notPooledInstaces
Make clients ready and servers ready internal

* Add logging test instances

* Give more info on ran tests

* Show total clients and servers in test output

* Wipe LayerMap on SpriteComponent.AfterDeserialize

* Fix server not properly kicking clients

* Rider shut

* Make all test metrics report totals

* Format tests ran better

* Replace Console.WriteLine with TestContext.Out.WriteLine

* Fix two server test pooling info prints using total clients instead of total servers

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2021-11-06 11:18:18 +01:00
Pieter-Jan Briers
6e84821233 Package CEF as NuGet package, does not need to be manually downloaded anymore. 2021-11-05 14:50:47 +01:00
Pieter-Jan Briers
881cfeb9a9 Small IRawInputControl doc. 2021-11-05 14:50:47 +01:00
Pieter-Jan Briers
0187e85700 Eye offset no longer offsets FOV.
This means camera recoil in SS14 won't clip into walls.
2021-11-04 17:37:00 +01:00
Pieter-Jan Briers
dee8a87acd Use headless WebViewManager on headless clients. 2021-11-03 15:26:52 +01:00
Pieter-Jan Briers
d4a7e8f3e0 Move CEF RequestResult to correct folder. 2021-11-03 15:14:46 +01:00
Pieter-Jan Briers
dae6424667 Allow swapping out internal WebViewManager implementation.
Everything moved to interfaces.
2021-11-03 15:12:49 +01:00
Pieter-Jan Briers
3770149cfc Update CefGlue 2021-11-03 15:03:25 +01:00
Alex Evgrashin
341c279265 Bandaid sprite animation in SpriteView (#2181) 2021-11-02 19:50:29 +01:00
metalgearsloth
37c20723ab Add WorldBounds as an arg for overlays (#2190) 2021-11-02 18:00:06 +01:00
ike709
c3e4a64ad7 Makes client connection timeout a cvar (#2186) 2021-11-02 17:56:17 +01:00
metalgearsloth
225446920a Make transform delete entity if parent invalid (#2193) 2021-11-02 17:54:48 +01:00
Saphire Lattice
9b2a50b1a8 Make camera lerp towards the grid rotation, and keep rotation when stepping off onto the world grid (#2187)
* Make camera lerp towards the grid rotation, and keep rotation when stepping off onto the world grid

* Fix lerp targeting, add a bunch of comments
2021-11-02 13:49:28 +01:00
metalgearsloth
b4ed513e8c Optimise anchor snap 2021-11-02 16:55:12 +11:00
20kdc
c28f2d77c3 Placement Manager: Spin Cycle 2 (#2184) 2021-11-02 16:21:03 +11:00
metalgearsloth
3e344d00a8 Move PVS to a system (#2189) 2021-11-02 16:19:11 +11:00
moonheart08
c321400347 Fix savemap/loadmap arguments. (#2055) 2021-11-02 16:07:31 +11:00
metalgearsloth
a4ff5d65ec Dirty changes (#2174) 2021-11-02 16:05:45 +11:00
Pieter-Jan Briers
97c70124a5 [CEF] properly close streams when loading from res:// and such. 2021-11-02 01:53:38 +01:00
Pieter-Jan Briers
cdcc5239ab Rename WebView control to WebViewControl to avoid namespace name clash. 2021-11-01 21:06:30 +01:00
Pieter-Jan Briers
ba2f464249 Restructure CEF stuff in preparation for launcher packaging.
Robust.Client.CEF Renamed to Robust.Client.WebView since CEF should really be an implementation detail.
Content is no longer responsible for initializing and managing the module, this is done automatically by the engine.
WebView is initialized by declaring it in a manifest.yml file in the game resources. In the future the launcher will read this same file to manage WebView module versions.
CefManager has been made private and the content-visible API is now IWebViewManager.
2021-11-01 21:03:51 +01:00
Paul
4210f30460 fixes positional audio not accounting for rotation 2021-10-31 22:49:05 +01:00
moonheart08
9fc95591d9 Introduce BQLv2, part 1 (#2170) 2021-10-31 20:23:13 +01:00
E F R
0a59079a4a Graphics: add a Font variant that supports font stacking (#2182) 2021-10-31 14:48:58 +01:00
20kdc
89f168c04d Improves PlacementManager (construction/etc.) handling of parent world rotation (#2165) 2021-10-31 14:47:18 +01:00
Visne
f11ac39cd5 Fix all warnings (#2171) 2021-10-31 12:22:58 +01:00
metalgearsloth
380de8c4c3 Expose OwnerUid on Component (#2183) 2021-10-31 11:35:21 +01:00
Vera Aguilera Puerto
539c161ea3 Fix client build when scripting is disabled. 2021-10-31 10:55:31 +01:00
metalgearsloth
b07187459b Fix physics for invalid parents 2021-10-31 15:24:12 +11:00
Pieter-Jan Briers
97fb54b6d7 Registry based HWIDs for Windows. 2021-10-30 14:21:23 +02:00
wixoa
fe30d974ca Add ImageSharp's Image.Load<>() to the sandbox (#2179) 2021-10-30 13:26:42 +02:00
E F R
884fade25a Add tab "completion" for the server-side C# console (#2176) 2021-10-30 13:26:23 +02:00
Pieter-Jan Briers
a3ab745121 Fix debug console not animating when dropping down for the first time. 2021-10-29 17:41:51 +02:00
ShadowCommander
4e0ad23272 Fix sound orientation when eye is rotated (#2177) 2021-10-29 16:49:16 +02:00
Vera Aguilera Puerto
b9b9cd0711 Revert "Fix PVS crash (#2173)"
This reverts commit b05b1a7c86.
2021-10-28 17:58:23 +02:00
metalgearsloth
b05b1a7c86 Fix PVS crash (#2173) 2021-10-29 01:26:39 +11:00
Visne
4ced901358 Remove SpriteComponent.Directional (#2172) 2021-10-28 13:20:06 +02:00
metalgearsloth
898d5a3ba5 Spaghettifix GetEntitiesIntersecting (#2167) 2021-10-27 08:55:24 +02:00
Visne
068c05c355 Use enum for access level in XamlUiPartialClassGenerator (#2161) 2021-10-27 08:52:09 +02:00
Saphire Lattice
cd693875e6 Fix resizing (#2160)
* Fix resizing moving the window beyond its minimum size and causing it to move visually

* Do a NaN-check the SetSize when resizing

* Use float instead to avoid conversion

As per PJB3005's suggestion

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2021-10-27 12:45:36 +11:00
20kdc
e2723a83b3 Revert "Technically fixes lathes, but not properly, but doesn't make things worse." :D (#2166)
This reverts commit cb19430d5c.
2021-10-27 12:44:34 +11:00
20kdc
f6ac8fbe1f Expose the Box2Rotated intersecting function in IEntityLookup for Content use (#2164) 2021-10-26 00:27:55 +11:00
metalgearsloth
a1e0f18bd6 Add Box2Rotated support to EntityLookup (#2163) 2021-10-25 15:00:18 +02:00
moonheart08
1e7a481911 better spawn command (#2156)
* better spawn command

* Update Robust.Server/Console/Commands/SpawnCommand.cs

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>

* Update Robust.Server/Console/Commands/SpawnCommand.cs

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>

* ok help

this is a web edit, sue me!

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
2021-10-24 22:15:47 -07:00
137 changed files with 3857 additions and 1695 deletions

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
using System;
namespace Robust.Client.CEF
{
public interface IBrowserWindow : IBrowserControl, IDisposable
{
bool Closed { get; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
namespace Robust.Client.CEF
namespace Robust.Client.WebView
{
public sealed class BrowserWindowCreateParameters
{

View File

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

View File

@@ -1,7 +1,7 @@

using System.Diagnostics.CodeAnalysis;
namespace Robust.Client.CEF
namespace Robust.Client.WebView.Cef
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]

View File

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

View File

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

View 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.
}
}
} */
}
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
using Xilium.CefGlue;
namespace Robust.Client.CEF
namespace Robust.Client.WebView.Cef
{
// Simple CEF client.
internal class RobustCefClient : CefClient

View File

@@ -1,6 +1,6 @@
using Xilium.CefGlue;
namespace Robust.Client.CEF
namespace Robust.Client.WebView.Cef
{
public sealed class RobustLoadHandler : CefLoadHandler
{

View File

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

View File

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

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

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

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

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

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

View File

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

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

View File

@@ -0,0 +1,7 @@
namespace Robust.Client.WebView
{
public interface IWebViewManager
{
IWebViewWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams);
}
}

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

View File

@@ -0,0 +1,7 @@
namespace Robust.Client.WebView
{
internal interface IWebViewManagerInternal : IWebViewManager
{
IWebViewControlImpl MakeControlImpl(WebViewControl owner);
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace Robust.Client.WebView
{
public interface IWebViewWindow : IWebViewControl, IDisposable
{
bool Closed { get; }
}
}

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

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
namespace Robust.Client.UserInterface
{
public enum AccessLevel
{
Public,
Protected,
Internal,
ProtectedInternal,
Private,
PrivateProtected,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
internal sealed class TransformSystem : SharedTransformSystem
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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