From c38e74a1e369994bf652dbd947d2ae879b608c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20M=C3=B8llegaard=20Pedersen?= Date: Wed, 4 Aug 2021 17:43:18 +0200 Subject: [PATCH] Introducing WebWidget WebWidget is userinterface controller via regular lua scripts, but are implemented via HTML/CSS/Javascript, served by Slipstream via a HTTP and WebSocket endpoint. HTTP is used to serve the assets, while websocket is used for communication between Lua and Javascript. LuaScripts have been reorganized, now that the samples might also include WebWidgets. Instead of LuaScripts you'll find a Samples directory --- Backend/WebWidget/InstanceIndex.html | 10 ++ Backend/WebWidget/ss.js | 52 +++++++ CHANGELOG.md | 4 + .../Twitch/Events/TwitchReceivedMessage.cs | 2 - .../EventFactory/WebWidgetEventFactory.cs | 16 +++ .../EventHandler/WebWidgetEventHandler.cs | 32 +++++ .../WebWidget/Events/WebWidgetCommandEvent.cs | 13 ++ Components/WebWidget/HttpServer.cs | 133 ++++++++++++++++++ Components/WebWidget/IHttpServer.cs | 10 ++ Components/WebWidget/IHttpServerApi.cs | 11 ++ .../WebWidget/IWebWidgetEventFactory.cs | 12 ++ Components/WebWidget/IWebWidgetInstances.cs | 16 +++ .../WebWidget/InstanceIndexWebModule.cs | 87 ++++++++++++ Components/WebWidget/InstanceWebModule.cs | 74 ++++++++++ Components/WebWidget/JavascriptWebModule.cs | 48 +++++++ .../WebWidget/Lua/IWebWidgetInstanceThread.cs | 11 ++ .../WebWidget/Lua/WebWidgetInstanceThread.cs | 36 +++++ .../WebWidget/Lua/WebWidgetLuaLibrary.cs | 53 +++++++ .../WebWidget/Lua/WebWidgetLuaReference.cs | 29 ++++ Components/WebWidget/README.md | 95 +++++++++++++ .../WebWidget/WebSocketsEventsServer.cs | 77 ++++++++++ Components/WebWidget/WebWidgetInstances.cs | 54 +++++++ .../WinFormUI/Services/EventDataService.cs | 2 +- LuaScripts/debug.lua | 19 --- Program.cs | 2 + .../lib => Samples/Common/Scripts}/common.lua | 0 .../lib => Samples/Common/Scripts}/config.lua | 0 Samples/IRacing - Audio - Prechecks/README.md | 6 + .../Scripts}/iracing_prechecks.lua | 0 .../IRacing - Twitch - Car Command/README.md | 7 + .../Scripts}/iracing_car.lua | 3 - .../IRacing - Twitch - IR Command/README.md | 9 ++ .../Scripts}/iracing_ir.lua | 10 -- .../IRacing - Twitch - SoF Command/README.md | 6 + .../Scripts}/iracing_sof.lua | 3 - .../README.md | 6 + .../Scripts}/iracing_track.lua | 3 - .../README.md | 7 + .../Scripts/chatstate.lua | 25 ++++ .../WebWidgets/Textbox/controller.js | 63 +++++++++ .../WebWidgets/Textbox/index.html | 15 ++ .../WebWidgets/Textbox/style.css | 64 +++++++++ Shared/Lua/BaseInstanceThread.cs | 2 +- Shared/Lua/BaseLuaLibrary.cs | 2 +- Shared/Lua/SingletonLuaLibrary.cs | 4 + Slipstream.csproj | 54 +++++-- packages.config | 2 + 47 files changed, 1137 insertions(+), 52 deletions(-) create mode 100644 Backend/WebWidget/InstanceIndex.html create mode 100644 Backend/WebWidget/ss.js create mode 100644 Components/WebWidget/EventFactory/WebWidgetEventFactory.cs create mode 100644 Components/WebWidget/EventHandler/WebWidgetEventHandler.cs create mode 100644 Components/WebWidget/Events/WebWidgetCommandEvent.cs create mode 100644 Components/WebWidget/HttpServer.cs create mode 100644 Components/WebWidget/IHttpServer.cs create mode 100644 Components/WebWidget/IHttpServerApi.cs create mode 100644 Components/WebWidget/IWebWidgetEventFactory.cs create mode 100644 Components/WebWidget/IWebWidgetInstances.cs create mode 100644 Components/WebWidget/InstanceIndexWebModule.cs create mode 100644 Components/WebWidget/InstanceWebModule.cs create mode 100644 Components/WebWidget/JavascriptWebModule.cs create mode 100644 Components/WebWidget/Lua/IWebWidgetInstanceThread.cs create mode 100644 Components/WebWidget/Lua/WebWidgetInstanceThread.cs create mode 100644 Components/WebWidget/Lua/WebWidgetLuaLibrary.cs create mode 100644 Components/WebWidget/Lua/WebWidgetLuaReference.cs create mode 100644 Components/WebWidget/README.md create mode 100644 Components/WebWidget/WebSocketsEventsServer.cs create mode 100644 Components/WebWidget/WebWidgetInstances.cs delete mode 100644 LuaScripts/debug.lua rename {LuaScripts/lib => Samples/Common/Scripts}/common.lua (100%) rename {LuaScripts/lib => Samples/Common/Scripts}/config.lua (100%) create mode 100644 Samples/IRacing - Audio - Prechecks/README.md rename {LuaScripts => Samples/IRacing - Audio - Prechecks/Scripts}/iracing_prechecks.lua (100%) create mode 100644 Samples/IRacing - Twitch - Car Command/README.md rename {LuaScripts => Samples/IRacing - Twitch - Car Command/Scripts}/iracing_car.lua (91%) create mode 100644 Samples/IRacing - Twitch - IR Command/README.md rename {LuaScripts => Samples/IRacing - Twitch - IR Command/Scripts}/iracing_ir.lua (88%) create mode 100644 Samples/IRacing - Twitch - SoF Command/README.md rename {LuaScripts => Samples/IRacing - Twitch - SoF Command/Scripts}/iracing_sof.lua (90%) create mode 100644 Samples/IRacing - Twitch - Track Command/README.md rename {LuaScripts => Samples/IRacing - Twitch - Track Command/Scripts}/iracing_track.lua (91%) create mode 100644 Samples/IRacing - WebWidget - ChatOn Command/README.md create mode 100644 Samples/IRacing - WebWidget - ChatOn Command/Scripts/chatstate.lua create mode 100644 Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/controller.js create mode 100644 Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/index.html create mode 100644 Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/style.css diff --git a/Backend/WebWidget/InstanceIndex.html b/Backend/WebWidget/InstanceIndex.html new file mode 100644 index 00000000..20ee33b1 --- /dev/null +++ b/Backend/WebWidget/InstanceIndex.html @@ -0,0 +1,10 @@ + + + Slipstream Actice WebWidget Instances + + +

