diff --git a/WFBot.Koharu/Base.cs b/WFBot.Koharu/Base.cs new file mode 100644 index 0000000..7f39494 --- /dev/null +++ b/WFBot.Koharu/Base.cs @@ -0,0 +1,593 @@ +using SkiaSharp; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Drawing; +using System.Reflection; +using System.Runtime.CompilerServices; +using Topten.RichTextKit; + +namespace WFBot.Koharu; + +public class Painters +{ + public static T Create() + { + return Activator.CreateInstance(); + } +} + +public abstract class Painter : IDisposable +{ + public abstract IDrawingCommand Draw(T data); + public static Color BaseBackgroundColor = Color.FromArgb(42, 43, 48); // https://danbooru.donmai.us/posts/2931102 + protected virtual double? BottomInfoTagSize => null; + public static TextOptions textOptions => CreateTextOptions(); + + static TextOptions CreateTextOptions() + { + return new TextOptions(40, Color.White, false, -1); + } + + protected static VerticalLayoutBuilder VerticalLayout(Alignment defaultAlignment = Alignment.TopOrLeft, int? minHeight = null) + { + var layout = new VerticalLayoutBuilder(); + layout.Options = textOptions; + layout.DefaultAlignment(defaultAlignment); + if (minHeight != null) + { + layout.MarginY(minHeight.Value); + } + + return layout; + } + + protected static HorizontalLayoutBuilder HorizontalLayout(Alignment defaultAlignment = Alignment.TopOrLeft, int? minWidth = null) + { + var layout = new HorizontalLayoutBuilder(); + layout.Options = textOptions; + layout.DefaultAlignment(defaultAlignment); + if (minWidth != null) + { + layout.MarginX(minWidth.Value); + } + return layout; + } + + protected static IDrawingCommand PlaceLeftAndRight(IDrawingCommand left, IDrawingCommand right, int? minWidth = null, bool forceWidth = false) + { + var layout = new HorizontalDockLayoutBuilder(forceWidth ? + minWidth : minWidth + right.Size.Width, forceWidth); + layout.Options = textOptions; + + return layout.Draw(left).Alignment(Alignment.TopOrLeft).Draw(right).Alignment(Alignment.DownOrRight).Build(); + } + + protected static IDrawingCommand SimpleImageRendering(string text, int maxWidth = 1000) + { + return new MarginCommand(new TextCommand(text, textOptions with{ MaxWidth = maxWidth}), 30,30,30,30); + } + + protected static IDrawingCommand Text(string text, TextOptions? options = null) + { + return new TextCommand(text, options ?? textOptions); + } + + protected static Color SwitchLineColor(ref bool lcb) + { + lcb = !lcb; + return lcb ? Color.FromArgb(23, 30, 33) : Color.FromArgb(16, 22, 25); + } + + static ConcurrentDictionary Cache = new(); + + public static SKBitmap GetResource(string path) + { + if (Cache.TryGetValue(path, out var res)) return res; + + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("WFBot.Koharu.Resources." + path + ".png"); + if (stream == null) + { + Console.WriteLine($"错误: {path} 为 null"); + //todo + /*var image = new Image(50, 50, new Rgba32(0, 0, 0)); + image.Mutate(x => x.Fill(new Color(new Rgba32(206, 64, 202)), new RectangleF(0, 0, 25, 25))); + image.Mutate(x => x.Fill(new Color(new Rgba32(206, 64, 202)), new RectangleF(25, 25, 25, 25))); + */ + Cache[path] = SKBitmap.FromImage(SKImage.Create(new SKImageInfo(1, 1))); + return SKBitmap.FromImage(SKImage.Create(new SKImageInfo(1,1))); + } + Cache[path] = SKBitmap.Decode(stream); + return Cache[path]; + } + + public static SKBitmap GetResourceWithSize(string path, int width, int height) + { + var cacheKey = $"{path}{width}x{height}"; + if (Cache.TryGetValue(cacheKey, out var res)) return res; + + var bitmap = GetResource(path).Resize(width, height); + Cache[cacheKey] = bitmap; + return bitmap; + } + + protected List DisposeObjects = new List(); + public virtual void Dispose() + { + foreach (var bitmap in DisposeObjects) + { + bitmap.Dispose(); + } + } +} + +public static class WFBotExtensions +{ + static Color BaseBackgroundColor = Color.FromArgb(42, 43, 48); // https://danbooru.donmai.us/posts/2931102 + public static IDrawingCommand ApplyWFBotInfoTag(this IDrawingCommand command, string wfbotCommand, bool isNotification = false, double? BottomInfoTagSize = null) + { + var v = command; + var size = BottomInfoTagSize ?? 60.0 / 0.78; + var text = "> WFBot_ "; // 好兄弟 虽然你可以改 但是不建议你改 至少保留一下原文吧 + TextCommand wfbotTextCommand; + + do + { + size *= 0.78; + wfbotTextCommand = new TextCommand(text, Painter.textOptions with { Size = (int)size }); + } while (wfbotTextCommand.Size.Width / (double)v.Size.Width > 0.42 && BottomInfoTagSize == null && size > 2); + + var bottomBarColor = isNotification ? Color.FromArgb(244, 67, 54) : Color.FromArgb(3, 169, 244); + var commandInfoText = new TextCommand(" / " + wfbotCommand, Painter.textOptions with{ Size = (int)size }); + + return new VerticalLayoutBuilder() + .Draw(command.ApplyBackground(BaseBackgroundColor)) + .Draw(new HorizontalDockLayoutBuilder(v.Size.Width, true) + .Draw(commandInfoText).Alignment(Alignment.TopOrLeft) + .Draw(wfbotTextCommand).Alignment(Alignment.DownOrRight) + .Build().ApplyBackground(bottomBarColor) + ).Build(); + } +} + + +public sealed class HorizontalLayoutBuilder : LayoutBuilder +{ + protected override ComplexDrawingCommand Layout { get; } = new HorizontalLayout(); +} + +public sealed class VerticalLayoutBuilder : LayoutBuilder +{ + protected override ComplexDrawingCommand Layout { get; } = new VerticalLayout(); +} + +public sealed class HorizontalDockLayoutBuilder : LayoutBuilder +{ + int _minWidth; + bool _forceWidth; + + public HorizontalDockLayoutBuilder(int? minWidth = null, bool forceWidth = false) + { + _minWidth = minWidth ?? -1; + _forceWidth = forceWidth; + Layout = new HorizontalDockLayout(_minWidth, _forceWidth); + } + + protected override ComplexDrawingCommand Layout { get; } +} + + + + +public abstract class LayoutBuilder +{ + protected IDrawingCommand? lastCommand = null; + protected abstract ComplexDrawingCommand Layout { get; } + public TextOptions Options { get; set; } + Alignment defaultAlignment = Koharu.Alignment.TopOrLeft; + public LayoutBuilder DefaultAlignment(Alignment alignment) + { + defaultAlignment = alignment; + return this; + } + public LayoutBuilder Alignment(Alignment alignment) + { + Layout.Draw(lastCommand!, alignment); + lastCommand = null; + return this; + } + + public LayoutBuilder Draw(IDrawingCommand command) + { + if (lastCommand != null) + { + Layout.Draw(lastCommand, defaultAlignment); + } + lastCommand = command; + return this; + } + + public LayoutBuilder DrawRange(params IDrawingCommand[] commands) + { + foreach (var drawingCommand in commands) + { + Draw(drawingCommand); + } + return this; + } + + public ComplexDrawingCommand Build() + { + if (lastCommand != null) + { + Layout.Draw(lastCommand, defaultAlignment); + } + + return Layout; + } + + public ComplexDrawingCommand UnsafeGetLayout() => Layout; + + // public static implicit operator IDrawingCommand(LayoutBuilder builder) + // { + // return builder.Build(); + // } +} + +public static class LayoutExtensions +{ + + public static LayoutBuilder Margin(this LayoutBuilder builder, int xy) + { + builder.Draw(new Margin(new Size(xy, xy))); + return builder; + } + + public static LayoutBuilder Margin10(this LayoutBuilder builder) => Margin(builder, 10); + public static LayoutBuilder Margin20(this LayoutBuilder builder) => Margin(builder, 20); + public static LayoutBuilder Margin100(this LayoutBuilder builder) => Margin(builder, 100); + + public static LayoutBuilder MarginX(this LayoutBuilder builder, int x) + { + builder.Draw(new Margin(new Size(x, 0))); + return builder; + } + public static LayoutBuilder MarginY(this LayoutBuilder builder, int y) + { + builder.Draw(new Margin(new Size(0, y))); + return builder; + } + + public static LayoutBuilder BackgroundColor(this LayoutBuilder builder, Color color) + { + builder.Draw(new FillCommand(builder.UnsafeGetLayout(), color)); + return builder; + } + + public static LayoutBuilder Text(this LayoutBuilder builder, string text, TextOptions? options = null) + { + builder.Draw(new TextCommand(text, options ?? builder.Options)); + return builder; + } + + public static LayoutBuilder Image(this LayoutBuilder builder, SKBitmap image) + { + builder.Draw(new ImageCommand(image)); + return builder; + } + + public static LayoutBuilder Rect(this LayoutBuilder builder, Size size, Color color) + { + builder.Draw(new FillCommandCore(size, color)); + return builder; + } + + public static LayoutBuilder ImageResource(this LayoutBuilder builder, string res) + { + builder.Draw(Painter.GetResource(res).AsCommand()); + return builder; + } + + public static IDrawingCommand ApplyRoundedCorner(this IDrawingCommand command, Color color, float curvature, + int margin = 0) + { + return new RoundedRectCommand(command, color, curvature, margin); + } + + public static IDrawingCommand ApplyBackground(this IDrawingCommand command, Color color) + { + return new FillCommand(command, color); + } + + public static IDrawingCommand ApplyMargin(this IDrawingCommand command, int margin) + { + return MarginCommand.Of(command, margin); + } +} + +public sealed class Margin : IDrawingContent +{ + public Size Size { get; } + + public Margin(Size size) => Size = size; + + // public static Margin Of(int xy) => new Margin(Size.Of(xy, xy)); + // public static Margin Of(int x, int y) => new Margin(Size.Of(x, y)); + // public static Margin OfX(int x) => new Margin(Size.Of(x, 0)); + // public static Margin OfY(int y) => new Margin(Size.Of(0, y)); + + public void DrawCore(SKCanvas canvas, Point position) { } + +} + + +public interface IDrawingCore +{ + void DrawCore(SKCanvas canvas, Point position); +} + +public interface IDrawingContent : IDrawingCommand, IDrawingCore +{ + +} + +public interface IDrawingCommand +{ + Size Size { get; } + virtual DrawingPriority DrawingPriority => DrawingPriority.Foreground; +} + +public record struct DrawingCommandWithAlignment(IDrawingCommand DrawingCommand, Alignment Alignment); +public record struct DrawingContentWithPosition(IDrawingContent DrawingCommand, Point Position); + + +public abstract class ComplexDrawingCommand : IDrawingCommand +{ + protected List DrawingCommands { get; } = new (); + protected int version = 0; + + public void Draw(IDrawingCommand command, Alignment alignment = Alignment.TopOrLeft) + { + DrawingCommands.Add(new(command, alignment)); + version++; + } + + public abstract Size Size { get; } + public abstract void AcquireAllDrawingCommands(List commands, Point offset); + public virtual DrawingPriority DrawingPriority => DrawingPriority.Foreground; + + protected static void ApplyCommand(List commands, IDrawingCommand drawingCommand, Point position) + { + if (drawingCommand is ComplexDrawingCommand layout) + { + layout.AcquireAllDrawingCommands(commands, position); + } + else if (drawingCommand is IDrawingContent content and not Margin) + { + commands.Add(new DrawingContentWithPosition(content, position)); + } + } +} + +public sealed class DrawingCommandWrapper : ComplexDrawingCommand +{ + IDrawingCommand _command; + + public DrawingCommandWrapper(IDrawingCommand command) + { + _command = command; + Size = _command.Size; + } + + public override Size Size { get; } + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + ApplyCommand(commands, _command, offset); + } +} + + + + +public sealed class HorizontalDockLayout : ComplexDrawingCommand +{ + readonly int _minWidth; + Size lastSize; + int lastVersion = -1; + bool _forceWidth; + + public HorizontalDockLayout(int minWidth = -1, bool forceWidth = false) + { + _minWidth = minWidth; + _forceWidth = forceWidth; + } + + public override Size Size + { + get + { + if (lastVersion != version) + { + lastSize = Size.Of( + _forceWidth ? _minWidth : Math.Max(_minWidth, DrawingCommands.Select(x => x.DrawingCommand.Size.Width).Max()), + DrawingCommands.Select(x => x.DrawingCommand.Size.Height).Max()); + } + Debug.Assert(_minWidth != 0); + return lastSize; + } + } + + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + var size = Size; + var globalY = offset.Y; + var maxWidth = size.Width; + foreach (var (drawingCommand, alignment) in DrawingCommands) + { + var xOrigin = offset.X; + var x = alignment switch + { + Alignment.TopOrLeft => xOrigin, + Alignment.Center => xOrigin + (maxWidth - drawingCommand.Size.Width) / 2, + Alignment.DownOrRight => xOrigin + (maxWidth - drawingCommand.Size.Width), + _ => throw new ArgumentOutOfRangeException() + }; + var position = Point.Of(x, globalY); + ApplyCommand(commands, drawingCommand, position); + } + } + +} + + +public sealed class VerticalLayout : ComplexDrawingCommand +{ + Size lastSize; + int lastVersion = -1; + + public override Size Size + { + get + { + if (lastVersion != version) + { + lastVersion = version; + var maxWidth = 0; + var height = 0; + foreach (var (drawingCommand, alignment) in DrawingCommands) + { + height += drawingCommand.Size.Height; + var width = drawingCommand.Size.Width; + maxWidth = width > maxWidth ? width : maxWidth; + } + lastSize = Size.Of(maxWidth, height); + } + + return lastSize; + } + } + + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + var size = Size; + var globalY = offset.Y; + var maxWidth = size.Width; + foreach (var (drawingCommand, alignment) in DrawingCommands) + { + var xOrigin = offset.X; + var x = alignment switch + { + Alignment.TopOrLeft => xOrigin, + Alignment.Center => xOrigin + (maxWidth - drawingCommand.Size.Width) / 2, + Alignment.DownOrRight => xOrigin + (maxWidth - drawingCommand.Size.Width), + _ => throw new ArgumentOutOfRangeException() + }; + var position = Point.Of(x, globalY); + ApplyCommand(commands, drawingCommand, position); + + globalY += drawingCommand.Size.Height; + } + } +} + + +public sealed class HorizontalLayout : ComplexDrawingCommand +{ + Size lastSize; + int lastVersion = -1; + + public override Size Size + { + get + { + if (lastVersion != version) + { + lastVersion = version; + var maxHeight = 0; + var width = 0; + foreach (var (drawingCommand, alignment) in DrawingCommands) + { + width += drawingCommand.Size.Width; + var height = drawingCommand.Size.Height; + maxHeight = height > maxHeight ? height : maxHeight; + } + lastSize = Size.Of(width, maxHeight); + } + + return lastSize; + } + } + + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + var size = Size; + var globalX = offset.X; + var maxHeight = size.Height; + foreach (var (drawingCommand, alignment) in DrawingCommands) + { + var yOrigin = offset.Y; + var y = alignment switch + { + Alignment.TopOrLeft => yOrigin, + Alignment.Center => yOrigin + (maxHeight - drawingCommand.Size.Height) / 2, + Alignment.DownOrRight => yOrigin + (maxHeight - drawingCommand.Size.Height), + _ => throw new ArgumentOutOfRangeException() + }; + var position = Point.Of(globalX, y); + ApplyCommand(commands, drawingCommand, position); + + globalX += drawingCommand.Size.Width; + } + } + + +} + +public enum Alignment +{ + TopOrLeft, Center, DownOrRight +} + +public enum DrawingPriority +{ + Background, Foreground +} + +public record struct Size(int Width, int Height) +{ + public static Size Of(int x, int y) => new (x, y); + public static Size Of(double x, int y) => new ((int)x, y); + public static Size Of(double x, double y) => new ((int)x, (int)y); + public static Size Of(int x, double y) => new ((int)x, (int)y); +} + +public record struct Point(int X, int Y) +{ + public static Point Of(int x, int y) => new Point(x, y); + public static implicit operator SKPoint(Point p) => new SKPoint(p.X, p.Y); +} + + + +public static class SbCSharpUtils +{ + public static SKColor ToSkColor(this Color color) => new SKColor(color.R, color.G, color.B, color.A); + public static ImageCommand AsCommand(this SKBitmap bitmap) => new ImageCommand(bitmap); + + public static void AcquireAllDrawingCommands(this IDrawingCommand content, List list, Point? pos = null) => + new DrawingCommandWrapper(content).AcquireAllDrawingCommands(list, pos ?? Point.Of(0, 0)); + + public static SKBitmap Resize(this SKBitmap bitmap, int resizedWidth, int resizedHeight) + { + using var surface2 = SKSurface.Create(new SKImageInfo(resizedWidth, resizedHeight, SKColorType.Rgba8888)); + using var paint = new SKPaint(); + paint.IsAntialias = true; + paint.FilterQuality = SKFilterQuality.High; + + surface2.Canvas.DrawBitmap(bitmap, new SKRectI(0, 0, resizedWidth, resizedHeight), + paint); + surface2.Canvas.Flush(); + + using var newImg = surface2.Snapshot(); + return SKBitmap.FromImage(newImg); + } +} \ No newline at end of file diff --git a/WFBot.Koharu/DrawingContents.cs b/WFBot.Koharu/DrawingContents.cs new file mode 100644 index 0000000..7822cdc --- /dev/null +++ b/WFBot.Koharu/DrawingContents.cs @@ -0,0 +1,282 @@ +using System.Diagnostics; +using System.Drawing; +using SkiaSharp; +using Topten.RichTextKit; +using static System.Net.Mime.MediaTypeNames; + +namespace WFBot.Koharu; + + + +public sealed class MarginCommand : ComplexDrawingCommand +{ + int _top; + int _left; + int _down; + int _right; + IDrawingCommand _drawingCommand; + + public MarginCommand(IDrawingCommand command, int top, int left, int down, int right) + { + _drawingCommand = command; + _top = top; + _left = left; + _down = down; + _right = right; + } + + public override Size Size + { + get + { + var size = _drawingCommand.Size; + size.Height += _top + _down; + size.Width += _left + _right; + return size; + } + } + + public static MarginCommand Of(IDrawingCommand command, int margin) => + new MarginCommand(command, margin, margin, margin, margin); + + + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + offset.X += _left; + offset.Y += _top; + ApplyCommand(commands, _drawingCommand, offset); + } +} + +public sealed class RoundedRectCommand : ComplexDrawingCommand +{ + IDrawingCommand _drawingCommand; + float _curvature; + Color _color; + int _margin; + + public RoundedRectCommand(IDrawingCommand command, Color color, float curvature, int margin = 0) + { + _color = color; + _drawingCommand = command; + _margin = margin; + _curvature = curvature; + } + + public override Size Size + { + get + { + var size = _drawingCommand.Size; + size.Height += 2 * _margin; + size.Width += 2 * _margin; + return size; + } + } + + public override DrawingPriority DrawingPriority => DrawingPriority.Background; + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + ApplyCommand(commands, new RoundedRectCommandCore(_drawingCommand.Size, _color, _curvature, _margin), offset); + offset.X += _margin; + offset.Y += _margin; + ApplyCommand(commands, _drawingCommand, offset); + } +} + +public sealed class RoundedRectCommandCore : IDrawingContent +{ + float _curvature; + + public RoundedRectCommandCore(Size size, Color color, float curvature, int margin) + { + size.Width += margin * 2; + size.Height += margin * 2; + Size = size; + Color = color; + _curvature = curvature; + } + + public Color Color { get; } + public Size Size { get; } + public void DrawCore(SKCanvas canvas, Point position) + { + canvas.DrawRoundRect(position.X, position.Y, Size.Width, Size.Height, _curvature, _curvature, new SKPaint() { Color = Color.ToSkColor() }); + } +} + +public sealed class FillCommand : ComplexDrawingCommand +{ + IDrawingCommand _drawingCommand; + Color _color; + + public FillCommand(IDrawingCommand command, Color color) + { + _color = color; + _drawingCommand = command; + } + + public override Size Size => _drawingCommand.Size; + public override DrawingPriority DrawingPriority => DrawingPriority.Background; + public override void AcquireAllDrawingCommands(List commands, Point offset) + { + ApplyCommand(commands, new FillCommandCore(_drawingCommand.Size, _color), offset); + ApplyCommand(commands, _drawingCommand, offset); + } +} +public sealed class FillCommandCore : IDrawingContent +{ + public FillCommandCore(Size size, Color color) + { + Size = size; + Color = color; + } + + public Color Color { get; } + public Size Size { get; } + public void DrawCore(SKCanvas canvas, Point position) + { + canvas.DrawRect(position.X, position.Y, Size.Width, Size.Height, new SKPaint() { Color = Color.ToSkColor() }); + } +} + +class MyFontMapper : FontMapper +{ + static SKTypeface font = SKTypeface.FromFile("WFConfigs/font.ttf"); + public override SKTypeface TypefaceFromStyle(IStyle style, bool ignoreFontVariants) + { + return font; + } +} + +public sealed class TextCommand : IDrawingContent +{ + public TextCommand(string text, TextOptions options) + { + textBlock = new TextBlock(); + textBlock.AddText(text, new Style() + { + TextColor = options.Color.ToSkColor(), + FontSize = options.Size, + FontWeight = options.Bold ? 400 : 700, + }); + + textBlock.MaxWidth = options.MaxWidth == -1 ? null : options.MaxWidth; + textBlock.FontMapper = new MyFontMapper(); + size = new Size((int)textBlock.MeasuredWidth, (int)textBlock.MeasuredHeight); + //Debug.Assert(size.Height != 0); + } + + //static SKCanvas measurerCanvas = SKSurface.Create(new SKImageInfo(800, 700, SKColorType.Rgba8888)).Canvas; + + + Size size; + public Size Size => size; + + TextBlock textBlock; + + public void DrawCore(SKCanvas canvas, Point position) + { + textBlock.Paint(canvas, position, new TextPaintOptions(){Edging = SKFontEdging.Antialias}); + } +} + +public class RichTextBuilder +{ + TextBlock textBlock = new TextBlock(); + TextOptions lastTextOptions; + string? lastText = null; + + void Commit() + { + if (lastText != null) + { + textBlock.AddText(lastText, new Style() + { + TextColor = lastTextOptions.Color.ToSkColor(), + FontSize = lastTextOptions.Size, + FontWeight = lastTextOptions.Bold ? 400 : 700, + }); + lastTextOptions = Painter.textOptions; + } + } + private RichTextBuilder() {} + + public static RichTextBuilder Create(int maxWidth = 1000) + { + var b = new RichTextBuilder(); + b.lastTextOptions = Painter.textOptions; + b.textBlock.MaxWidth = maxWidth; + return b; + } + + public RichTextBuilder Text(string text) + { + Commit(); + lastText = text; + return this; + } + + public RichTextBuilder Bold() + { + lastTextOptions.Bold = true; + return this; + } + + public RichTextBuilder Color(Color color) + { + lastTextOptions.Color = color; + return this; + } + + public RichTextBuilder Size(int size) + { + lastTextOptions.Size = size; + return this; + } + + public IDrawingCommand Build() + { + Commit(); + return new RichTextCommand(textBlock); + } +} + +public class RichTextCommand : IDrawingCommand +{ + TextBlock textBlock; + + public RichTextCommand(TextBlock textBlock) + { + this.textBlock = textBlock; + + textBlock.FontMapper = new MyFontMapper(); + size = new Size((int)textBlock.MeasuredWidth, (int)textBlock.MeasuredHeight); + } + Size size; + public Size Size => size; + + public void DrawCore(SKCanvas canvas, Point position) + { + textBlock.Paint(canvas, position, new TextPaintOptions() { Edging = SKFontEdging.Antialias }); + } +} + +public record struct TextOptions(int Size, Color Color, bool Bold, int MaxWidth); + +public sealed class ImageCommand : IDrawingContent +{ + SKBitmap bitmap; + + public ImageCommand(SKBitmap bitmap) + { + this.bitmap = bitmap; + } + + public Size Size => Size.Of(bitmap.Width, bitmap.Height); + public void DrawCore(SKCanvas canvas, Point position) + { + canvas.DrawBitmap(bitmap, position); + } + +} \ No newline at end of file diff --git a/WFBot.Koharu/GlobalUsings.cs b/WFBot.Koharu/GlobalUsings.cs new file mode 100644 index 0000000..2e9f188 --- /dev/null +++ b/WFBot.Koharu/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading.Tasks; +// ReSharper disable UnusedMember.Global +global using Color = System.Drawing.Color; diff --git a/WFBot.Koharu/Models.cs b/WFBot.Koharu/Models.cs new file mode 100644 index 0000000..f82da41 --- /dev/null +++ b/WFBot.Koharu/Models.cs @@ -0,0 +1,62 @@ +namespace WFBot.Koharu; + +public record InvasionData(InvasionData.WFInvasion[] Invasions) +{ + public class WFInvasion + { + public string id { get; set; } + public DateTime activation { get; set; } + public string startString { get; set; } + public string node { get; set; } + public string desc { get; set; } + public RewardInfo attackerReward { get; set; } + public string attackingFaction { get; set; } + public RewardInfo defenderReward { get; set; } + public string defendingFaction { get; set; } + public bool vsInfestation { get; set; } + public int count { get; set; } + public int requiredRuns { get; set; } + public float completion { get; set; } + public bool completed { get; set; } + public string eta { get; set; } + public string[] rewardTypes { get; set; } + } + + public class RewardInfo + { + public Counteditem[] countedItems { get; set; } + public int credits { get; set; } + public string asString { get; set; } + public string itemString { get; set; } + public string thumbnail { get; set; } + public int color { get; set; } + } + + public class Counteditem + { + public int count { get; set; } + public string type { get; set; } + } +} + + +public record FissureData(List Fissures, int Tier) +{ + public class Fissure + { + public string id { get; set; } + public DateTime activation { get; set; } + public string startString { get; set; } + public DateTime expiry { get; set; } + public bool active { get; set; } + public string node { get; set; } + public string missionType { get; set; } + public string enemy { get; set; } + public string tier { get; set; } + public int tierNum { get; set; } + public bool expired { get; set; } + public string eta { get; set; } + public bool isStorm { get; set; } + public bool isHard { get; set; } + } +} diff --git a/WFBot.Koharu/Painters.cs b/WFBot.Koharu/Painters.cs new file mode 100644 index 0000000..f92936c --- /dev/null +++ b/WFBot.Koharu/Painters.cs @@ -0,0 +1,150 @@ +using GammaLibrary.Extensions; +using SkiaSharp; +using System.Drawing; +using static WFBot.Koharu.InvasionData; + +namespace WFBot.Koharu; + + +public class FissurePainter : Painter +{ + public override IDrawingCommand Draw(FissureData data) + { + var fissures = data.Fissures; + var tier = data.Tier; + if (!fissures.Any()) return SimpleImageRendering("目前没有裂隙"); + fissures = fissures.Where(x => tier == 0 || x.tierNum == tier).OrderBy(f => f.tierNum).ToList(); + var commands = fissures.Select(Single).ToArray(); + var max = commands.Max(x => x.Size.Width); + var i = 0; + var lineColorBool = true; + foreach (ref var command in commands.AsSpan()) + { + command = PlaceLeftAndRight(command, + HorizontalLayout().Margin100().ImageResource($"Factions.{fissures[i++].enemy.ToLower()}").Build(), max) + .ApplyBackground(SwitchLineColor(ref lineColorBool)); + } + + return VerticalLayout().DrawRange(commands).Build(); + } + + static IDrawingCommand Single(FissureData.Fissure fissure) + { + var left = HorizontalLayout(Alignment.Center) + .ImageResource($"Fissures.{fissure.tierNum}").Margin20() + .Draw(VerticalLayout() + .Text($"{fissure.missionType} - {fissure.enemy}", textOptions with { Size = 43 }) + .Text( + $"{fissure.tier}(T{fissure.tierNum}) {(fissure.isHard ? "钢铁裂缝" : fissure.isStorm ? "虚空风暴" : "普通裂缝")}", + textOptions with { Size = 43 }) + .Text($"{fissure.node}", textOptions with { Size = 33 }) + .Text($"{fissure.eta}", textOptions with { Size = 33, Color = Color.FromArgb(170, 170, 170) }) + .Margin10() + .Build()).Build(); + return left; + } +} + + +public class InvasionPainter : Painter +{ + public override IDrawingCommand Draw(InvasionData data) + { + var invasions = data.Invasions.Select(SingleInvasion).ToArray(); + var lineColorBool = true; + foreach (ref var command in invasions.AsSpan()) + { + command = command.ApplyBackground(SwitchLineColor(ref lineColorBool)); + } + + return VerticalLayout().DrawRange(invasions).Build(); + } + + + public IDrawingCommand SingleInvasion(InvasionData.WFInvasion invasion) + { + var grineerColor = Color.FromArgb(227, 49, 62); + var infestedColor = Color.FromArgb(106, 220, 141); + var corpusColor = Color.FromArgb(96, 182, 229); + var aColor = invasion.attackingFaction switch + { + "Corpus" => corpusColor, + "Infested" => infestedColor, + "Grineer" => grineerColor + }; + var aPercent = invasion.completion / 100.0; + var bColor = invasion.defendingFaction switch + { + "Corpus" => corpusColor, + "Infested" => infestedColor, + "Grineer" => grineerColor + }; + + var bPercent = 1 - aPercent; + var percentageBarWidth = 560; + var percentageBarHeight = 10; + var percentageBar = HorizontalLayout().Rect(Size.Of(aPercent * percentageBarWidth, percentageBarHeight), aColor) + .Rect(Size.Of(bPercent * percentageBarWidth, percentageBarHeight), bColor).Build(); + + var factionA = $"{invasion.attackingFaction.ToUpper()} {aPercent * 100:F1}%"; + var factionB = $"{bPercent * 100:F1}% {invasion.defendingFaction.ToUpper()}"; + var rewardA = $"{(!invasion.vsInfestation ? $"{ToString(invasion.attackerReward)}" : "")}"; + var rewardB = $"{ToString(invasion.defenderReward)}"; + var attackerImage = GetResourceWithSize($"Factions.{invasion.attackingFaction.ToLower()}",30, 30); + var defenderImage = GetResourceWithSize($"Factions.{invasion.defendingFaction.ToLower()}",30, 30); + + + SKBitmap attackerReward1 = null; + if (!invasion.vsInfestation) + { + attackerReward1 = GetInvasionReward(invasion.attackerReward.countedItems.FirstOrDefault()?.type); + } + IDrawingCommand attackerReward = invasion.vsInfestation + ? new Margin(Size.Of(35, 35)) + : attackerReward1.AsCommand(); + var defenderRewardImage = GetInvasionReward(invasion.defenderReward.countedItems.First().type); + var defenderReward = defenderRewardImage.AsCommand(); + + var desc = Text($"{FlipNode(invasion.node)}", textOptions with { Size = 23, Bold = true }); + var infoTextOptions = textOptions with {Size = 18}; + var faction = PlaceLeftAndRight(HorizontalLayout().Image(attackerImage).Margin10().Text(factionA, infoTextOptions).Build(), + HorizontalLayout().Text(factionB, infoTextOptions).Margin10().Image(defenderImage).Build(), percentageBarWidth, true); + var reward = PlaceLeftAndRight(HorizontalLayout().Draw(attackerReward).Margin10().Text(rewardA, infoTextOptions).Build(), + HorizontalLayout().Text(rewardB, infoTextOptions).Margin10().Draw(defenderReward).Build(), percentageBarWidth, true); + + var result = VerticalLayout().Draw(desc).Margin10().Draw(percentageBar).Margin10().Draw(faction).Margin10() + .Draw(reward).Build().ApplyMargin(20); + + + + return result; + } + + public static string ToString(RewardInfo reward) + { + var rewards = new List(); + if (reward.credits > 0) + { + rewards.Add($"{reward.credits} cr"); + } + + foreach (var item in reward.countedItems) + { + rewards.Add($"{item.count}x{item.type}"); + } + + return string.Join(" + ", rewards); + } + public static string FlipNode(string node) + { + return node.Split(' ').Reverse().Connect(" "); + } + + public static SKBitmap GetInvasionReward(string name) + { + var trims = new List { "蓝图", "枪管", "枪机", "枪托", "连接器", "刀刃", "握柄", "散热器" }; + name = trims.Aggregate(name, (current, trim) => current.Replace(trim, "")); + name = name.Trim(); + return GetResourceWithSize($"InvasionRewards.{name}", 35, 35); + } +} \ No newline at end of file diff --git a/WFBot.Koharu/Resources/Factions/corpus.png b/WFBot.Koharu/Resources/Factions/corpus.png new file mode 100644 index 0000000..cfaeab5 Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/corpus.png differ diff --git a/WFBot.Koharu/Resources/Factions/corrupted.png b/WFBot.Koharu/Resources/Factions/corrupted.png new file mode 100644 index 0000000..b23f9d9 Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/corrupted.png differ diff --git a/WFBot.Koharu/Resources/Factions/crossfire.png b/WFBot.Koharu/Resources/Factions/crossfire.png new file mode 100644 index 0000000..0448cec Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/crossfire.png differ diff --git a/WFBot.Koharu/Resources/Factions/grineer.png b/WFBot.Koharu/Resources/Factions/grineer.png new file mode 100644 index 0000000..f7178f3 Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/grineer.png differ diff --git a/WFBot.Koharu/Resources/Factions/infested.png b/WFBot.Koharu/Resources/Factions/infested.png new file mode 100644 index 0000000..6d409f1 Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/infested.png differ diff --git a/WFBot.Koharu/Resources/Factions/orokin.png b/WFBot.Koharu/Resources/Factions/orokin.png new file mode 100644 index 0000000..5f29c9f Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/orokin.png differ diff --git a/WFBot.Koharu/Resources/Factions/sentient.png b/WFBot.Koharu/Resources/Factions/sentient.png new file mode 100644 index 0000000..089b40b Binary files /dev/null and b/WFBot.Koharu/Resources/Factions/sentient.png differ diff --git a/WFBot.Koharu/Resources/Fissures/1.png b/WFBot.Koharu/Resources/Fissures/1.png new file mode 100644 index 0000000..7900a93 Binary files /dev/null and b/WFBot.Koharu/Resources/Fissures/1.png differ diff --git a/WFBot.Koharu/Resources/Fissures/2.png b/WFBot.Koharu/Resources/Fissures/2.png new file mode 100644 index 0000000..387d251 Binary files /dev/null and b/WFBot.Koharu/Resources/Fissures/2.png differ diff --git a/WFBot.Koharu/Resources/Fissures/3.png b/WFBot.Koharu/Resources/Fissures/3.png new file mode 100644 index 0000000..ce62d2c Binary files /dev/null and b/WFBot.Koharu/Resources/Fissures/3.png differ diff --git a/WFBot.Koharu/Resources/Fissures/4.png b/WFBot.Koharu/Resources/Fissures/4.png new file mode 100644 index 0000000..1e42b5b Binary files /dev/null and b/WFBot.Koharu/Resources/Fissures/4.png differ diff --git a/WFBot.Koharu/Resources/Fissures/5.png b/WFBot.Koharu/Resources/Fissures/5.png new file mode 100644 index 0000000..3849ffe Binary files /dev/null and b/WFBot.Koharu/Resources/Fissures/5.png differ diff --git a/WFBot.Koharu/Resources/InvasionRewards/FORMA.png b/WFBot.Koharu/Resources/InvasionRewards/FORMA.png new file mode 100644 index 0000000..b053911 Binary files /dev/null and b/WFBot.Koharu/Resources/InvasionRewards/FORMA.png differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/OROKIN\345\202\254\345\214\226\345\211\202.png" "b/WFBot.Koharu/Resources/InvasionRewards/OROKIN\345\202\254\345\214\226\345\211\202.png" new file mode 100644 index 0000000..9a24c45 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/OROKIN\345\202\254\345\214\226\345\211\202.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/OROKIN\345\217\215\345\272\224\345\240\206.png" "b/WFBot.Koharu/Resources/InvasionRewards/OROKIN\345\217\215\345\272\224\345\240\206.png" new file mode 100644 index 0000000..73e3f3d Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/OROKIN\345\217\215\345\272\224\345\240\206.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\345\215\241\346\213\211\345\205\213 \344\272\241\351\255\202.png" "b/WFBot.Koharu/Resources/InvasionRewards/\345\215\241\346\213\211\345\205\213 \344\272\241\351\255\202.png" new file mode 100644 index 0000000..3a2ee02 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\345\215\241\346\213\211\345\205\213 \344\272\241\351\255\202.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\345\217\214\345\255\220\350\235\260\350\233\207 \344\272\241\351\255\202.png" "b/WFBot.Koharu/Resources/InvasionRewards/\345\217\214\345\255\220\350\235\260\350\233\207 \344\272\241\351\255\202.png" new file mode 100644 index 0000000..5763cf7 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\345\217\214\345\255\220\350\235\260\350\233\207 \344\272\241\351\255\202.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\345\270\214\350\212\231.png" "b/WFBot.Koharu/Resources/InvasionRewards/\345\270\214\350\212\231.png" new file mode 100644 index 0000000..f5a9424 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\345\270\214\350\212\231.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\345\274\202\350\236\215ALAD V\345\257\274\350\210\252\345\272\247\346\240\207.png" "b/WFBot.Koharu/Resources/InvasionRewards/\345\274\202\350\236\215ALAD V\345\257\274\350\210\252\345\272\247\346\240\207.png" new file mode 100644 index 0000000..90e253c Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\345\274\202\350\236\215ALAD V\345\257\274\350\210\252\345\272\247\346\240\207.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\345\276\267\346\213\211 \347\240\264\345\235\217\350\200\205.png" "b/WFBot.Koharu/Resources/InvasionRewards/\345\276\267\346\213\211 \347\240\264\345\235\217\350\200\205.png" new file mode 100644 index 0000000..7d59ade Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\345\276\267\346\213\211 \347\240\264\345\235\217\350\200\205.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\346\213\211\347\211\271\346\230\202 \344\272\241\351\255\202.png" "b/WFBot.Koharu/Resources/InvasionRewards/\346\213\211\347\211\271\346\230\202 \344\272\241\351\255\202.png" new file mode 100644 index 0000000..6558cfe Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\346\213\211\347\211\271\346\230\202 \344\272\241\351\255\202.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\346\226\257\347\211\271\346\234\227 \344\272\241\351\255\202.png" "b/WFBot.Koharu/Resources/InvasionRewards/\346\226\257\347\211\271\346\234\227 \344\272\241\351\255\202.png" new file mode 100644 index 0000000..a433f03 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\346\226\257\347\211\271\346\234\227 \344\272\241\351\255\202.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\347\210\206\347\207\203\345\226\267\345\260\204\345\231\250.png" "b/WFBot.Koharu/Resources/InvasionRewards/\347\210\206\347\207\203\345\226\267\345\260\204\345\231\250.png" new file mode 100644 index 0000000..08e0007 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\347\210\206\347\207\203\345\226\267\345\260\204\345\231\250.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\347\211\271\346\256\212\345\212\237\350\203\275\346\247\275\350\277\236\346\216\245\345\231\250.png" "b/WFBot.Koharu/Resources/InvasionRewards/\347\211\271\346\256\212\345\212\237\350\203\275\346\247\275\350\277\236\346\216\245\345\231\250.png" new file mode 100644 index 0000000..ea18257 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\347\211\271\346\256\212\345\212\237\350\203\275\346\247\275\350\277\236\346\216\245\345\231\250.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\347\213\231\345\207\273\347\211\271\346\230\202 \347\240\264\345\235\217\350\200\205.png" "b/WFBot.Koharu/Resources/InvasionRewards/\347\213\231\345\207\273\347\211\271\346\230\202 \347\240\264\345\235\217\350\200\205.png" new file mode 100644 index 0000000..758dddb Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\347\213\231\345\207\273\347\211\271\346\230\202 \347\240\264\345\235\217\350\200\205.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\347\224\265\347\243\201\345\212\233\345\234\272\350\243\205\347\275\256.png" "b/WFBot.Koharu/Resources/InvasionRewards/\347\224\265\347\243\201\345\212\233\345\234\272\350\243\205\347\275\256.png" new file mode 100644 index 0000000..7a246e5 Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\347\224\265\347\243\201\345\212\233\345\234\272\350\243\205\347\275\256.png" differ diff --git "a/WFBot.Koharu/Resources/InvasionRewards/\347\252\201\345\217\230\345\216\237\350\201\232\345\220\210\347\211\251.png" "b/WFBot.Koharu/Resources/InvasionRewards/\347\252\201\345\217\230\345\216\237\350\201\232\345\220\210\347\211\251.png" new file mode 100644 index 0000000..41ad8de Binary files /dev/null and "b/WFBot.Koharu/Resources/InvasionRewards/\347\252\201\345\217\230\345\216\237\350\201\232\345\220\210\347\211\251.png" differ diff --git a/WFBot.Koharu/Resources/WarframeMarket/Cubes.png b/WFBot.Koharu/Resources/WarframeMarket/Cubes.png new file mode 100644 index 0000000..2ed41cf Binary files /dev/null and b/WFBot.Koharu/Resources/WarframeMarket/Cubes.png differ diff --git a/WFBot.Koharu/Resources/WarframeMarket/PlatinumLarge.png b/WFBot.Koharu/Resources/WarframeMarket/PlatinumLarge.png new file mode 100644 index 0000000..b262145 Binary files /dev/null and b/WFBot.Koharu/Resources/WarframeMarket/PlatinumLarge.png differ diff --git a/WFBot.Koharu/Resources/WarframeMarket/PlatinumSimple.png b/WFBot.Koharu/Resources/WarframeMarket/PlatinumSimple.png new file mode 100644 index 0000000..638cb4c Binary files /dev/null and b/WFBot.Koharu/Resources/WarframeMarket/PlatinumSimple.png differ diff --git a/WFBot.Koharu/Resources/Weathers/cold.png b/WFBot.Koharu/Resources/Weathers/cold.png new file mode 100644 index 0000000..46dbc94 Binary files /dev/null and b/WFBot.Koharu/Resources/Weathers/cold.png differ diff --git a/WFBot.Koharu/Resources/Weathers/night.png b/WFBot.Koharu/Resources/Weathers/night.png new file mode 100644 index 0000000..3ec6773 Binary files /dev/null and b/WFBot.Koharu/Resources/Weathers/night.png differ diff --git a/WFBot.Koharu/Resources/Weathers/sun.png b/WFBot.Koharu/Resources/Weathers/sun.png new file mode 100644 index 0000000..8864be1 Binary files /dev/null and b/WFBot.Koharu/Resources/Weathers/sun.png differ diff --git a/WFBot.Koharu/Resources/Weathers/warm.png b/WFBot.Koharu/Resources/Weathers/warm.png new file mode 100644 index 0000000..c23a341 Binary files /dev/null and b/WFBot.Koharu/Resources/Weathers/warm.png differ diff --git a/WFBot.Koharu/Resources/network-unstable.gif b/WFBot.Koharu/Resources/network-unstable.gif new file mode 100644 index 0000000..3df693d Binary files /dev/null and b/WFBot.Koharu/Resources/network-unstable.gif differ diff --git a/WFBot.Koharu/WFBot.Koharu.csproj b/WFBot.Koharu/WFBot.Koharu.csproj new file mode 100644 index 0000000..f2dc270 --- /dev/null +++ b/WFBot.Koharu/WFBot.Koharu.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/WFBot.Tests/WFBot.Tests.csproj b/WFBot.Tests/WFBot.Tests.csproj index cd512dc..dd36a4b 100644 --- a/WFBot.Tests/WFBot.Tests.csproj +++ b/WFBot.Tests/WFBot.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 false diff --git a/WFBot.sln b/WFBot.sln index 5f88467..eadcc2c 100644 --- a/WFBot.sln +++ b/WFBot.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WFBot", "WFBot\WFBot.csproj EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WFBot.Tests", "WFBot.Tests\WFBot.Tests.csproj", "{21E1A018-8A75-4A8A-9D1E-7972B782665A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WFBot.Koharu", "WFBot.Koharu\WFBot.Koharu.csproj", "{966DE4BC-4D55-452A-8868-488D47FBACAE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,14 @@ Global {21E1A018-8A75-4A8A-9D1E-7972B782665A}.Release|Any CPU.Build.0 = Release|Any CPU {21E1A018-8A75-4A8A-9D1E-7972B782665A}.Windows Release|Any CPU.ActiveCfg = Release|Any CPU {21E1A018-8A75-4A8A-9D1E-7972B782665A}.Windows Release|Any CPU.Build.0 = Release|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Linux Release|Any CPU.ActiveCfg = Release|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Linux Release|Any CPU.Build.0 = Release|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Release|Any CPU.Build.0 = Release|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Windows Release|Any CPU.ActiveCfg = Release|Any CPU + {966DE4BC-4D55-452A-8868-488D47FBACAE}.Windows Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WFBot/Features/Commands/NotificationCommands.cs b/WFBot/Features/Commands/NotificationCommands.cs index 16d18c5..732f82b 100644 --- a/WFBot/Features/Commands/NotificationCommands.cs +++ b/WFBot/Features/Commands/NotificationCommands.cs @@ -3,12 +3,16 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using SharpVk; +using SkiaSharp; using WFBot.Features.Common; using WFBot.Features.ImageRendering; using WFBot.Features.Other; using WFBot.Features.Utils; +using WFBot.Koharu; using WFBot.TextCommandCore; using WFBot.Utils; +using Version = System.Version; namespace WFBot.Features.Commands { @@ -17,6 +21,10 @@ public partial class CommandsHandler { WFNotificationHandler WFNotificationHandler => WFBotCore.Instance.NotificationHandler; + + + + [Matchers("警报")] [AddPlatformInfo] async Task Alerts() @@ -59,9 +67,11 @@ async Task Invasions() return; } } + var s = new ImageRenderProfiler(); try { await WFNotificationHandler.UpdateInvasionPool(); + s.Segment("网络请求"); } catch (OperationCanceledException) { @@ -83,7 +93,12 @@ async Task Invasions() } else { - SendImage(ImageRenderHelper.Invasion(invasions.Where(invasion => !invasion.completed))); + using var painter = Painters.Create(); + var data = new InvasionData(invasions.Where(invasion => !invasion.completed).ToJsonStringS().JsonDeserializeS()); + + var img = painter.Draw(data).BuildImage(); + s.Segment("画图总耗时"); + SendImage(img); } } diff --git a/WFBot/Features/Commands/StatusCommands.cs b/WFBot/Features/Commands/StatusCommands.cs index 9d78aac..e7139d2 100644 --- a/WFBot/Features/Commands/StatusCommands.cs +++ b/WFBot/Features/Commands/StatusCommands.cs @@ -5,9 +5,13 @@ using System.Text; using System.Threading.Tasks; using GammaLibrary.Extensions; +using Humanizer; +using SharpVk; +using SkiaSharp; using WFBot.Features.ImageRendering; using WFBot.Features.Resource; using WFBot.Features.Utils; +using WFBot.Koharu; using WFBot.Orichalt; using WFBot.TextCommandCore; using WFBot.Utils; @@ -32,8 +36,8 @@ async Task FortunaMissions(int index = 0) { OutputStringBuilder.Value.AddPlatformInfo().AddRemainCallCount(); var s = OutputStringBuilder.Value.ToString(); - OutputStringBuilder.Value.Clear(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + OutputStringBuilder.Value.Clear(); + SendImage(KoharuAdapter.SimpleStringRendering(s)); } } @@ -50,7 +54,7 @@ async Task CetusMissions(int index = 0) OutputStringBuilder.Value.AddPlatformInfo().AddRemainCallCount(); var s = OutputStringBuilder.Value.ToString(); OutputStringBuilder.Value.Clear(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); } } @@ -67,7 +71,7 @@ async Task NecraliskMissions(int index = 0) OutputStringBuilder.Value.AddPlatformInfo().AddRemainCallCount(); var s = OutputStringBuilder.Value.ToString(); OutputStringBuilder.Value.Clear(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); } } @@ -143,7 +147,7 @@ async Task Sortie() } var s = WFFormatter.ToString(await api.GetSortie()); s = s.AddRemainCallCount().AddPlatformInfo(); - SendImage(ImageRenderHelper.SimpleImageRendering(s)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); return null; } else @@ -166,8 +170,9 @@ async Task VoidTrader() return null; } var s = WFFormatter.ToString(await api.GetVoidTrader()); - s = s.AddRemainCallCount().AddPlatformInfo(); - SendImage(ImageRenderHelper.SimpleImageRendering(s)); + s = s.AddRemainCallCount().AddPlatformInfo(); + SendImage(KoharuAdapter.SimpleStringRendering(s)); + return null; } else @@ -188,8 +193,7 @@ async Task Events() { var s = WFFormatter.ToString(events); s = s.AddRemainCallCount().AddPlatformInfo(); - - SendImage(ImageRenderHelper.SimpleImageRendering(s)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); return ""; } else @@ -203,6 +207,9 @@ async Task Events() } } + + + [Matchers("裂隙", "裂缝")] [AddPlatformInfoAndAddRemainCallCountToTheCommandResultAndMakeTRKSHappyByDoingSoWhatSoEver] async Task Fissures(int tier = 0) @@ -217,7 +224,12 @@ async Task Fissures(int tier = 0) return null; } var fissures = (await api.GetFissures()).Where(fissure => fissure.active && !fissure.isStorm && !fissure.isHard).ToList(); - RichMessageSender(new RichMessages() { new ImageMessage() { Content = ImageRenderHelper.Fissures(fissures, tier) } }); + + var command = new FissurePainter().Draw(new FissureData(fissures.ToJsonStringS().JsonDeserializeS>(), tier)); + var bytes = command.BuildImage(); + SendImage(bytes); + + return null; } else @@ -242,7 +254,9 @@ async Task FissuresStorm(int tier = 0) return null; } var fissures = (await api.GetFissures()).Where(fissure => fissure.active && fissure.isStorm).ToList(); - RichMessageSender(new RichMessages() { new ImageMessage() { Content = ImageRenderHelper.Fissures(fissures, tier) } }); + + var command = new FissurePainter().Draw(new FissureData(fissures.ToJsonStringS().JsonDeserializeS>(), tier)); + SendImage(command.BuildImage()); return null; } else @@ -267,7 +281,10 @@ async Task FissuresHard(int tier = 0) return null; } var fissures = (await api.GetFissures()).Where(fissure => fissure.active && fissure.isHard).ToList(); - RichMessageSender(new RichMessages() { new ImageMessage() { Content = ImageRenderHelper.Fissures(fissures, tier) } }); + + var command = new FissurePainter().Draw(new FissureData(fissures.ToJsonStringS().JsonDeserializeS>(), tier)); + SendImage(command.BuildImage()); + return null; } else @@ -285,7 +302,7 @@ async Task NightWave() var s = WFFormatter.ToString(await api.GetNightWave()); if (AsyncContext.GetUseImageRendering()) { - SendImage(ImageRenderHelper.SimpleImageRendering(s)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); return null; } else @@ -311,7 +328,7 @@ async Task Arbitration() OutputStringBuilder.Value.AddRemainCallCount().AddPlatformInfo(); var s = OutputStringBuilder.Value.ToString(); OutputStringBuilder.Value.Clear(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); } } @@ -337,7 +354,7 @@ async Task Kuva() OutputStringBuilder.Value.AddRemainCallCount().AddPlatformInfo(); var s = OutputStringBuilder.Value.ToString(); OutputStringBuilder.Value.Clear(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); } } @@ -353,7 +370,7 @@ async Task SentientOutpost() OutputStringBuilder.Value.AddRemainCallCount().AddPlatformInfo(); var s = OutputStringBuilder.Value.ToString(); OutputStringBuilder.Value.Clear(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); } } @@ -365,7 +382,7 @@ async Task ArchonHunt() if (AsyncContext.GetUseImageRendering()) { s = s.AddRemainCallCount().AddPlatformInfo(); - SendImage(ImageRenderHelper.SimpleImageRendering(s, maxLength: 1000)); + SendImage(KoharuAdapter.SimpleStringRendering(s)); return ""; } diff --git a/WFBot/Features/Common/WFNotificationHandler.cs b/WFBot/Features/Common/WFNotificationHandler.cs index bb14c81..19d9646 100644 --- a/WFBot/Features/Common/WFNotificationHandler.cs +++ b/WFBot/Features/Common/WFNotificationHandler.cs @@ -8,6 +8,7 @@ using WFBot.Features.Timers; using WFBot.Features.Timers.Base; using WFBot.Features.Utils; +using WFBot.Koharu; using WFBot.Orichalt; using WFBot.Utils; @@ -256,7 +257,8 @@ private void CheckInvasions() AsyncContext.SetCommandIdentifier("WFBot通知"); if (invs.Any()) { - MiguelNetwork.Broadcast(new RichMessages() { new ImageMessage() { Content = ImageRenderHelper.Invasion(invs) } }); + using var painter = Painters.Create(); + MiguelNetwork.Broadcast(new RichMessages() { new ImageMessage() { Content = painter.Draw(new InvasionData(invs.ToJsonStringS().JsonDeserializeS())).BuildImage() } }); } } diff --git a/WFBot/Features/ImageRendering/ImageRenderHelper.cs b/WFBot/Features/ImageRendering/ImageRenderHelper.cs index b254703..34fc094 100644 --- a/WFBot/Features/ImageRendering/ImageRenderHelper.cs +++ b/WFBot/Features/ImageRendering/ImageRenderHelper.cs @@ -38,120 +38,7 @@ public static TextOptions CreateTextOptions(int size = 40, bool bold = false) return new TextOptions(font); } - public static byte[] Fissures(List fissures, int tier) - { - if (!fissures.Any()) - { - return SimpleImageRendering("目前没有裂隙."); - } - fissures = fissures.Where(x => tier == 0 || x.tierNum == tier).OrderBy(f => f.tierNum).ToList(); - var images = fissures.AsParallel().AsOrdered().Select(x => SingleFissure(x)).ToArray(); - var max = images.Max(x => x.Width); - Parallel.For(0, fissures.Count, i => - { - images[i] = StackImageXCentered(images[i], new Image(max - images[i].Width + 10, 1), - Margin100, - GetResource($"Factions.{fissures[i].enemy.ToLower()}")); - }); - - - var lineColorBool = true; - images = images.ForEach(i => i.SetBackgroundColor(SwitchLineColor(ref lineColorBool))).ToArray(); - var image = StackImageY(images); - return Finish(image); - // 我还没想到怎么给Margin上色 - } - - public static Image SingleFissure(Fissure fissure) - { - return StackImageXCentered(GetResource($"Fissures.{fissure.tierNum}"), Margin20, - StackImageY( - RenderText($"{fissure.missionType} - {fissure.enemy}", options: CreateTextOptions(43, true)), - RenderText($"{fissure.tier}(T{fissure.tierNum}) {(fissure.isHard ? "钢铁裂缝" : fissure.isStorm ? "虚空风暴" : "普通裂缝")}", CreateTextOptions(33), Color.White), - RenderText($"{fissure.node}", CreateTextOptions(33)), - RenderText($"{fissure.eta}", CreateTextOptions(33), new Color(new Rgba32(170,170,170))), Margin10)); - } - - public static byte[] Invasion(IEnumerable invasions) - { - if (!invasions.Any()) - { - return SimpleImageRendering("额, 没有任何入侵."); - } - var images = invasions.AsParallel().AsOrdered().Select(x => SingleInvasion(x)).ToArray(); - var lineColorBool = true; - foreach (var image in images) - { - image.SetBackgroundColor(SwitchLineColor(ref lineColorBool)); - } - - return Finish(StackImageY(images), predefinedSize: 45); - } - - public static string FlipNode(string node) - { - return node.Split(' ').Reverse().Connect(" "); - } - - public static Image GetInvasionReward(string name) - { - var trims = new List { "蓝图", "枪管", "枪机", "枪托", "连接器", "刀刃", "握柄", "散热器" }; - name = trims.Aggregate(name, (current, trim) => current.Replace(trim, "")); - name = name.Trim(); - return GetResource($"InvasionRewards.{name}"); - } - public static Image SingleInvasion(WFInvasion invasion) - { - var grineerColor = ColorX.FromArgb(227, 49, 62); - var infestedColor = ColorX.FromArgb(106, 220, 141); - var corpusColor = ColorX.FromArgb(96, 182, 229); - var aColor = invasion.attackingFaction switch - { - "Corpus" => corpusColor, - "Infested" => infestedColor, - "Grineer" => grineerColor - }; - var aPercent = invasion.completion / 100.0; - var bColor = invasion.defendingFaction switch - { - "Corpus" => corpusColor, - "Infested" => infestedColor, - "Grineer" => grineerColor - }; - - var bPercent = 1 - aPercent; - var percentageImage = new Image(560, 10); - var breakPoint = (int)(aPercent * percentageImage.Width); - percentageImage.Mutate(x => x.Fill(new Rgba32(aColor.R, aColor.G, aColor.B), new RectangleF(0,0, breakPoint, percentageImage.Height))); - percentageImage.Mutate(x => x.Fill(new Rgba32(bColor.R, bColor.G, bColor.B), new RectangleF(breakPoint, 0, percentageImage.Width - breakPoint, percentageImage.Height))); - var factionA = $"{invasion.attackingFaction.ToUpper()} {aPercent*100:F1}%"; - var factionB = $"{bPercent*100:F1}% {invasion.defendingFaction.ToUpper()}"; - var rewardA = $"{(!invasion.vsInfestation ? $"{WFFormatter.ToString(invasion.attackerReward)}" : "")}"; - var rewardB = $"{WFFormatter.ToString(invasion.defenderReward)}"; - var textOptions = CreateTextOptions(18); - var factionImage = new Image(560, 40); - var rewardImage = new Image(560, 40); - using var attackerImage = GetResource($"Factions.{invasion.attackingFaction.ToLower()}").Clone().Resize(30, 30); - using var defenderImage = GetResource($"Factions.{invasion.defendingFaction.ToLower()}").Clone().Resize(30, 30); - using var attackerReward = invasion.vsInfestation - ? new Image(35, 35) - : GetInvasionReward(invasion.attackerReward.countedItems.First().type).Clone().Resize(35, 35); - using var defenderReward = GetInvasionReward(invasion.defenderReward.countedItems.First().type).Clone().Resize(35, 35); - var desc = RenderText($"{FlipNode(invasion.node)}", CreateTextOptions(23)); - factionImage.Mutate(x => x.DrawImage(attackerImage, new Point(0,0), new GraphicsOptions())); - rewardImage.Mutate(x => x.DrawImage(attackerReward, new Point(0,0), new GraphicsOptions())); - factionImage.Mutate(x => x.DrawImage(defenderImage, new Point(factionImage.Width - defenderImage.Width,0), new GraphicsOptions())); - rewardImage.Mutate(x => x.DrawImage(defenderReward, new Point(rewardImage.Width - defenderReward.Width, 0), new GraphicsOptions())); - textOptions.Origin = new Vector2(attackerImage.Width + 10, 0); - factionImage.Mutate(x => x.DrawText(textOptions, factionA, Color.White)); - rewardImage.Mutate(x => x.DrawText(textOptions, rewardA, Color.White)); - textOptions.HorizontalAlignment = HorizontalAlignment.Right; - textOptions.TextAlignment = TextAlignment.End; - textOptions.Origin = new Vector2(factionImage.Width - defenderImage.Width - 10, 0); - factionImage.Mutate(x => x.DrawText(textOptions, factionB, Color.White)); - rewardImage.Mutate(x => x.DrawText(textOptions, rewardB, Color.White)); - return StackImageX(Margin20, StackImageY(Margin10, desc, StackImageY(Margin10, percentageImage, Margin10, factionImage, rewardImage, Margin20)), Margin20); - } + static Image Sell = RenderRectangle(40, 40, new Rgba32(63, 35, 59)) .OverlayTextCentered("卖", new Rgba32(203, 74, 158), 30) .ApplyRoundedCorners(10); diff --git a/WFBot/Features/ImageRendering/ImageRenderProfiler.cs b/WFBot/Features/ImageRendering/ImageRenderProfiler.cs index bbc7998..5db7c1c 100644 --- a/WFBot/Features/ImageRendering/ImageRenderProfiler.cs +++ b/WFBot/Features/ImageRendering/ImageRenderProfiler.cs @@ -1,30 +1,35 @@ //#define PROFILE +#define PROFILE using System.Diagnostics; - namespace WFBot.Features.ImageRendering { public class ImageRenderProfiler : IDisposable { Stopwatch sw; Stopwatch swBase; + bool fake; - public ImageRenderProfiler() + public ImageRenderProfiler(bool fake = false) { + this.fake = fake; + if (fake) return; + sw = Stopwatch.StartNew(); swBase = Stopwatch.StartNew(); } public void Segment(string s) { + if (fake) return; #if PROFILE - - Console.WriteLine($"Profiler: {s} {sw.Elapsed.TotalSeconds:F2}s"); + Console.WriteLine($"Profiler: {s} {sw.Elapsed.TotalMilliseconds:N0}ms"); sw.Restart(); #endif } public void Dispose() { + if (fake) return; #if PROFILE Console.WriteLine($"Profiler: 完成渲染 {swBase.Elapsed.TotalSeconds:F2}s"); #endif diff --git a/WFBot/Features/ImageRendering/ImageRenderingPGO.cs b/WFBot/Features/ImageRendering/ImageRenderingPGO.cs index 4994449..4ed519a 100644 --- a/WFBot/Features/ImageRendering/ImageRenderingPGO.cs +++ b/WFBot/Features/ImageRendering/ImageRenderingPGO.cs @@ -183,52 +183,52 @@ public static async void Tick() try { - AsyncContext.SetCommandIdentifier("入侵"); - await WFBotCore.Instance.NotificationHandler.UpdateInvasionPool(); - var inv = WFBotCore.Instance.NotificationHandler.InvasionPool; - var invPath = _invPath; - lock (fileAccessLock) File.WriteAllBytes(invPath, ImageRenderHelper.Invasion(inv.Where(i => !i.completed))); - - AsyncContext.SetCommandIdentifier("平原"); - var cetuscycle = await api.GetCetusCycle(); - var valliscycle = await api.GetVallisCycle(); - var earthcycle = await api.GetEarthCycle(); - var cambioncycle = await api.GetCambionCycle(); - // 均衡时间差 - cambioncycle.expiry += TimeSpan.FromSeconds(15); - valliscycle.expiry += TimeSpan.FromSeconds(15); - earthcycle.expiry += TimeSpan.FromSeconds(15); - cambioncycle.expiry += TimeSpan.FromSeconds(15); - var cyclesPath = _cyclesPath; - lock (fileAccessLock) File.WriteAllBytes(cyclesPath, ImageRenderHelper.Cycles(cetuscycle, valliscycle, earthcycle, cambioncycle)); - - AsyncContext.SetCommandIdentifier("裂隙"); - var fs = await api.GetFissures(); - var fissures = fs.Where(fissure => fissure.active && !fissure.isStorm && !fissure.isHard).ToList(); - var f1 = ImageRenderHelper.Fissures(fissures, 0); - var f1Path = _f1Path; - AsyncContext.SetCommandIdentifier("虚空风暴"); - var fissuresStorm = fs.Where(fissure => fissure.active && fissure.isStorm).ToList(); - var f2 = ImageRenderHelper.Fissures(fissuresStorm, 0); - var f2Path = _f2Path; - AsyncContext.SetCommandIdentifier("钢铁裂缝"); - var fissuresHard = fs.Where(fissure => fissure.active && fissure.isHard).ToList(); - var f3 = ImageRenderHelper.Fissures(fissuresHard, 0); - var f3Path = _f3Path; - lock (fileAccessLock) File.WriteAllBytes(f1Path, f1); - lock (fileAccessLock) File.WriteAllBytes(f2Path, f2); - lock (fileAccessLock) File.WriteAllBytes(f3Path, f3); - - AsyncContext.SetCommandIdentifier("虚空商人"); - var trader = WFFormatter.ToString(await api.GetVoidTrader()); - var traderPath = _traderPath; - lock (fileAccessLock) File.WriteAllBytes(traderPath, ImageRenderHelper.SimpleImageRendering(trader)); - - AsyncContext.SetCommandIdentifier("突击"); - var sortie = WFFormatter.ToString(await api.GetSortie()); - var sortiePath = _sortiePath; - lock (fileAccessLock) File.WriteAllBytes(sortiePath, ImageRenderHelper.SimpleImageRendering(sortie)); - Console.WriteLine("普通PGO完成一次轮转"); + // AsyncContext.SetCommandIdentifier("入侵"); + // await WFBotCore.Instance.NotificationHandler.UpdateInvasionPool(); + // var inv = WFBotCore.Instance.NotificationHandler.InvasionPool; + // var invPath = _invPath; + // lock (fileAccessLock) File.WriteAllBytes(invPath, ImageRenderHelper.Invasion(inv.Where(i => !i.completed))); + // + // AsyncContext.SetCommandIdentifier("平原"); + // var cetuscycle = await api.GetCetusCycle(); + // var valliscycle = await api.GetVallisCycle(); + // var earthcycle = await api.GetEarthCycle(); + // var cambioncycle = await api.GetCambionCycle(); + // // 均衡时间差 + // cambioncycle.expiry += TimeSpan.FromSeconds(15); + // valliscycle.expiry += TimeSpan.FromSeconds(15); + // earthcycle.expiry += TimeSpan.FromSeconds(15); + // cambioncycle.expiry += TimeSpan.FromSeconds(15); + // var cyclesPath = _cyclesPath; + // lock (fileAccessLock) File.WriteAllBytes(cyclesPath, ImageRenderHelper.Cycles(cetuscycle, valliscycle, earthcycle, cambioncycle)); + // + // AsyncContext.SetCommandIdentifier("裂隙"); + // var fs = await api.GetFissures(); + // var fissures = fs.Where(fissure => fissure.active && !fissure.isStorm && !fissure.isHard).ToList(); + // var f1 = ImageRenderHelper.Fissures(fissures, 0); + // var f1Path = _f1Path; + // AsyncContext.SetCommandIdentifier("虚空风暴"); + // var fissuresStorm = fs.Where(fissure => fissure.active && fissure.isStorm).ToList(); + // var f2 = ImageRenderHelper.Fissures(fissuresStorm, 0); + // var f2Path = _f2Path; + // AsyncContext.SetCommandIdentifier("钢铁裂缝"); + // var fissuresHard = fs.Where(fissure => fissure.active && fissure.isHard).ToList(); + // var f3 = ImageRenderHelper.Fissures(fissuresHard, 0); + // var f3Path = _f3Path; + // lock (fileAccessLock) File.WriteAllBytes(f1Path, f1); + // lock (fileAccessLock) File.WriteAllBytes(f2Path, f2); + // lock (fileAccessLock) File.WriteAllBytes(f3Path, f3); + // + // AsyncContext.SetCommandIdentifier("虚空商人"); + // var trader = WFFormatter.ToString(await api.GetVoidTrader()); + // var traderPath = _traderPath; + // lock (fileAccessLock) File.WriteAllBytes(traderPath, ImageRenderHelper.SimpleImageRendering(trader)); + // + // AsyncContext.SetCommandIdentifier("突击"); + // var sortie = WFFormatter.ToString(await api.GetSortie()); + // var sortiePath = _sortiePath; + // lock (fileAccessLock) File.WriteAllBytes(sortiePath, ImageRenderHelper.SimpleImageRendering(sortie)); + // Console.WriteLine("普通PGO完成一次轮转"); } catch (Exception e) { diff --git a/WFBot/Features/ImageRendering/KoharuAdapter.cs b/WFBot/Features/ImageRendering/KoharuAdapter.cs new file mode 100644 index 0000000..130aeb7 --- /dev/null +++ b/WFBot/Features/ImageRendering/KoharuAdapter.cs @@ -0,0 +1,192 @@ +using System.Buffers; +using BlazorStrap; +using Humanizer; +using SharpVk; +using SkiaSharp; +using WFBot.Koharu; +using WFBot.Utils; +using WFBot.WebUI; + +namespace WFBot.Features.ImageRendering; + +public class StringPainter : Painter +{ + public override IDrawingCommand Draw(string data) + { + return SimpleImageRendering(data); + } +} + +public static class KoharuAdapter +{ + static bool UseGPU = false; + + public static byte[] SimpleStringRendering(string s) + { + return new StringPainter().Draw(s).BuildImage(); + } + + + public static byte[] BuildImage(this IDrawingCommand command) + { + var profiler = new ImageRenderProfiler(true); + + var vulkan = context ??= CreateVulkan(); + + command = command.ApplyWFBotInfoTag(AsyncContext.GetCommandIdentifier()); + var list = new List(); + profiler.Segment("Make commands"); + + command.AcquireAllDrawingCommands(list); + profiler.Segment("Get all drawing commands"); + + using var surface = UseGPU ? SKSurface.Create(vulkan, false, new SKImageInfo(command.Size.Width, command.Size.Height, SKColorType.Rgba8888)) + : SKSurface.Create(new SKImageInfo(command.Size.Width, command.Size.Height, SKColorType.Rgba8888)); + using var canvas = surface.Canvas; + foreach (var (drawingContent, position) in list) + { + if (drawingContent.DrawingPriority == DrawingPriority.Background) + { + drawingContent.DrawCore(canvas, position); + } + } + + + foreach (var (drawingContent, position) in list) + { + if (drawingContent.DrawingPriority == DrawingPriority.Foreground) + { + drawingContent.DrawCore(canvas, position); + } + } + profiler.Segment("Draw call"); + using var skImage = surface.Snapshot(); + profiler.Segment("Snapshot"); + + // const double ratio = 0.7; + // var resizedWidth = (int)(skImage.Width * ratio); + // var resizedHeight = (int)(skImage.Height * ratio); + // using var surface2 = UseGPU ? SKSurface.Create(vulkan, false, new SKImageInfo(resizedWidth, resizedHeight, SKColorType.Rgba8888)) : + // SKSurface.Create(new SKImageInfo(resizedWidth, resizedHeight, SKColorType.Rgba8888)); + // using var paint = new SKPaint(); + // paint.IsAntialias = true; + // paint.FilterQuality = SKFilterQuality.High; + // + // surface2.Canvas.DrawImage(skImage, new SKRectI(0, 0, resizedWidth, resizedHeight), + // paint); + // surface2.Canvas.Flush(); + // + // using var newImg = surface2.Snapshot(); + profiler.Segment("Resize"); + using var gl1 = skImage.Encode(SKEncodedImageFormat.Jpeg, 100); + var gl1Span = gl1.Span; + var gl = ArrayPool.Shared.Rent(gl1Span.Length); + gl1Span.CopyTo(gl); + Console.WriteLine($"图片大小: {gl1.Size.Bytes().Kilobytes:F1}KB"); + profiler.Segment("Encode"); + + return gl; + } + + static GRContext CreateVulkan() + { + if (!UseGPU) return null; + try + { + var grVkBackendContext = new GRSharpVkBackendContext(); + + Instance? _instance; + Device? _device; + + _instance = Instance.Create(Array.Empty(), Array.Empty(), applicationInfo: new ApplicationInfo() + { + }); + grVkBackendContext.VkInstance = _instance; + + var physicalDevices = _instance.EnumeratePhysicalDevices(); + PhysicalDevice? physicalDevice = null; + string? deviceName = null; + + Console.WriteLine($"All GPU(s):"); + for (var i = 0; i < physicalDevices.Length; ++i) + { + var pd = physicalDevices[i]; + var property = pd.GetProperties(); + Console.WriteLine($"GPU {i}: {property.DeviceName}"); + + // 只采用VirtualCpu独立显卡和核显 + if (property.DeviceType != PhysicalDeviceType.IntegratedGpu && + property.DeviceType != PhysicalDeviceType.DiscreteGpu && + property.DeviceType != PhysicalDeviceType.IntegratedGpu) + continue; + + // 默认不使用虚拟Gpu + if (property.DeviceType == PhysicalDeviceType.VirtualGpu && + physicalDevice != null) + continue; + physicalDevice = pd; + deviceName = property.DeviceName; + } + Console.WriteLine($"Selected GPU: {deviceName}"); + if (physicalDevice == null) + throw new Exception("Unable to find physical device"); + + grVkBackendContext.VkPhysicalDevice = physicalDevice; + + var queueFamilyProperties = physicalDevice.GetQueueFamilyProperties(); + + var families = queueFamilyProperties + .Select((properties, index) => new { properties, index }) + .Where(pair => pair.properties.QueueFlags.HasFlag(QueueFlags.Graphics)).ToArray(); + + var graphicsFamily = families + .FirstOrDefault()?.index; + + if (graphicsFamily == null) + throw new Exception("Unable to find graphics queue"); + + var queueInfos = new[] + { + new DeviceQueueCreateInfo { QueueFamilyIndex = (uint)graphicsFamily.Value, QueuePriorities = new[] { 1f } } + }; + + + _device = physicalDevice.CreateDevice(queueInfos, null, null); + + if (_device == null) + throw new Exception("Failed to create device"); + + grVkBackendContext.VkDevice = _device; + + + var graphicsQueue = _device.GetQueue((uint)graphicsFamily.Value, 0); + + grVkBackendContext.VkQueue = graphicsQueue ?? throw new Exception("Failed to get queue"); + grVkBackendContext.GraphicsQueueIndex = (uint)graphicsFamily.Value; + + grVkBackendContext.GetProcedureAddress = (name, ins, dev) => + { + IntPtr ptr; + if (dev != null) + ptr = dev.GetProcedureAddress(name); + else if (ins != null) + ptr = ins.GetProcedureAddress(name); + else + ptr = _instance.GetProcedureAddress(name); + + if (ptr == IntPtr.Zero) + Console.WriteLine($"{name} not found"); + return ptr; + }; + + var grContext = GRContext.CreateVulkan(grVkBackendContext, null); + return grContext; + } + catch (Exception e) + { + return null; + } + } + static GRContext context; + +} \ No newline at end of file diff --git a/WFBot/Features/ImageRendering/Obsolete.cs b/WFBot/Features/ImageRendering/Obsolete.cs new file mode 100644 index 0000000..3ccd154 --- /dev/null +++ b/WFBot/Features/ImageRendering/Obsolete.cs @@ -0,0 +1,65 @@ + +/* + + + + + + + public static byte[] Fissures(List fissures, int tier) + { + if (!fissures.Any()) + { + return SimpleImageRendering("目前没有裂隙."); + } + fissures = fissures.Where(x => tier == 0 || x.tierNum == tier).OrderBy(f => f.tierNum).ToList(); + var images = fissures.AsParallel().AsOrdered().Select(x => SingleFissure(x)).ToArray(); + var max = images.Max(x => x.Width); + Parallel.For(0, fissures.Count, i => + { + images[i] = StackImageXCentered(images[i], new Image(max - images[i].Width + 10, 1), + Margin100, + GetResource($"Factions.{fissures[i].enemy.ToLower()}")); + }); + + + var lineColorBool = true; + images = images.ForEach(i => i.SetBackgroundColor(SwitchLineColor(ref lineColorBool))).ToArray(); + var image = StackImageY(images); + return Finish(image); + // 我还没想到怎么给Margin上色 + } + + public static Image SingleFissure(Fissure fissure) + { + return StackImageXCentered(GetResource($"Fissures.{fissure.tierNum}"), Margin20, + StackImageY( + RenderText($"{fissure.missionType} - {fissure.enemy}", options: CreateTextOptions(43, true)), + RenderText($"{fissure.tier}(T{fissure.tierNum}) {(fissure.isHard ? "钢铁裂缝" : fissure.isStorm ? "虚空风暴" : "普通裂缝")}", CreateTextOptions(33), Color.White), + RenderText($"{fissure.node}", CreateTextOptions(33)), + RenderText($"{fissure.eta}", CreateTextOptions(33), new Color(new Rgba32(170,170,170))), Margin10)); + } + + + + + + + + + + + + + + + + + + + + + + + + */ \ No newline at end of file diff --git a/WFBot/Features/ImageRendering/RichMessage.cs b/WFBot/Features/ImageRendering/RichMessage.cs index 35ba6ef..fc590f8 100644 --- a/WFBot/Features/ImageRendering/RichMessage.cs +++ b/WFBot/Features/ImageRendering/RichMessage.cs @@ -23,6 +23,7 @@ public class TextMessage : RichMessage public class ImageMessage : RichMessage { public byte[] Content { get; set; } + public bool ShouldDispose { get; set; } = true; } public class AtMessage : RichMessage diff --git a/WFBot/Features/Utils/WFObjects.cs b/WFBot/Features/Utils/WFObjects.cs index 801ffaf..7292078 100644 --- a/WFBot/Features/Utils/WFObjects.cs +++ b/WFBot/Features/Utils/WFObjects.cs @@ -705,7 +705,7 @@ public class Component { public string uniqueName { get; set; } public string name { get; set; } -#if DEBUG +#if DEBUGA public string description { get; set; } public int itemCount { get; set; } public string imageName { get; set; } @@ -1777,7 +1777,6 @@ public class WFInvasion public class RewardInfo { - public object[] items { get; set; } public Counteditem[] countedItems { get; set; } public int credits { get; set; } public string asString { get; set; } diff --git a/WFBot/Orichalt/MiguelNetwork.cs b/WFBot/Orichalt/MiguelNetwork.cs index 1a76c6d..0493920 100644 --- a/WFBot/Orichalt/MiguelNetwork.cs +++ b/WFBot/Orichalt/MiguelNetwork.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -369,6 +370,12 @@ public static void Reply(OrichaltContext o, RichMessages msg) File.AppendAllText(resultPath, msg + Environment.NewLine); break; } + + foreach (var message in msg.Where(m => m is ImageMessage i && i.ShouldDispose)) + { + var imageMessage = (ImageMessage)message; + ArrayPool.Shared.Return(imageMessage.Content); + } } /// @@ -568,12 +575,34 @@ await Task.Delay(TimeSpan.FromSeconds(OneBotConfig.Instance.RevokeTimeInSeconds) } private static async Task OneBotSendToGroupWithAutoRevoke(GroupID group, RichMessages msg) { - var response = await OneBotCore.OneBotClient.SendGroupMessageAsync(group, msg.Select(x => x switch{ImageMessage image => SendingMessage.ByteArrayImage(image.Content), TextMessage t=> new SendingMessage(t.Content) }).Aggregate((a, b) => a + b)); + var tempPaths = new List(); + var response = await OneBotCore.OneBotClient.SendGroupMessageAsync(group, msg.Select(x => x switch + { + ImageMessage image => SendingMessage.LocalImage(ConvertImage(image.Content)), TextMessage t => new SendingMessage(t.Content) + }).Aggregate((a, b) => a + b)); + if (OneBotConfig.Instance.AutoRevoke) { await Task.Delay(TimeSpan.FromSeconds(OneBotConfig.Instance.RevokeTimeInSeconds)) .ContinueWith(t => { OneBotCore.OneBotClient.RecallMessageAsync(response); }); } + foreach (var tempPath in tempPaths) + { + try + { + File.Delete(tempPath); + } + catch (Exception e) + { + } + } + string ConvertImage(byte[] bytes) + { + var t = Path.GetTempFileName(); + tempPaths.Add(t); + File.WriteAllBytes(t, bytes); + return t; + } } private static void OneBotSendToPrivate(UserID qq, string msg) diff --git a/WFBot/Utils/Cylib.cs b/WFBot/Utils/Cylib.cs index a1b1e4e..e2afdcc 100644 --- a/WFBot/Utils/Cylib.cs +++ b/WFBot/Utils/Cylib.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using WFBot.Features.Utils; using WFBot.Orichalt; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace WFBot.Utils { @@ -146,6 +147,18 @@ public static T JsonDeserialize(this string source) return JsonConvert.DeserializeObject(source, SerializeSettings); } + public static string ToJsonStringS(this T source) + { + return JsonSerializer.Serialize(source); + } + + public static T JsonDeserializeS(this string source) + { + return JsonSerializer.Deserialize(source); + } + + + public static string ToJsonString(this T source, JsonSerializerSettings settings) { return JsonConvert.SerializeObject(source, settings); diff --git a/WFBot/WFBot.csproj b/WFBot/WFBot.csproj index 856be2e..39997f8 100644 --- a/WFBot/WFBot.csproj +++ b/WFBot/WFBot.csproj @@ -3,7 +3,7 @@ - net6.0 + net7.0 zh-CN;zh-Hans Exe false @@ -69,6 +69,9 @@ + + + @@ -95,6 +98,10 @@ + + + + Dll\PininSharp.dll