using Godot; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; public partial class CommandHandler : Node { private Settings Settings { get; set; } private game Game { get; set; } private readonly ConcurrentQueue ActionQueue = new(); private System.Net.Http.HttpClient _Client = null; private System.Net.Http.HttpClient Client { get { _Client ??= new(); return _Client; } } private defer_manager _Deferer = null; private defer_manager Deferer { get { _Deferer ??= GetNode("/root/DeferManager"); return _Deferer; } } // Called when the node enters the scene tree for the first time. public override void _Ready() { //force initialization of Deferer in main thread _ = Deferer; Settings = GetNode("/root/Settings"); Game = GetNode("/root/Game"); GetNode("/root/TwitchChatWatcher").IncomingCommand += IncomingCommand; } // Called every frame. 'delta' is the elapsed time since the previous frame. public override void _Process(double delta) { while (ActionQueue.TryDequeue(out Action t)) { t?.Invoke(); } } private void IncomingCommand(Command command) { GD.Print(command.GetArgs().Remaining()); if (!Settings.IsUserAuthorized(command.User, command.IsStreamer, command.IsModerator)) return; var baseArgs = command.GetArgs(); var type = CommandTypeHelper.ParseCommand(baseArgs.Pop()); var args = baseArgs.DuplicateAtState(); switch (type) { case CommandType.MoveCard: MoveCard(args); break; case CommandType.MoveRow: MoveRow(args); break; case CommandType.DeleteCard: DeleteCards(args); break; case CommandType.DeleteRow: DeleteRow(args); break; case CommandType.CreateCard: Task.Run(async () => await CreateCard(args)) .ContinueWith(t => GD.PrintErr(t.Exception.StackTrace), TaskContinuationOptions.OnlyOnFaulted); break; case CommandType.CreateRow: CreateRow(args); break; case CommandType.RenameCard: RenameCard(args); break; case CommandType.RenameRow: RenameRow(args); break; case CommandType.RecolorRow: RecolorRow(args); break; case CommandType.ChangeCardImage: Task.Run(async () => await ChangeCardImage(args)) .ContinueWith(t => GD.PushError(t.Exception.InnerException.StackTrace), TaskContinuationOptions.OnlyOnFaulted); break; default: throw new Exception("invalid command type"); } } private void MoveCard(CommandArguments args) { var cardId = args.Pop(); var rowId = args.Pop(); var indexStr = args.Pop(); if (int.TryParse(indexStr, out int index)) Game.MoveCard(cardId, rowId, index); else Game.MoveCard(cardId, rowId, null); } private void MoveRow(CommandArguments args) { var rowId = args.Pop(); var newIndexStr = args.Pop(); var newIndex = int.Parse(newIndexStr); Game.MoveRow(rowId, newIndex); } private void DeleteCards(CommandArguments args) { var ids = args.Enumerate().ToArray(); Game.DeleteCards(ids); } private void DeleteRow(CommandArguments args) { var rowId = args.Pop(); var deleteCards = args.Pop(); if (bool.TryParse(deleteCards, out bool del)) Game.DeleteRow(rowId, del); else Game.DeleteRow(rowId); } private async Task CreateCard(CommandArguments args) { var url = args.Pop(); var title = args.Remaining(); ImageWithMetadata img; if (!string.IsNullOrWhiteSpace(url) && url != "_") img = await ImageFromUrl(url); else img = null; await Deferer.DeferAsync(() => Game.CreateCard(title, img)); } private void CreateRow(CommandArguments args) { var colorStr = args.Pop(); var title = args.Remaining(); var color = Color.FromString(colorStr, new Color(0, 0, 0, 0)); if (color == new Color(0, 0, 0, 0)) Game.CreateRow(null, title); else Game.CreateRow(color, title); } private void RenameCard(CommandArguments args) { var cardId = args.Pop(); var title = args.Remaining(); Game.RenameCard(cardId, title); } private void RenameRow(CommandArguments args) { var rowId = args.Pop(); var title = args.Remaining(); Game.RenameRow(rowId, title); } private void RecolorRow(CommandArguments args) { var rowId = args.Pop(); var colorStr = args.Pop(); var color = Color.FromString(colorStr, new Color(0, 0, 0, 0)); if (color.IsEqualApprox(new Color(0, 0, 0, 0))) throw new Exception($"invalid color {colorStr}"); Game.RecolorRow(rowId, color); } private async Task ChangeCardImage(CommandArguments args) { var cardId = args.Pop(); var img = await ImageFromUrl(args.Pop()); await Deferer.DeferAsync(() => Game.ChangeCardImage(cardId, img)); // Game.ChangeCardImage(cardId, img); } private async Task ImageFromUrl(string url) { StretchMode mode = StretchMode.Unspecified; if (url.Contains('|')) { var spl = url.Split('|', 2); url = spl[0]; mode = spl[1].ToLower() switch { "unspecified" => StretchMode.Unspecified, "fit" => StretchMode.Fit, "stretch" => StretchMode.Stretch, "crop" => StretchMode.Crop, _ => throw new Exception($"Unrecognized {nameof(StretchMode)}"), }; } var uri = new Uri(url); GD.Print("Starting image download"); var resp = await Client.GetAsync(uri); if (!resp.IsSuccessStatusCode) throw new Exception("Failed to download image"); GD.Print("Downloaded image successfully"); Image img = new(); var arr = await resp.Content.ReadAsByteArrayAsync(); var ext = Path.GetExtension(uri.AbsolutePath).TrimStart('.').ToLower(); GD.Print($"Image extension: {ext}"); //TODO detect images by header rather than extension switch (ext) { case "png": img.LoadPngFromBuffer(arr); break; case "jpg": case "jpeg": img.LoadJpgFromBuffer(arr); break; case "svg": img.LoadSvgFromBuffer(arr); break; case "webp": img.LoadWebpFromBuffer(arr); break; default: throw new Exception("unrecognized filetype"); } GD.Print($"Loaded picture {img}"); return new ImageWithMetadata(img, mode); } protected override void Dispose(bool disposing) { if (disposing) { _Client?.Dispose(); } base.Dispose(disposing); } }