Slipstream Actice WebWidget Instances

+ + {{CONTENT}} + + \ No newline at end of file diff --git a/Backend/WebWidget/ss.js b/Backend/WebWidget/ss.js new file mode 100644 index 00000000..cac15bfd --- /dev/null +++ b/Backend/WebWidget/ss.js @@ -0,0 +1,52 @@ +var INSTANCE_ID +var WEB_WIDGET_TYPE +var ASSETS + +function connect() { + let socket = a = new WebSocket("ws://" + document.location.host + "/events/" + INSTANCE_ID) + + socket.onopen = function (e) { + console.log("[slipstream ws] [open] Connection established") + + if (typeof(onConnect) === "function") { + onConnect() + } + } + + socket.onmessage = function (event) { + if (typeof (onData) !== "function") { + console.log("[slipstream ws] [onmessage] got data, but no onData() function defined. Ignored", event.data) + } + else { + console.log("[slipstream ws] [onmessage] got data", event.data) + onData(JSON.parse(event.data)) + } + } + + socket.onclose = function (event) { + if (event.wasClean) { + console.log("[slipstream ws] [close] Connection closed cleanly, code=" + event.code + " reason=" + event.reason) + } else { + console.log('[slipstream ws] [close] Connection died') + } + + if (typeof (onDisconnect) === "function") { + onDisconnect() + } + + setTimeout(connect, 1000); + } + + socket.onerror = function (error) { + console.log("[slipstream ws] [error] " + error.message) + socket.close() + } +} + +window.addEventListener('load', function () { + INSTANCE_ID = document.body.getAttribute("data-instance-id") + WEB_WIDGET_TYPE = document.body.getAttribute("data-web-widget-type") + ASSETS = document.body.getAttribute("data-assets") + + connect() +}) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8a9a68..77635b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ And only scripts using that instance will be notified of its events. Meaning, you will not get IRacing events, if you don't have require("api/iracing"):instance(..) - You need a new init.lua, so delete your existing init.lua to get a new one. + - IRacing: Adds pit-stop lua functions: pit_clear_all(), pit_clear_tyres(), + pit_fast_repair(), pit_add_fuel(), pit_change_(left|right)_(front|rear)_tyre() + and pit_clean_windshield() + - New Component: WebWidgets - Merged UI components into WinFormUI. You need to change `require("api/ui")` to `require("api/winformui")` diff --git a/Components/Twitch/Events/TwitchReceivedMessage.cs b/Components/Twitch/Events/TwitchReceivedMessage.cs index e003c47b..33daee9d 100644 --- a/Components/Twitch/Events/TwitchReceivedMessage.cs +++ b/Components/Twitch/Events/TwitchReceivedMessage.cs @@ -8,9 +8,7 @@ namespace Slipstream.Components.Twitch.Events public class TwitchReceivedMessage : IEvent { public string EventType => nameof(TwitchReceivedMessage); - public IEventEnvelope Envelope { get; set; } = new EventEnvelope(); - [Description("User that sent the message")] public string From { get; set; } = string.Empty; diff --git a/Components/WebWidget/EventFactory/WebWidgetEventFactory.cs b/Components/WebWidget/EventFactory/WebWidgetEventFactory.cs new file mode 100644 index 00000000..8cdd71e9 --- /dev/null +++ b/Components/WebWidget/EventFactory/WebWidgetEventFactory.cs @@ -0,0 +1,16 @@ +using Slipstream.Components.WebWidget; +using Slipstream.Components.WebWidget.Events; +using Slipstream.Shared; + +#nullable enable + +namespace Slipstream.Components.Internal.EventFactory +{ + public class WebWidgetEventFactory : IWebWidgetEventFactory + { + public WebWidgetCommandEvent CreateWebWidgetCommandEvent(IEventEnvelope envelope, string data) + { + return new WebWidgetCommandEvent { Envelope = envelope, Data = data }; + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/EventHandler/WebWidgetEventHandler.cs b/Components/WebWidget/EventHandler/WebWidgetEventHandler.cs new file mode 100644 index 00000000..0ac85866 --- /dev/null +++ b/Components/WebWidget/EventHandler/WebWidgetEventHandler.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Slipstream.Components.WebWidget.Events; +using Slipstream.Shared; +using System; + +namespace Slipstream.Components.WebWidget.EventHandler +{ + internal class WebWidgetEventHandler : IEventHandler + { + public event EventHandler? OnWebWidgetCommandEvent; + + public IEventHandler.HandledStatus HandleEvent(IEvent @event) + { + return @event switch + { + WebWidgetCommandEvent tev => OnEvent(OnWebWidgetCommandEvent, tev), + _ => IEventHandler.HandledStatus.NotMine, + }; + } + + private IEventHandler.HandledStatus OnEvent(EventHandler? onEvent, TEvent args) + { + if (onEvent != null) + { + onEvent.Invoke(this, args); + return IEventHandler.HandledStatus.Handled; + } + return IEventHandler.HandledStatus.UseDefault; + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/Events/WebWidgetCommandEvent.cs b/Components/WebWidget/Events/WebWidgetCommandEvent.cs new file mode 100644 index 00000000..a7a20275 --- /dev/null +++ b/Components/WebWidget/Events/WebWidgetCommandEvent.cs @@ -0,0 +1,13 @@ +#nullable enable + +using Slipstream.Shared; + +namespace Slipstream.Components.WebWidget.Events +{ + public class WebWidgetCommandEvent : IEvent + { + public string EventType => typeof(WebWidgetCommandEvent).Name; + public IEventEnvelope Envelope { get; set; } = new EventEnvelope(); + public string Data { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Components/WebWidget/HttpServer.cs b/Components/WebWidget/HttpServer.cs new file mode 100644 index 00000000..a5f2a6c8 --- /dev/null +++ b/Components/WebWidget/HttpServer.cs @@ -0,0 +1,133 @@ +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO; +using Serilog; +using Slipstream.Components.WebWidget.EventHandler; +using Slipstream.Shared; + +namespace Slipstream.Components.WebWidget +{ + public class HttpServer : IHttpServer, IHttpServerApi + { + private const string WEB_WIDGET_ROOT_DIRECTORY = "WebWidgets/"; + + private readonly Object Lock = new object(); + private Thread? ServiceThread; + private readonly ILogger Logger; + private readonly IEventBusSubscription Subscription; + private readonly WebSocketsEventsServer EventsServerModule; + private readonly IEventHandlerController EventHandlerController; + private readonly IWebWidgetInstances Instances = new WebWidgetInstances(); + private const string Url = "http://127.0.0.1:1919"; // Must NOT end with slash + + public HttpServer(ILogger logger, IEventBusSubscription subscription, IEventHandlerController eventHandlerController) + { + Logger = logger; + Subscription = subscription; + EventHandlerController = eventHandlerController; + + EventsServerModule = new WebSocketsEventsServer(Logger, Instances); + + System.IO.Directory.CreateDirectory(WEB_WIDGET_ROOT_DIRECTORY); + } + + public void AddInstance(string instanceId, string webWidgetType, string? data) + { + lock (Lock) + { + if (ServiceThread == null) + { + ServiceThread = new Thread(new ThreadStart(ThreadMain)) + { + Name = GetType().Name, + }; + ServiceThread.Start(); + } + } + + // Crude sanity check + var indexFile = WEB_WIDGET_ROOT_DIRECTORY + webWidgetType + "/index.html"; + if (System.IO.File.Exists(indexFile)) + { + Instances.Add(instanceId, webWidgetType, data); + Subscription.AddImpersonate(instanceId); + + Logger.Information($"HttpServer: {Url}/instances/{instanceId} added"); + } + else + { + Logger.Error($"HttpServer: {Url}/instances/{instanceId} not added, as {indexFile} does not exist"); + } + } + + private void ThreadMain() + { + Logger.Information("HttpServer started"); + + using (var server = CreateWebServer(Url)) + { + Task serverTask = server.RunAsync(); + bool stopping = false; + + var internalHandler = EventHandlerController.Get(); + var webWidgetHandler = EventHandlerController.Get(); + + internalHandler.OnInternalCommandShutdown += (_, e) => stopping = true; + webWidgetHandler.OnWebWidgetCommandEvent += (_, e) => + { + if (e.Envelope.Recipients == null) + return; + + foreach (var recipient in e.Envelope.Recipients) + { + EventsServerModule.Broadcast(recipient, e.Data); + } + }; + + while (!stopping) + { + IEvent? @event = Subscription.NextEvent(100); + EventHandlerController.HandleEvent(@event); + + stopping = stopping || serverTask.IsCompleted; + } + } + + Logger.Information("HttpServer stopped"); + } + + public void RemoveInstance(string instanceId) + { + Instances.Remove(instanceId); + Subscription.DeleteImpersonation(instanceId); + Logger.Information($"HttpServer: {Url}/instances/{instanceId} removed"); + } + + private WebServer CreateWebServer(string url) + { + // endpoints: + // / + // /ss.js + // /instances// + // /webwidgets// + // /events// + + var server = new WebServer(o => o + .WithUrlPrefix(url)) + .WithStaticFolder("/webwidgets/", WEB_WIDGET_ROOT_DIRECTORY, false) + .WithModule(EventsServerModule) + .WithModule(new JavascriptWebModule()) + .WithModule(new InstanceWebModule(Instances, WEB_WIDGET_ROOT_DIRECTORY, Logger)) + .WithModule(new InstanceIndexWebModule(Instances, Logger)); + + return server; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/IHttpServer.cs b/Components/WebWidget/IHttpServer.cs new file mode 100644 index 00000000..3de14261 --- /dev/null +++ b/Components/WebWidget/IHttpServer.cs @@ -0,0 +1,10 @@ +#nullable enable + +using System; + +namespace Slipstream.Components.WebWidget +{ + public interface IHttpServer : IDisposable + { + } +} \ No newline at end of file diff --git a/Components/WebWidget/IHttpServerApi.cs b/Components/WebWidget/IHttpServerApi.cs new file mode 100644 index 00000000..b6ed54c8 --- /dev/null +++ b/Components/WebWidget/IHttpServerApi.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Slipstream.Components.WebWidget +{ + public interface IHttpServerApi + { + // These needs to be threadsafe! + void AddInstance(string instanceId, string instanceType, string? data); + void RemoveInstance(string instanceId); + } +} \ No newline at end of file diff --git a/Components/WebWidget/IWebWidgetEventFactory.cs b/Components/WebWidget/IWebWidgetEventFactory.cs new file mode 100644 index 00000000..f4f374ad --- /dev/null +++ b/Components/WebWidget/IWebWidgetEventFactory.cs @@ -0,0 +1,12 @@ +using Slipstream.Components.WebWidget.Events; +using Slipstream.Shared; + +#nullable enable + +namespace Slipstream.Components.WebWidget +{ + public interface IWebWidgetEventFactory + { + WebWidgetCommandEvent CreateWebWidgetCommandEvent(IEventEnvelope envelope, string data); + } +} \ No newline at end of file diff --git a/Components/WebWidget/IWebWidgetInstances.cs b/Components/WebWidget/IWebWidgetInstances.cs new file mode 100644 index 00000000..6c923822 --- /dev/null +++ b/Components/WebWidget/IWebWidgetInstances.cs @@ -0,0 +1,16 @@ +#nullable enable + + +using System.Collections.Generic; + +namespace Slipstream.Components.WebWidget +{ + public interface IWebWidgetInstances + { + void Add(string id, string type, string? data); + void Remove(string id); + string this[string id] {get;} + ICollection GetIds(); + string? InitData(string id); + } +} \ No newline at end of file diff --git a/Components/WebWidget/InstanceIndexWebModule.cs b/Components/WebWidget/InstanceIndexWebModule.cs new file mode 100644 index 00000000..653a07f2 --- /dev/null +++ b/Components/WebWidget/InstanceIndexWebModule.cs @@ -0,0 +1,87 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO; +using EmbedIO.Routing; +using Serilog; + +namespace Slipstream.Components.WebWidget +{ + public class InstanceIndexWebModule : IWebModule + { + private readonly RouteMatcher RouteMatcher; + private readonly ILogger Logger; + private readonly IWebWidgetInstances Instances; + private readonly string Template; + public bool IsFinalHandler => true; + public ExceptionHandlerCallback? OnUnhandledException { get; set; } + public HttpExceptionHandlerCallback? OnHttpException { get; set; } + public string BaseRoute => "/"; + + public InstanceIndexWebModule(IWebWidgetInstances instances, ILogger logger) + { + Instances = instances; + RouteMatcher = RouteMatcher.Parse(BaseRoute, true); + Logger = logger; + + var assembly = GetType().Assembly; + using var s = assembly.GetManifestResourceStream("Slipstream.Backend.WebWidget.InstanceIndex.html"); + using var sr = new StreamReader(s); + Template = sr.ReadToEnd(); + } + + public RouteMatch? MatchUrlPath(string urlPath) + { + return RouteMatcher.Match(urlPath); + } + + public void Start(CancellationToken cancellationToken) + { + } + + public Task HandleRequestAsync(IHttpContext context) + { + if (context.IsHandled) + return Task.CompletedTask; + + try + { + var content = ""; + + if(Instances.GetIds().Count == 0) + { + content = Template.Replace("{{CONTENT}}", "No instances"); + } + else + { + var instancesBlock = ""; + + foreach (var instanceId in Instances.GetIds()) + { + instancesBlock += $"
  • {instanceId}
  • "; + } + + content = Template.Replace("{{CONTENT}}", $"
      {instancesBlock}
    "); + } + + context.Response.Headers.Add(HttpHeaderNames.CacheControl, "no-cache"); + return context.SendStringAsync(content, MimeType.Html, Encoding.UTF8); + } + catch(KeyNotFoundException _) + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + } + catch(Exception e) + { + Logger.Error(e.Message); + throw; + } + } + } +} diff --git a/Components/WebWidget/InstanceWebModule.cs b/Components/WebWidget/InstanceWebModule.cs new file mode 100644 index 00000000..5d3788b4 --- /dev/null +++ b/Components/WebWidget/InstanceWebModule.cs @@ -0,0 +1,74 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO; +using EmbedIO.Routing; +using Serilog; + +namespace Slipstream.Components.WebWidget +{ + public class InstanceWebModule : IWebModule + { + private readonly RouteMatcher RouteMatcher; + private readonly ILogger Logger; + private readonly IWebWidgetInstances Instances; + private readonly string WebWidgetDirectory; + public bool IsFinalHandler => true; + public ExceptionHandlerCallback? OnUnhandledException { get; set; } + public HttpExceptionHandlerCallback? OnHttpException { get; set; } + public string BaseRoute => "/instances/{id}"; + + public InstanceWebModule(IWebWidgetInstances instances, string webWidgetDirectory, ILogger logger) + { + Instances = instances; + WebWidgetDirectory = webWidgetDirectory; + RouteMatcher = RouteMatcher.Parse(BaseRoute, true); + Logger = logger; + } + + public RouteMatch? MatchUrlPath(string urlPath) + { + return RouteMatcher.Match(urlPath); + } + + public void Start(CancellationToken cancellationToken) + { + } + + public Task HandleRequestAsync(IHttpContext context) + { + try + { + var instanceId = context.Route["id"]; + var webWidgetType = Instances[instanceId]; + + var template = File.ReadAllText(WebWidgetDirectory + webWidgetType + "/index.html"); + var assets = "/webwidgets/" + webWidgetType; + var rendered = template + .Replace("{{ASSETS}}", assets) + .Replace("{{SLIPSTREAM_BODY_ATTRS}}", $" data-instance-id=\"{instanceId}\" data-web-widget-type=\"{webWidgetType}\" data-assets=\"{assets}\"") + .Replace("{{SLIPSTREAM_HEADERS}}", ""); + + context.SetHandled(); + context.Response.Headers.Add(HttpHeaderNames.CacheControl, "no-cache"); + return context.SendStringAsync(rendered, MimeType.Html, Encoding.UTF8); + } + catch(KeyNotFoundException _) + { + context.Response.StatusCode = 404; + context.SetHandled(); + return Task.CompletedTask; + } + catch(Exception e) + { + Logger.Error(e.Message); + throw; + } + } + } +} diff --git a/Components/WebWidget/JavascriptWebModule.cs b/Components/WebWidget/JavascriptWebModule.cs new file mode 100644 index 00000000..c0e8d764 --- /dev/null +++ b/Components/WebWidget/JavascriptWebModule.cs @@ -0,0 +1,48 @@ +#nullable enable + +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO; +using EmbedIO.Routing; + +namespace Slipstream.Components.WebWidget +{ + public class JavascriptWebModule : IWebModule + { + private readonly RouteMatcher RouteMatcher; + private readonly string Content; + public bool IsFinalHandler => true; + public ExceptionHandlerCallback? OnUnhandledException { get; set; } + public HttpExceptionHandlerCallback? OnHttpException { get; set; } + public string BaseRoute => "/ss.js"; + + public JavascriptWebModule() + { + RouteMatcher = RouteMatcher.Parse(BaseRoute, true); + + var assembly = GetType().Assembly; + using var s = assembly.GetManifestResourceStream("Slipstream.Backend.WebWidget.ss.js"); + using var sr = new StreamReader(s); + Content = sr.ReadToEnd(); + } + + public RouteMatch? MatchUrlPath(string urlPath) + { + return RouteMatcher.Match(urlPath); + } + + public void Start(CancellationToken cancellationToken) + { + } + + public Task HandleRequestAsync(IHttpContext context) + { + if (context.IsHandled) + return Task.CompletedTask; + + return context.SendStringAsync(Content, "application/javascript", Encoding.UTF8); + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/Lua/IWebWidgetInstanceThread.cs b/Components/WebWidget/Lua/IWebWidgetInstanceThread.cs new file mode 100644 index 00000000..cc53ff18 --- /dev/null +++ b/Components/WebWidget/Lua/IWebWidgetInstanceThread.cs @@ -0,0 +1,11 @@ +#nullable enable + +using Slipstream.Shared.Lua; +using System; + +namespace Slipstream.Components.WebWidget.Lua +{ + public interface IWebWidgetInstanceThread : ILuaInstanceThread, IDisposable + { + } +} \ No newline at end of file diff --git a/Components/WebWidget/Lua/WebWidgetInstanceThread.cs b/Components/WebWidget/Lua/WebWidgetInstanceThread.cs new file mode 100644 index 00000000..a217d44d --- /dev/null +++ b/Components/WebWidget/Lua/WebWidgetInstanceThread.cs @@ -0,0 +1,36 @@ +#nullable enable + +using Serilog; + +namespace Slipstream.Components.WebWidget.Lua +{ + public class WebWidgetInstanceThread : IWebWidgetInstanceThread + { + private readonly string InstanceId; + private readonly string WebWidgetType; + private readonly IHttpServerApi HttpServer; + private readonly string? Data; + + public WebWidgetInstanceThread(string instanceId, string webWidgetType, string data, IHttpServerApi httpServer) + { + InstanceId = instanceId; + WebWidgetType = webWidgetType; + HttpServer = httpServer; + Data = data; + } + + public void Dispose() + { + } + + public void Start() + { + HttpServer.AddInstance(InstanceId, WebWidgetType, Data); + } + + public void Stop() + { + HttpServer.RemoveInstance(InstanceId); + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/Lua/WebWidgetLuaLibrary.cs b/Components/WebWidget/Lua/WebWidgetLuaLibrary.cs new file mode 100644 index 00000000..9e5a3a37 --- /dev/null +++ b/Components/WebWidget/Lua/WebWidgetLuaLibrary.cs @@ -0,0 +1,53 @@ +#nullable enable + +using Autofac; +using Newtonsoft.Json; +using Slipstream.Shared; +using Slipstream.Shared.Helpers.StrongParameters; +using Slipstream.Shared.Helpers.StrongParameters.Validators; +using Slipstream.Shared.Lua; +using System.Collections.Generic; + +namespace Slipstream.Components.WebWidget.Lua +{ + public class WebWidgetLuaLibrary : BaseLuaLibrary + { + private IHttpServer? HttpServer; + public static DictionaryValidator ConfigurationValidator { get; } + + static WebWidgetLuaLibrary() + { + ConfigurationValidator = new DictionaryValidator() + .RequireString("id") + .PermitDictionary("data", a => a.AllowAnythingElse()) + .RequireString("type"); + } + + public WebWidgetLuaLibrary(ILifetimeScope scope, IEventBus eventBus) : base(ConfigurationValidator, scope, eventBus) + { + } + + protected override IWebWidgetInstanceThread CreateInstance(ILifetimeScope scope, Parameters cfg) + { + var instanceId = cfg.Extract("id"); + var webWidgetType = cfg.Extract("type"); + var data = cfg.ExtractOrDefault>("data", null); + var json = JsonConvert.SerializeObject(data); + + if (HttpServer == null) + { + var subscription = EventBus.RegisterListener(instanceId); + HttpServer = LifetimeScope.Resolve( + new TypedParameter(typeof(IEventBusSubscription), subscription) + ); + } + + return scope.Resolve( + new NamedParameter("instanceId", instanceId), + new NamedParameter("webWidgetType", webWidgetType), + new NamedParameter("data", json), + new NamedParameter("httpServer", HttpServer) + ); + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/Lua/WebWidgetLuaReference.cs b/Components/WebWidget/Lua/WebWidgetLuaReference.cs new file mode 100644 index 00000000..b31a7859 --- /dev/null +++ b/Components/WebWidget/Lua/WebWidgetLuaReference.cs @@ -0,0 +1,29 @@ +#nullable enable + +using NLua; +using Slipstream.Shared; +using Slipstream.Shared.Lua; +using Slipstream.Shared.Helpers.StrongParameters; +using Newtonsoft.Json; + +namespace Slipstream.Components.WebWidget.Lua +{ + public class WebWidgetLuaReference : BaseLuaReference + { + private readonly IWebWidgetEventFactory EventFactory; + private readonly IEventBus EventBus; + + public WebWidgetLuaReference(string instanceId, string luaScriptInstanceId, IWebWidgetEventFactory eventFactory, IEventBus eventBus) : base(instanceId, luaScriptInstanceId) + { + EventFactory = eventFactory; + EventBus = eventBus; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "This is expose in Lua, so we want to keep that naming style")] + public void send(LuaTable data) + { + var json = JsonConvert.SerializeObject(Parameters.From(data)); + EventBus.PublishEvent(EventFactory.CreateWebWidgetCommandEvent(Envelope, json)); + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/README.md b/Components/WebWidget/README.md new file mode 100644 index 00000000..6bab732e --- /dev/null +++ b/Components/WebWidget/README.md @@ -0,0 +1,95 @@ +# WebWidget + +Enables you to create http endpoints hosted by Slipstream. These endpoints called WebWidgets +instances can serve html and optionally assets. You can send updates to them, if this is needed. + +This can be used with OBS as a browswer source, allowing Slipstream to visually +display data on stream. + +## Lua + +
    Construction
    + +```lua +local webwidget = require("api/webwidget"):instance(config) +``` + +This will return a WebWidget instance and create it if it does not exists. + +`config` is the initial configuration of the instance if one needs to be created. +It is a table with one or more keys as defined below. + +| Parameter | Type | Default | Description | +| :---------- | :-----------: | :--------: | :----------------------------- | +| id | string | | Mandatory: Id of this instance | +| type | string | | Mandatory: WebWidget type | +| data | lua table- | nil | Optional: Initial data | + +When creating an instance, Slipstream will start up an embedded HTTP server, serving +that endpoint. URL is shown in the console, but will be http://127.0.0.1:1919/instances/, +where is replaced with the id provided to the instance. The endpoint is removed +when no longer referenced from Lua. + +In Slipstream data directory, you'll find a WebWidgets directory. The content of this directory +is served via http://127.0.0.1:1919/webwidgets/ allowing you to have you javascript, +css, images and other assets located here. + +The WebWidget directory, will also require an index.html exists in a subdirectory with the same +name as provided as the WebWidgetType. + +For example: +```lua +local webwidget = require("api/webwidget"):instance({ id = "mywidget", type = "test", data = "Hello") +``` + +Will create the endpoint http://127.0.0.1:1919/instances/wmywidget. If you go to this URL with +you browser, it will serve `WebWidget\test\index.html`. If this file does not exist, Slipstream +will not create the endpoint, but show a message in the console saying it is ignoring it. + +The `index.html` file should be based on the following template +```html + + + + {{SLIPSTREAM_HEADERS}} + + + + + + +``` + +For most part, this is regular HTML, except there are a few special strings: + +| Name | Required | Description | +| :-------- | :-----------:| :----------------------------- | +| {{SLIPSTREAM_HEADERS}} | Yes | Needs to be in the -tags for your HTML document. This will include javascript needed for the WebWidget to work | +| {{SLIPSTREAM_BODY_ATTRS}} | Yes | Needs to be a part of your -tag. e.g: `` | +| {{ASSETS}} | No | Will be replaced with the path to the WebWidgets assets directory (a subdirectory under `WebWidget` in your Slipstream data Directory) | + +To get access to the data provided, you need to define a javascript function named `onData`, +taking one argument, `data`, which is whatever data sent to the WebWidget. Additional two other javascript functions can +be implemented. `onConnect` (takes no arguments) and `onDisconnect()`, these are triggered when connecting/disconnecting to +Slipstream backend. It's optinal to implement these, but can be used to implement a "ghosted" view of the widget, +not connected to Slipstream. + +There are also three global variables available: `ASSETS` (for the assets path), `INSTANCE_ID` and finally `WEB_WIDGET_TYPE` + +See `widget:send(data)` for how to send data to your widget. +
    + +
    widget:send(data)
    +Delivers data to the WebWidget. Data itself is unparsed so it can be formatted in whatever +way that is appropiate. The callback in the `index.html` will receive it as-is. So you can +provide a string, json, numbers and so on. You will need to make the callback parse it, and +handle it as needed. + +```lua +local webwidget = require("api/webwidget"):instance(config) +webwidget:send("green") +``` diff --git a/Components/WebWidget/WebSocketsEventsServer.cs b/Components/WebWidget/WebSocketsEventsServer.cs new file mode 100644 index 00000000..4cf630c3 --- /dev/null +++ b/Components/WebWidget/WebSocketsEventsServer.cs @@ -0,0 +1,77 @@ +#nullable enable + +using System.Collections.Generic; +using System.Threading.Tasks; +using EmbedIO.WebSockets; +using Serilog; + +namespace Slipstream.Components.WebWidget +{ + public class WebSocketsEventsServer : WebSocketModule + { + private readonly ILogger Logger; + private readonly IWebWidgetInstances Instances; + private readonly Dictionary> InstanceToContextIdMap = new Dictionary>(); + + public WebSocketsEventsServer(ILogger logger, IWebWidgetInstances instances) : base("/events/{id}", true) + { + Logger = logger; + Instances = instances; + } + + protected override Task OnMessageReceivedAsync(IWebSocketContext context, byte[] rxBuffer, IWebSocketReceiveResult rxResult) + => Task.CompletedTask; + + protected override Task OnClientConnectedAsync(IWebSocketContext context) + { + string instanceId = ParseInstanceId(context); + Logger.Information($"HttpServer - connected {context} - ctxid={context.Id}, instanceID={instanceId}"); + + lock (InstanceToContextIdMap) + { + if (!InstanceToContextIdMap.ContainsKey(instanceId)) + InstanceToContextIdMap.Add(instanceId, new List()); + + InstanceToContextIdMap[instanceId].Add(context.Id); + } + + var initData = Instances.InitData(instanceId); + + // if we encoded null, it will be "null" - no need to send that + if (initData != null && initData != "null") + Broadcast(instanceId, initData); + + return base.OnClientConnectedAsync(context); + } + + private static string ParseInstanceId(IWebSocketContext context) + { + return context.RequestUri.LocalPath.Substring("/events/".Length); + } + + protected override Task OnClientDisconnectedAsync(IWebSocketContext context) + { + string instanceId = ParseInstanceId(context); + Logger.Information($"HttpServer - disconnected {context.RequestUri.LocalPath} - ctxid={context.Id}, instanceId={instanceId}"); + + lock (InstanceToContextIdMap) + { + if (InstanceToContextIdMap.ContainsKey(instanceId)) + InstanceToContextIdMap[instanceId].Remove(context.Id); + } + + return base.OnClientDisconnectedAsync(context); + } + + public void Broadcast(string instanceId, string data) + { + BroadcastAsync(data, s => + { + List ctxIds; + InstanceToContextIdMap.TryGetValue(instanceId, out ctxIds); + + return ctxIds != null && ctxIds.Contains(s.Id); + }); + } + } +} \ No newline at end of file diff --git a/Components/WebWidget/WebWidgetInstances.cs b/Components/WebWidget/WebWidgetInstances.cs new file mode 100644 index 00000000..811ac3e9 --- /dev/null +++ b/Components/WebWidget/WebWidgetInstances.cs @@ -0,0 +1,54 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Slipstream.Components.WebWidget +{ + public class WebWidgetInstances : IWebWidgetInstances + { + private readonly IDictionary InstancesType = new Dictionary(); + private readonly IDictionary InstanceInitData = new Dictionary(); + + public void Add(string id, string type, string? data) + { + lock(InstancesType) + { + InstancesType.Add(id, type); + InstanceInitData.Add(id, data); + } + + } + + public void Remove(string id) + { + lock (InstancesType) + { + InstancesType.Remove(id); + InstanceInitData.Remove(id); + } + } + + public string this[string id] + { + get + { + return InstancesType[id]; + } + } + + public string? InitData(string id) + { + lock(InstanceInitData) + if(InstanceInitData.ContainsKey(id)) + return InstanceInitData[id]; + + return null; + } + + public ICollection GetIds() + { + lock(InstancesType) + return InstancesType.Keys; + } + } +} \ No newline at end of file diff --git a/Components/WinFormUI/Services/EventDataService.cs b/Components/WinFormUI/Services/EventDataService.cs index 91ef3dce..c5c39d16 100644 --- a/Components/WinFormUI/Services/EventDataService.cs +++ b/Components/WinFormUI/Services/EventDataService.cs @@ -40,7 +40,7 @@ private static EventInfoModel BuildEventInfo(Type t) private static string GetDescriptionFromAttribute(PropertyInfo p) { return Attribute.IsDefined(p, typeof(DescriptionAttribute)) ? - (Attribute.GetCustomAttribute(p, typeof(DescriptionAttribute)) as DescriptionAttribute).Description : null; + (Attribute.GetCustomAttribute(p, typeof(DescriptionAttribute)) as DescriptionAttribute).Description : ""; } private static IList BuildEventProperties(Type t) diff --git a/LuaScripts/debug.lua b/LuaScripts/debug.lua deleted file mode 100644 index 37fc186e..00000000 --- a/LuaScripts/debug.lua +++ /dev/null @@ -1,19 +0,0 @@ ---[[ - -This script will display all events seen by it to the console. Only for development / debugging purposes. - -Before using this, edit lib/config.lua - ---]] - -local cfg = require("lib/config") -local util = require("api/util"):instance(cfg.util) -local ui = require("api/winformui"):instance(cfg.winformui) - -function handle(event) - if event.EventType ~= "WinFormUICommandWriteToConsole" then - ui:print(util:event_to_json(event)) - end -end - -print "debug.lua loaded" \ No newline at end of file diff --git a/Program.cs b/Program.cs index 7f0df0d5..03e23dca 100644 --- a/Program.cs +++ b/Program.cs @@ -105,6 +105,8 @@ private static void ConfigureServices(ContainerBuilder builder) builder.RegisterType().As().InstancePerDependency(); builder.RegisterType().As().InstancePerDependency(); builder.RegisterType().As().InstancePerDependency(); + builder.RegisterType().As().InstancePerDependency(); + builder.RegisterType().As().As().SingleInstance(); } private class PopulateSink diff --git a/LuaScripts/lib/common.lua b/Samples/Common/Scripts/common.lua similarity index 100% rename from LuaScripts/lib/common.lua rename to Samples/Common/Scripts/common.lua diff --git a/LuaScripts/lib/config.lua b/Samples/Common/Scripts/config.lua similarity index 100% rename from LuaScripts/lib/config.lua rename to Samples/Common/Scripts/config.lua diff --git a/Samples/IRacing - Audio - Prechecks/README.md b/Samples/IRacing - Audio - Prechecks/README.md new file mode 100644 index 00000000..048baf44 --- /dev/null +++ b/Samples/IRacing - Audio - Prechecks/README.md @@ -0,0 +1,6 @@ +# IRacing/Audio - Prechecks + +Will remind player to check fuel, setup, etc at qualify/race start. + +Before using this, edit lib/config.lua. You'll need `config.lua` and +`common.lua`. Both files can be found in `Samples/Common` folder. \ No newline at end of file diff --git a/LuaScripts/iracing_prechecks.lua b/Samples/IRacing - Audio - Prechecks/Scripts/iracing_prechecks.lua similarity index 100% rename from LuaScripts/iracing_prechecks.lua rename to Samples/IRacing - Audio - Prechecks/Scripts/iracing_prechecks.lua diff --git a/Samples/IRacing - Twitch - Car Command/README.md b/Samples/IRacing - Twitch - Car Command/README.md new file mode 100644 index 00000000..27b74c33 --- /dev/null +++ b/Samples/IRacing - Twitch - Car Command/README.md @@ -0,0 +1,7 @@ +# IRacing/Twitch !car command + +Implements a "!car" command for twitch users to see the current car driven. +This script assumes you + +Before using this, edit lib/config.lua. You'll need `config.lua` and +`common.lua`. Both files can be found in `Samples/Common` folder. \ No newline at end of file diff --git a/LuaScripts/iracing_car.lua b/Samples/IRacing - Twitch - Car Command/Scripts/iracing_car.lua similarity index 91% rename from LuaScripts/iracing_car.lua rename to Samples/IRacing - Twitch - Car Command/Scripts/iracing_car.lua index ba33774c..266395c0 100644 --- a/LuaScripts/iracing_car.lua +++ b/Samples/IRacing - Twitch - Car Command/Scripts/iracing_car.lua @@ -1,6 +1,3 @@ --- Implements a "!car" command for twitch users to see the current car driven --- Before using this, edit lib/config.lua - local cfg = require("lib/config") local common = require("lib/common") local iracing = require("api/iracing"):instance(cfg.iracing) diff --git a/Samples/IRacing - Twitch - IR Command/README.md b/Samples/IRacing - Twitch - IR Command/README.md new file mode 100644 index 00000000..227e274d --- /dev/null +++ b/Samples/IRacing - Twitch - IR Command/README.md @@ -0,0 +1,9 @@ +# IRacing/Twitch !ir command + +Provides "!ir" command, that will shown the latest seen IRating/License info + +- "!ir" - shows IRating/License for current category of the session (road, dirt, dirtoval, dirtroad) +- "!ir " - show of another category than current + +Before using this, edit lib/config.lua. You'll need `config.lua` and +`common.lua`. Both files can be found in `Samples/Common` folder. \ No newline at end of file diff --git a/LuaScripts/iracing_ir.lua b/Samples/IRacing - Twitch - IR Command/Scripts/iracing_ir.lua similarity index 88% rename from LuaScripts/iracing_ir.lua rename to Samples/IRacing - Twitch - IR Command/Scripts/iracing_ir.lua index 6ecf6da7..11236b9c 100644 --- a/LuaScripts/iracing_ir.lua +++ b/Samples/IRacing - Twitch - IR Command/Scripts/iracing_ir.lua @@ -1,13 +1,3 @@ ---[[ - - Provides "!ir" command, that will shown the latest seen IRating/License info - - - "!ir" - shows IRating/License for current category of the session (road, dirt, dirtoval, dirtroad) - - "!ir " - show of another category than current - - Before using this, edit lib/config.lua ---]] - local cfg = require("lib/config") local iracing = require("api/iracing"):instance(cfg.iracing) local twitch = require("api/twitch"):instance(cfg.twitch) diff --git a/Samples/IRacing - Twitch - SoF Command/README.md b/Samples/IRacing - Twitch - SoF Command/README.md new file mode 100644 index 00000000..12a9e5f9 --- /dev/null +++ b/Samples/IRacing - Twitch - SoF Command/README.md @@ -0,0 +1,6 @@ +# IRacing/Twitch !sof command + +Implements a "!sof" command for twitch users to see the current strength-of-field + +Before using this, edit lib/config.lua. You'll need `config.lua` and +`common.lua`. Both files can be found in `Samples/Common` folder. \ No newline at end of file diff --git a/LuaScripts/iracing_sof.lua b/Samples/IRacing - Twitch - SoF Command/Scripts/iracing_sof.lua similarity index 90% rename from LuaScripts/iracing_sof.lua rename to Samples/IRacing - Twitch - SoF Command/Scripts/iracing_sof.lua index 67984b21..3886e794 100644 --- a/LuaScripts/iracing_sof.lua +++ b/Samples/IRacing - Twitch - SoF Command/Scripts/iracing_sof.lua @@ -1,6 +1,3 @@ --- Implements a "!sof" command for twitch users to see the current strength-of-field --- Before using this, edit lib/config.lua - local cfg = require("lib/config") local common = require("lib/common") local iracing = require("api/iracing"):instance(cfg.iracing) diff --git a/Samples/IRacing - Twitch - Track Command/README.md b/Samples/IRacing - Twitch - Track Command/README.md new file mode 100644 index 00000000..5037c8d8 --- /dev/null +++ b/Samples/IRacing - Twitch - Track Command/README.md @@ -0,0 +1,6 @@ +# IRacing/Twitch !track command + +Implements a "!track" command for twitch users to see the current track. + +Before using this, edit lib/config.lua. You'll need `config.lua` and +`common.lua`. Both files can be found in `Samples/Common` folder. diff --git a/LuaScripts/iracing_track.lua b/Samples/IRacing - Twitch - Track Command/Scripts/iracing_track.lua similarity index 91% rename from LuaScripts/iracing_track.lua rename to Samples/IRacing - Twitch - Track Command/Scripts/iracing_track.lua index efe61894..b9d23eee 100644 --- a/LuaScripts/iracing_track.lua +++ b/Samples/IRacing - Twitch - Track Command/Scripts/iracing_track.lua @@ -1,6 +1,3 @@ --- Adds "!track" twitch command that displays current track --- Before using this, edit lib/config.lua - local cfg = require("lib/config") local common = require("lib/common") local iracing = require("api/iracing"):instance(cfg.iracing) diff --git a/Samples/IRacing - WebWidget - ChatOn Command/README.md b/Samples/IRacing - WebWidget - ChatOn Command/README.md new file mode 100644 index 00000000..8f20c4b7 --- /dev/null +++ b/Samples/IRacing - WebWidget - ChatOn Command/README.md @@ -0,0 +1,7 @@ +# IRacing/Twitch/WebWidget ChatOn command + +Implements !chaton and !chatoff commands, that will show/hide a focused-message (on screen) +and in chat. + +In OBS use http://127.0.0.1:1919/instances/focused-mode as browser-source for seeing the +webwidget. diff --git a/Samples/IRacing - WebWidget - ChatOn Command/Scripts/chatstate.lua b/Samples/IRacing - WebWidget - ChatOn Command/Scripts/chatstate.lua new file mode 100644 index 00000000..898ffee7 --- /dev/null +++ b/Samples/IRacing - WebWidget - ChatOn Command/Scripts/chatstate.lua @@ -0,0 +1,25 @@ +local cfg = require("lib/config") + +local widget = require("api/webwidget"):instance({ id = "focused-mode", type = "textbox"}) +local twitch = require("api/twitch"):instance(cfg.twitch) + +local focused_msg = "Focused time! Chat will not monitored to keep focus." +local relaxed_msg = "No more focusing, let's chat!'" + +function handle(event) + if event.EventType == "TwitchReceivedMessage" then + if event.Message == "!chatoff" or event.Message == "!chaton" then + if event.Moderator or event.Vip or event.Broadcaster then + if event.Message == "!chatoff" then + widget:send({ text = focused_msg}) + twitch:send_channel_message(focused_msg) + else + widget:send({ text = ""}) + twitch:send_channel_message(relaxed_msg) + end + else + twitch:send_channel_message(event.From .. " You are not allowed to use !chaton and !chatoff") + end + end + end +end diff --git a/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/controller.js b/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/controller.js new file mode 100644 index 00000000..ad7e1895 --- /dev/null +++ b/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/controller.js @@ -0,0 +1,63 @@ +let wrapper; + +if (!String.prototype.replaceAll) { + String.prototype.replaceAll = function (search, replace) { + return this.split(search).join(replace); + } +} + +function onConnect() { + document.body.classList.remove("offline") +} + +function onDisconnect() { + document.body.classList.add("offline") +} + +function onData(data) { + if (wrapper === undefined) { + wrapper = document.getElementsByClassName("wrapper")[0] + } + + let visible = wrapper.classList.contains("show") + + if (data.mode) { + if (!visible) { + wrapper.classList.remove("transition") + } + + wrapper.classList.remove("from-right") + wrapper.classList.remove("from-left") + wrapper.classList.remove("from-top") + wrapper.classList.remove("from-bottom") + + if (!wrapper.classList.contains("from-" + data.mode)) { + wrapper.classList.add("from-" + data.mode) + } + data = "" + } + else { + if (!visible) { + wrapper.classList.add("transition") + } + } + + let html = undefined; + + if (data.text) { + html = "

    " + data.text.trim() + "

    " + } + if (data.html) { + // We need to split the "needle" up as to strings, to avoid having it replaced with + // the assets path + html = data.html.replace("{{ASSETS}}", ASSETS) + } + + if (html === undefined || html === "") { + wrapper.classList.remove("show") + } + else { + wrapper.children[0].innerHTML = html + wrapper.classList.add("show") + } +} \ No newline at end of file diff --git a/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/index.html b/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/index.html new file mode 100644 index 00000000..3eaa43f2 --- /dev/null +++ b/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/index.html @@ -0,0 +1,15 @@ + + + + {{SLIPSTREAM_HEADERS}} + + + + + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/style.css b/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/style.css new file mode 100644 index 00000000..3230dc71 --- /dev/null +++ b/Samples/IRacing - WebWidget - ChatOn Command/WebWidgets/Textbox/style.css @@ -0,0 +1,64 @@ +html, body { + font-family: 'Open Sans', sans-serif; +} + +.offline { + background-color: #000a; +} + +.online { +} + +.wrapper { + width: 100%; + z-index: 10; + position: absolute; + overflow: hidden; +} + + .wrapper.from-left { + left: 0; + } + +wrapper.from-right { + right: 0; +} + +wrapper.from-top { + top: 0; +} + +wrapper.from-bottom { + bottom: 0; +} + +.wrapper .content { + padding: 5px 20px; + background-color: black; + color: white; + border-left: 1rem solid blue; +} + +.wrapper.transition .content { + transition: transform .5s ease; +} + +.wrapper.from-left .content { + transform: translateX(-100%); +} + +.wrapper.from-right .content { + transform: translateX(100%); +} + +.wrapper.from-top .content { + transform: translateY(-100%); +} + +.wrapper.from-bottom .content { + transform: translateY(100%); +} + +.wrapper.show .content { + transform: translateX(0); +} diff --git a/Shared/Lua/BaseInstanceThread.cs b/Shared/Lua/BaseInstanceThread.cs index 798c9c4d..0bf8aa17 100644 --- a/Shared/Lua/BaseInstanceThread.cs +++ b/Shared/Lua/BaseInstanceThread.cs @@ -104,7 +104,7 @@ public void Join() ServiceThread.Join(); } - public void Dispose() + public virtual void Dispose() { Stopping = true; if (ServiceThread?.IsAlive == true) diff --git a/Shared/Lua/BaseLuaLibrary.cs b/Shared/Lua/BaseLuaLibrary.cs index 20affa7b..4e893e6d 100644 --- a/Shared/Lua/BaseLuaLibrary.cs +++ b/Shared/Lua/BaseLuaLibrary.cs @@ -64,7 +64,7 @@ public void Dispose() protected abstract TInstance CreateInstance(ILifetimeScope scope, Parameters cfg); - protected void HandleInstance(string instanceId, Parameters cfg) + protected virtual void HandleInstance(string instanceId, Parameters cfg) { if (!Instances.ContainsKey(instanceId)) { diff --git a/Shared/Lua/SingletonLuaLibrary.cs b/Shared/Lua/SingletonLuaLibrary.cs index 86ef6f2a..91d6271b 100644 --- a/Shared/Lua/SingletonLuaLibrary.cs +++ b/Shared/Lua/SingletonLuaLibrary.cs @@ -1,11 +1,15 @@ #nullable enable using Autofac; +using NLua; using Slipstream.Shared.Helpers.StrongParameters; using Slipstream.Shared.Helpers.StrongParameters.Validators; +using System.Diagnostics; namespace Slipstream.Shared.Lua { + // This will at most create one instance with id "singleton". Can be used for eg. IRacing, where more than one instance + // doesn't make sense. public abstract class SingletonLuaLibrary : BaseLuaLibrary where TInstance : ILuaInstanceThread where TReference : ILuaReference diff --git a/Slipstream.csproj b/Slipstream.csproj index 0835cdcb..a94f8d27 100644 --- a/Slipstream.csproj +++ b/Slipstream.csproj @@ -64,6 +64,9 @@ packages\DSharpPlus.4.0.1\lib\netstandard2.0\DSharpPlus.dll + + packages\EmbedIO.3.4.3\lib\netstandard2.0\EmbedIO.dll + packages\Emzi0767.Common.2.6.2\lib\net45\Emzi0767.Common.dll @@ -125,6 +128,9 @@ packages\Serilog.Sinks.Console.4.0.0\lib\net45\Serilog.Sinks.Console.dll + + packages\Unosquare.Swan.Lite.3.0.0\lib\net461\Swan.Lite.dll + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll @@ -267,6 +273,23 @@ + + + + + + + + + + + + + + + + + @@ -508,18 +531,22 @@ - + - - - - - - - + + + + + + + + + + + - + SettingsSingleFileGenerator @@ -530,6 +557,10 @@ Settings.settings True + + + + @@ -548,6 +579,11 @@ + + + + + diff --git a/packages.config b/packages.config index a4fb43fa..a6e399ea 100644 --- a/packages.config +++ b/packages.config @@ -3,6 +3,7 @@ + @@ -45,4 +46,5 @@ + \ No newline at end of file