mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-01 19:08:19 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
514 lines
12 KiB
C#
514 lines
12 KiB
C#
using Sandbox.Utility;
|
|
using Steamworks;
|
|
using Steamworks.Data;
|
|
|
|
namespace Sandbox;
|
|
|
|
|
|
/// <summary>
|
|
/// Enables rendering and interacting with a webpage
|
|
/// </summary>
|
|
public sealed class WebSurface : IDisposable
|
|
{
|
|
static ISteamHTMLSurface steamHTMLSurface;
|
|
static Dictionary<uint, WeakReference<WebSurface>> All;
|
|
|
|
Task<HTML_BrowserReady_t> initTask;
|
|
Action _initQueue;
|
|
|
|
uint BrowserId;
|
|
|
|
public bool IsLimited { get; private set; }
|
|
|
|
static WebSurface()
|
|
{
|
|
if ( Application.IsUnitTest || Application.IsHeadless )
|
|
return;
|
|
|
|
All = new Dictionary<uint, WeakReference<WebSurface>>();
|
|
steamHTMLSurface = NativeEngine.Steam.SteamHTMLSurface();
|
|
|
|
if ( !steamHTMLSurface.IsValid )
|
|
{
|
|
Log.Warning( "WebSurface unavailable - SteamHTMLSurface is null" );
|
|
return;
|
|
}
|
|
|
|
steamHTMLSurface.Init();
|
|
|
|
Dispatch.Install<HTML_NeedsPaint_t>( x => GetBrowser( x.UnBrowserHandle )?.OnNeedsRepaint( x ) );
|
|
Dispatch.Install<HTML_StartRequest_t>( x => GetBrowser( x.UnBrowserHandle )?.OnStartRequest( x ) );
|
|
Dispatch.Install<HTML_URLChanged_t>( x => GetBrowser( x.UnBrowserHandle )?.OnURLChanged( x ) );
|
|
Dispatch.Install<HTML_ChangedTitle_t>( x => GetBrowser( x.UnBrowserHandle )?.OnChangedTitle( x ) );
|
|
Dispatch.Install<HTML_FinishedRequest_t>( x => GetBrowser( x.UnBrowserHandle )?.OnFinishedRequest( x ) );
|
|
Dispatch.Install<HTML_SetCursor_t>( x => GetBrowser( x.UnBrowserHandle )?.OnSetCursor( x ) );
|
|
}
|
|
|
|
static WebSurface GetBrowser( uint handle )
|
|
{
|
|
lock ( All )
|
|
{
|
|
if ( All.TryGetValue( handle, out var v ) && v.TryGetTarget( out var target ) )
|
|
{
|
|
return target;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public delegate void TextureChangedDelegate( ReadOnlySpan<byte> span, Vector2 size );
|
|
|
|
/// <summary>
|
|
/// Called when the texture has changed and should be updated
|
|
/// </summary>
|
|
public TextureChangedDelegate OnTexture { get; set; }
|
|
|
|
internal WebSurface( bool isLimited )
|
|
{
|
|
IsLimited = isLimited;
|
|
_ = CreateAsync();
|
|
}
|
|
|
|
async Task<HTML_BrowserReady_t> CreateBrowser()
|
|
{
|
|
if ( !steamHTMLSurface.IsValid )
|
|
return default;
|
|
|
|
var callback = steamHTMLSurface.CreateBrowser( "Mozilla/5.0 (Linux; U; X11; en-US; Facepunch Sandbox; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36", null );
|
|
var t = await new CallResult<HTML_BrowserReady_t>( callback, false );
|
|
return t ?? default;
|
|
}
|
|
|
|
async Task CreateAsync()
|
|
{
|
|
initTask = CreateBrowser();
|
|
var s = await initTask;
|
|
|
|
if ( initTask == null )
|
|
return;
|
|
|
|
BrowserId = s.UnBrowserHandle;
|
|
initTask = null;
|
|
|
|
lock ( All )
|
|
{
|
|
All[BrowserId] = new WeakReference<WebSurface>( this );
|
|
}
|
|
|
|
_initQueue?.Invoke();
|
|
_initQueue = null;
|
|
}
|
|
|
|
~WebSurface()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if ( BrowserId == 0 )
|
|
return;
|
|
|
|
lock ( All )
|
|
{
|
|
All.Remove( BrowserId );
|
|
steamHTMLSurface.RemoveBrowser( BrowserId );
|
|
BrowserId = 0;
|
|
|
|
foreach ( var expired in All.Where( x => x.Value.TryGetTarget( out var target ) == false ).Select( x => x.Key ).ToArray() )
|
|
{
|
|
All.Remove( expired );
|
|
}
|
|
}
|
|
}
|
|
|
|
string currentUrl;
|
|
|
|
|
|
public string PageTitle { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The current Url
|
|
/// </summary>
|
|
public string Url
|
|
{
|
|
get => currentUrl;
|
|
set
|
|
{
|
|
if ( BrowserId == 0 )
|
|
{
|
|
_initQueue += () => Url = value;
|
|
return;
|
|
}
|
|
|
|
steamHTMLSurface.LoadURL( BrowserId, value ?? "", null );
|
|
}
|
|
}
|
|
|
|
/*
|
|
* None of this works, in my experience
|
|
*
|
|
internal void SetCookie( string hostname, string key, string value, string path = "/", bool insecure = false )
|
|
{
|
|
var expires = (uint)DateTime.UtcNow.AddDays( 7 ).GetEpoch();
|
|
|
|
if ( insecure )
|
|
{
|
|
steamHTMLSurface.SetCookie( hostname, key, value, path, expires, false, true );
|
|
}
|
|
else
|
|
{
|
|
steamHTMLSurface.SetCookie( hostname, key, value, path, expires, true, true );
|
|
}
|
|
}
|
|
*/
|
|
|
|
static string[] blacklistHostname = new[]
|
|
{
|
|
"*steampowered.com",
|
|
"*steamchina.com",
|
|
"*steamcommunity.com",
|
|
"*steamgames.com",
|
|
"*steamusercontent.com",
|
|
"*steamcontent.com",
|
|
"*steamstatic.com",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Is this URL allowed
|
|
/// </summary>
|
|
internal static void CheckUrlIsAllowed( Uri uri )
|
|
{
|
|
if ( uri.Scheme != "http" && uri.Scheme != "https" )
|
|
throw new InvalidOperationException( $"The url scheme '{uri.Scheme}' is not allowed" );
|
|
|
|
if ( uri.IsUnc )
|
|
throw new InvalidOperationException( $"UNC urls are not allowed" );
|
|
|
|
if ( uri.IsFile )
|
|
throw new InvalidOperationException( $"File urls are not allowed" );
|
|
|
|
if ( uri.HostNameType == UriHostNameType.IPv4 || uri.HostNameType == UriHostNameType.IPv6 )
|
|
throw new InvalidOperationException( $"IP urls are not allowed" );
|
|
|
|
var host = uri.Host;
|
|
if ( blacklistHostname.Any( x => host.WildcardMatch( x ) ) )
|
|
throw new InvalidOperationException( $"You cannot access this hostname" );
|
|
|
|
// Users can opt for local http in editor, don't allow in non-editor please.
|
|
// Opt in is so they know they're doing something that won't work normally
|
|
if ( Application.IsEditor && CommandLine.HasSwitch( "-allowlocalhttp" ) )
|
|
return;
|
|
|
|
if ( uri.IsLoopback )
|
|
throw new InvalidOperationException( $"Loopback urls are not allowed" );
|
|
|
|
try
|
|
{
|
|
if ( uri.IsPrivate() )
|
|
throw new InvalidOperationException( $"Urls that resolve to private IPs are not allowed" );
|
|
}
|
|
catch ( System.Net.Sockets.SocketException e )
|
|
{
|
|
throw new InvalidOperationException( $"Socket error: {e.Message}" );
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
Vector2 _size;
|
|
|
|
/// <summary>
|
|
/// The size of the browser
|
|
/// </summary>
|
|
public Vector2 Size
|
|
{
|
|
get => _size;
|
|
set
|
|
{
|
|
if ( BrowserId == 0 )
|
|
{
|
|
_initQueue += () => Size = value;
|
|
return;
|
|
}
|
|
|
|
_size = value;
|
|
steamHTMLSurface.SetSize( BrowserId, (uint)value.x, (uint)value.y );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the browser needs to be repainted
|
|
/// </summary>
|
|
private unsafe void OnNeedsRepaint( HTML_NeedsPaint_t x )
|
|
{
|
|
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>( (void*)x.PBGRA, (int)(x.UnWide * x.UnTall * 4) );
|
|
|
|
var size = new Vector2( x.UnWide, x.UnTall );
|
|
_size = size;
|
|
OnTexture?.Invoke( s, size );
|
|
}
|
|
|
|
private void OnChangedTitle( HTML_ChangedTitle_t x )
|
|
{
|
|
PageTitle = x.PchTitle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A navigation has happened, allow or deny it
|
|
/// </summary>
|
|
/// <param name="r"></param>
|
|
private unsafe void OnStartRequest( HTML_StartRequest_t r )
|
|
{
|
|
var url = r.PchURL;
|
|
|
|
try
|
|
{
|
|
if ( IsLimited )
|
|
CheckUrlIsAllowed( new Uri( url ) );
|
|
}
|
|
catch ( InvalidOperationException e )
|
|
{
|
|
Log.Warning( $"{e.Message}" );
|
|
steamHTMLSurface.AllowStartRequest( BrowserId, false );
|
|
return;
|
|
}
|
|
|
|
steamHTMLSurface.AllowStartRequest( BrowserId, true );
|
|
currentUrl = url;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the browser is navigating to a new url
|
|
/// </summary>
|
|
/// <param name="x"></param>
|
|
private unsafe void OnURLChanged( HTML_URLChanged_t x )
|
|
{
|
|
currentUrl = x.PchURL;
|
|
}
|
|
|
|
|
|
private void OnFinishedRequest( HTML_FinishedRequest_t x )
|
|
{
|
|
currentUrl = x.PchURL;
|
|
PageTitle = x.PchPageTitle;
|
|
}
|
|
|
|
public string Cursor { get; private set; }
|
|
|
|
enum EMouseCursor
|
|
{
|
|
dc_user = 0,
|
|
dc_none,
|
|
dc_arrow,
|
|
dc_ibeam,
|
|
dc_hourglass,
|
|
dc_waitarrow,
|
|
dc_crosshair,
|
|
dc_up,
|
|
dc_sizenw,
|
|
dc_sizese,
|
|
dc_sizene,
|
|
dc_sizesw,
|
|
dc_sizew,
|
|
dc_sizee,
|
|
dc_sizen,
|
|
dc_sizes,
|
|
dc_sizewe,
|
|
dc_sizens,
|
|
dc_sizeall,
|
|
dc_no,
|
|
dc_hand,
|
|
dc_blank, // don't show any custom cursor, just use your default
|
|
dc_middle_pan,
|
|
dc_north_pan,
|
|
dc_north_east_pan,
|
|
dc_east_pan,
|
|
dc_south_east_pan,
|
|
dc_south_pan,
|
|
dc_south_west_pan,
|
|
dc_west_pan,
|
|
dc_north_west_pan,
|
|
dc_alias,
|
|
dc_cell,
|
|
dc_colresize,
|
|
dc_copycur,
|
|
dc_verticaltext,
|
|
dc_rowresize,
|
|
dc_zoomin,
|
|
dc_zoomout,
|
|
dc_help,
|
|
dc_custom,
|
|
|
|
dc_last, // custom cursors start from this value and up
|
|
}
|
|
|
|
private void OnSetCursor( HTML_SetCursor_t x )
|
|
{
|
|
Cursor = "default";
|
|
|
|
if ( x.EMouseCursor == (int)EMouseCursor.dc_hand )
|
|
Cursor = "pointer";
|
|
|
|
if ( x.EMouseCursor == (int)EMouseCursor.dc_ibeam )
|
|
Cursor = "text";
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tell the browser the mouse has moved
|
|
/// </summary>
|
|
public void TellMouseMove( Vector2 position )
|
|
{
|
|
steamHTMLSurface.MouseMove( BrowserId, (int)position.x, (int)position.y );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tell the browser the mouse wheel has moved
|
|
/// </summary>
|
|
/// <param name="delta"></param>
|
|
public void TellMouseWheel( int delta )
|
|
{
|
|
steamHTMLSurface.MouseWheel( BrowserId, delta );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tell the browser a mouse button has been pressed
|
|
/// </summary>
|
|
public void TellMouseButton( MouseButtons button, bool state )
|
|
{
|
|
if ( (button & MouseButtons.Left) != 0 )
|
|
{
|
|
if ( state ) steamHTMLSurface.MouseDown( BrowserId, 0 );
|
|
else steamHTMLSurface.MouseUp( BrowserId, 0 );
|
|
}
|
|
|
|
if ( (button & MouseButtons.Right) != 0 )
|
|
{
|
|
if ( state ) steamHTMLSurface.MouseDown( BrowserId, 1 );
|
|
else steamHTMLSurface.MouseUp( BrowserId, 1 );
|
|
}
|
|
|
|
if ( (button & MouseButtons.Middle) != 0 )
|
|
{
|
|
if ( state ) steamHTMLSurface.MouseDown( BrowserId, 2 );
|
|
else steamHTMLSurface.MouseUp( BrowserId, 2 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tell the browser a unicode key has been pressed
|
|
/// </summary>
|
|
public void TellChar( uint unicodeKey, KeyboardModifiers modifiers )
|
|
{
|
|
// don't allow paste
|
|
if ( IsLimited && modifiers.Contains( KeyboardModifiers.Ctrl ) )
|
|
{
|
|
if ( unicodeKey == 'x' ) return;
|
|
if ( unicodeKey == 'c' ) return;
|
|
if ( unicodeKey == 'v' ) return;
|
|
}
|
|
|
|
steamHTMLSurface.KeyChar( BrowserId, unicodeKey, (int)modifiers );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tell the browser a key has been pressed or released
|
|
/// </summary>
|
|
public void TellKey( uint virtualKeyCode, KeyboardModifiers modifiers, bool state )
|
|
{
|
|
//
|
|
// Don't allow paste. Since the potential is there to open a webpage and simulate
|
|
// key presses to past the contents of the clipboard into a hidden textarea and
|
|
// record that shit on a server somewhere. They could do this without the game having
|
|
// focus - so anything happening in the background could get captured.
|
|
//
|
|
// There is a strategy to allow this, by having Copy, Paste, Cut functions that take some
|
|
// special token class that only we can make, that come as part of the UI panel input systems.
|
|
// but we'll leave that implementation until someone notices and cares.
|
|
//
|
|
|
|
|
|
if ( IsLimited && modifiers.Contains( KeyboardModifiers.Ctrl ) )
|
|
{
|
|
if ( virtualKeyCode == 86 ) return;
|
|
if ( virtualKeyCode == 67 ) return;
|
|
if ( virtualKeyCode == 88 ) return;
|
|
}
|
|
|
|
if ( state ) steamHTMLSurface.KeyDown( BrowserId, virtualKeyCode, (int)modifiers, false );
|
|
else steamHTMLSurface.KeyUp( BrowserId, virtualKeyCode, (int)modifiers );
|
|
}
|
|
|
|
bool _keyFocus;
|
|
|
|
/// <summary>
|
|
/// Tell the html control if it has key focus currently, controls showing the I-beam cursor in text controls amongst other things
|
|
/// </summary>
|
|
public bool HasKeyFocus
|
|
{
|
|
get => _keyFocus;
|
|
|
|
set
|
|
{
|
|
if ( BrowserId == 0 )
|
|
{
|
|
_initQueue += () => HasKeyFocus = value;
|
|
return;
|
|
}
|
|
|
|
_keyFocus = value;
|
|
steamHTMLSurface.SetKeyFocus( BrowserId, value );
|
|
}
|
|
}
|
|
|
|
float _scaleFactor = 1.0f;
|
|
|
|
/// <summary>
|
|
/// DPI Scaling factor
|
|
/// </summary>
|
|
public float ScaleFactor
|
|
{
|
|
get => _scaleFactor;
|
|
|
|
set
|
|
{
|
|
if ( BrowserId == 0 )
|
|
{
|
|
_initQueue += () => ScaleFactor = value;
|
|
return;
|
|
}
|
|
|
|
_scaleFactor = value;
|
|
steamHTMLSurface.SetDPIScalingFactor( BrowserId, value );
|
|
}
|
|
}
|
|
|
|
bool _backgroudMode;
|
|
|
|
/// <summary>
|
|
/// Enable/disable low-resource background mode, where javascript and repaint timers are throttled, resources are
|
|
/// more aggressively purged from memory, and audio/video elements are paused. When background mode is enabled,
|
|
/// all HTML5 video and audio objects will execute ".pause()" and gain the property "._steam_background_paused = 1".
|
|
/// When background mode is disabled, any video or audio objects with that property will resume with ".play()".
|
|
/// </summary>
|
|
public bool InBackgroundMode
|
|
{
|
|
get => _backgroudMode;
|
|
set
|
|
{
|
|
if ( BrowserId == 0 )
|
|
{
|
|
_initQueue += () => InBackgroundMode = value;
|
|
return;
|
|
}
|
|
|
|
_backgroudMode = value;
|
|
steamHTMLSurface.SetBackgroundMode( BrowserId, value );
|
|
}
|
|
}
|
|
}
|