mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 02:08:27 -04:00
Added .nc and related commands.
You can set pixel colors (and text) on a 500x350 canvas, pepega version of r/place You use currency to set pixels. see whole canvas: .nc set pixel: .ncsp <pos> <color> <text?> get pixel: .ncp <pos> zoom: .ncz <pos> or .ncz x y
This commit is contained in:
24
src/NadekoBot/Modules/Games/NCanvas/INCanvasService.cs
Normal file
24
src/NadekoBot/Modules/Games/NCanvas/INCanvasService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public interface INCanvasService
|
||||
{
|
||||
Task<uint[]> GetCanvas();
|
||||
Task<NCPixel[]> GetPixelGroup(int position);
|
||||
|
||||
Task<SetPixelResult> SetPixel(
|
||||
int position,
|
||||
uint color,
|
||||
string text,
|
||||
ulong userId,
|
||||
long price);
|
||||
|
||||
Task<bool> SetImage(uint[] img);
|
||||
|
||||
Task<NCPixel?> GetPixel(int x, int y);
|
||||
Task<NCPixel?> GetPixel(int position);
|
||||
int GetHeight();
|
||||
int GetWidth();
|
||||
Task ResetAsync();
|
||||
}
|
289
src/NadekoBot/Modules/Games/NCanvas/NCanvasCommands.cs
Normal file
289
src/NadekoBot/Modules/Games/NCanvas/NCanvasCommands.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public sealed class NCanvasCommands : NadekoModule
|
||||
{
|
||||
private readonly INCanvasService _service;
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly FontProvider _fonts;
|
||||
private readonly GamblingConfigService _gcs;
|
||||
|
||||
public NCanvasCommands(
|
||||
INCanvasService service,
|
||||
IHttpClientFactory http,
|
||||
FontProvider fonts,
|
||||
GamblingConfigService gcs)
|
||||
{
|
||||
_service = service;
|
||||
_http = http;
|
||||
_fonts = fonts;
|
||||
_gcs = gcs;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task NCanvas()
|
||||
{
|
||||
var pixels = await _service.GetCanvas();
|
||||
var image = new Image<Rgba32>(_service.GetWidth(), _service.GetHeight());
|
||||
|
||||
Parallel.For(0,
|
||||
image.Height,
|
||||
y =>
|
||||
{
|
||||
var pixelAccessor = image.DangerousGetPixelRowMemory(y);
|
||||
var row = pixelAccessor.Span;
|
||||
for (int x = 0; x < image.Width; x++)
|
||||
{
|
||||
row[x] = new Rgba32(pixels[(y * image.Width) + x]);
|
||||
}
|
||||
});
|
||||
|
||||
await using var stream = await image.ToStreamAsync();
|
||||
|
||||
var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight()));
|
||||
await Response()
|
||||
.File(stream, "ncanvas.png")
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
#if GLOBAL_NADEKO
|
||||
.WithDescription("https://dashy.nadeko.bot/ncanvas")
|
||||
#endif
|
||||
.WithFooter(hint)
|
||||
.WithImageUrl("attachment://ncanvas.png"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public Task NCzoom(int row, int col)
|
||||
=> NCzoom((col * _service.GetWidth()) + row);
|
||||
|
||||
[Cmd]
|
||||
public async Task NCzoom(kwum position)
|
||||
{
|
||||
var w = _service.GetWidth();
|
||||
var h = _service.GetHeight();
|
||||
|
||||
if (position < 0 || position >= w * h)
|
||||
{
|
||||
await Response().Error(strs.invalid_input).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
using var img = await GetZoomImage(position);
|
||||
await using var stream = await img.ToStreamAsync();
|
||||
await ctx.Channel.SendFileAsync(stream, $"zoom_{position}.png");
|
||||
}
|
||||
|
||||
private async Task<Image<Rgba32>> GetZoomImage(kwum position)
|
||||
{
|
||||
var w = _service.GetWidth();
|
||||
var pixels = await _service.GetPixelGroup(position);
|
||||
|
||||
var origX = ((position % w) - 2) * 100;
|
||||
var origY = ((position / w) - 2) * 100;
|
||||
|
||||
var image = new Image<Rgba32>(500, 500);
|
||||
|
||||
const float fontSize = 30;
|
||||
|
||||
var posFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
|
||||
var size = TextMeasurer.MeasureSize("wwww", new TextOptions(posFont));
|
||||
var scale = 100f / size.Width;
|
||||
if (scale < 1)
|
||||
posFont = _fonts.NotoSans.CreateFont(fontSize * scale, FontStyle.Bold);
|
||||
var outlinePen = new SolidPen(SixLabors.ImageSharp.Color.Black, 1f);
|
||||
|
||||
Parallel.For(0,
|
||||
pixels.Length,
|
||||
i =>
|
||||
{
|
||||
var pix = pixels[i];
|
||||
var startX = pix.Position % w * 100 - origX;
|
||||
var startY = pix.Position / w * 100 - origY;
|
||||
|
||||
var color = new Rgba32(pix.Color);
|
||||
image.Mutate(x => FillRectangleExtensions.Fill(x,
|
||||
new SolidBrush(color),
|
||||
new RectangleF(startX, startY, 100, 100)));
|
||||
|
||||
image.Mutate(x =>
|
||||
{
|
||||
x.DrawText(new RichTextOptions(posFont)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(startX + 50, startY + 50)
|
||||
},
|
||||
((kwum)pix.Position).ToString().PadLeft(2, '2'),
|
||||
Brushes.Solid(SixLabors.ImageSharp.Color.White),
|
||||
outlinePen);
|
||||
});
|
||||
});
|
||||
|
||||
// write the position on each section of the image
|
||||
return image;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task NcSetPixel(kwum position, string colorHex, [Leftover] string text = "")
|
||||
{
|
||||
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
|
||||
{
|
||||
await Response().Error(strs.invalid_input).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (colorHex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
colorHex = colorHex[2..];
|
||||
|
||||
if (!Rgba32.TryParseHex(colorHex, out var clr))
|
||||
{
|
||||
await Response().Error(strs.invalid_color).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var pixel = await _service.GetPixel(position);
|
||||
if (pixel is null)
|
||||
{
|
||||
await Response().Error(strs.nc_pixel_not_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var prompt = GetText(strs.nc_pixel_set_confirm(Format.Code(position.ToString()),
|
||||
Format.Bold(CurrencyHelper.N(pixel.Price,
|
||||
Culture,
|
||||
_gcs.Data.Currency.Sign))));
|
||||
|
||||
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription(prompt)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _service.SetPixel(position, clr.PackedValue, text, ctx.User.Id, pixel.Price);
|
||||
|
||||
using var img = await GetZoomImage(position);
|
||||
await using var stream = await img.ToStreamAsync();
|
||||
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
|
||||
.WithImageUrl($"attachment://zoom_{position}.png"))
|
||||
.File(stream, $"zoom_{position}.png")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task NcPixel(int x, int y)
|
||||
=> await NcPixel((y * _service.GetWidth()) + x);
|
||||
|
||||
[Cmd]
|
||||
public async Task NcPixel(kwum position)
|
||||
{
|
||||
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
|
||||
{
|
||||
await Response().Error(strs.invalid_input).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var pixel = await _service.GetPixel(position);
|
||||
if (pixel is null)
|
||||
{
|
||||
await Response().Error(strs.nc_pixel_not_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var image = new Image<Rgba32>(100, 100);
|
||||
image.Mutate(x
|
||||
=> x.Fill(new SolidBrush(new Rgba32(pixel.Color)),
|
||||
new RectangleF(0, 0, 100, 100)));
|
||||
|
||||
await using var stream = await image.ToStreamAsync();
|
||||
|
||||
var pos = new kwum(pixel.Position);
|
||||
await Response()
|
||||
.File(stream, $"{pixel.Position}.png")
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
|
||||
.WithTitle(GetText(strs.nc_pixel(pos)))
|
||||
.AddField(GetText(strs.nc_position),
|
||||
$"{pixel.Position % _service.GetWidth()} {pixel.Position / _service.GetWidth()}",
|
||||
true)
|
||||
.AddField(GetText(strs.price), pixel.Price.ToString(), true)
|
||||
.AddField(GetText(strs.color), "#" + new Rgba32(pixel.Color).ToHex())
|
||||
.WithImageUrl($"attachment://{pixel.Position}.png"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task NcSetImg()
|
||||
{
|
||||
var attach = ctx.Message.Attachments.FirstOrDefault();
|
||||
if (attach is null)
|
||||
{
|
||||
await Response().Error(strs.no_attach_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var w = _service.GetWidth();
|
||||
var h = _service.GetHeight();
|
||||
if (attach.Width != w || attach.Height != h)
|
||||
{
|
||||
await Response().Error(strs.invalid_img_size(w, h)).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||
.WithDescription(
|
||||
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
|
||||
+ "Are you sure you want to continue?")))
|
||||
return;
|
||||
|
||||
using var http = _http.CreateClient();
|
||||
await using var stream = await http.GetStreamAsync(attach.Url);
|
||||
using var img = await Image.LoadAsync<Rgba32>(stream);
|
||||
|
||||
var pixels = new uint[_service.GetWidth() * _service.GetHeight()];
|
||||
|
||||
Parallel.For(0,
|
||||
_service.GetWidth() * _service.GetHeight(),
|
||||
i => pixels[i] = img[i % _service.GetWidth(), i / _service.GetWidth()].PackedValue);
|
||||
|
||||
// for (var y = 0; y < _service.GetHeight(); y++)
|
||||
// for (var x = 0; x < _service.GetWidth(); x++)
|
||||
// pixels[(y * _service.GetWidth()) + x] = img[x, y].PackedValue;
|
||||
|
||||
await _service.SetImage(pixels);
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task NcReset()
|
||||
{
|
||||
await _service.ResetAsync();
|
||||
|
||||
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||
.WithDescription(
|
||||
"This will delete all pixels and reset the canvas.\n\n"
|
||||
+ "Are you sure you want to continue?")))
|
||||
return;
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
}
|
||||
}
|
206
src/NadekoBot/Modules/Games/NCanvas/NCanvasService.cs
Normal file
206
src/NadekoBot/Modules/Games/NCanvas/NCanvasService.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
using SixLabors.ImageSharp.ColorSpaces;
|
||||
using SixLabors.ImageSharp.ColorSpaces.Conversion;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class NCanvasService : INCanvasService, IReadyExecutor, INService
|
||||
{
|
||||
private readonly TypedKey<uint[]> _canvasKey = new("ncanvas");
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ICurrencyService _cs;
|
||||
|
||||
public const int CANVAS_WIDTH = 500;
|
||||
public const int CANVAS_HEIGHT = 350;
|
||||
public const int INITIAL_PRICE = 10;
|
||||
|
||||
public NCanvasService(
|
||||
DbService db,
|
||||
IBotCache cache,
|
||||
DiscordSocketClient client,
|
||||
ICurrencyService cs)
|
||||
{
|
||||
_db = db;
|
||||
_cache = cache;
|
||||
_client = client;
|
||||
_cs = cs;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
if (_client.ShardId != 0)
|
||||
return;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
if (await uow.GetTable<NCPixel>().CountAsyncLinqToDB() > 0)
|
||||
return;
|
||||
|
||||
await ResetAsync();
|
||||
}
|
||||
|
||||
public async Task ResetAsync()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<NCPixel>().DeleteAsync();
|
||||
|
||||
var toAdd = new List<int>();
|
||||
for (var i = 0; i < CANVAS_WIDTH * CANVAS_HEIGHT; i++)
|
||||
{
|
||||
toAdd.Add(i);
|
||||
}
|
||||
|
||||
await uow.GetTable<NCPixel>()
|
||||
.BulkCopyAsync(toAdd.Select(x =>
|
||||
{
|
||||
var clr = ColorSpaceConverter.ToRgb(new Hsv(((float)Random.Shared.NextDouble() * 360),
|
||||
(float)(0.5 + (Random.Shared.NextDouble() * 0.49)),
|
||||
(float)(0.4 + (Random.Shared.NextDouble() / 5 + (x % 100 * 0.2)))))
|
||||
.ToVector3();
|
||||
|
||||
var packed = new Rgba32(clr).PackedValue;
|
||||
return new NCPixel()
|
||||
{
|
||||
Color = packed,
|
||||
Price = 1,
|
||||
Position = x,
|
||||
Text = "",
|
||||
OwnerId = 0
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
private async Task<uint[]> InternalGetCanvas()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var colors = await uow.GetTable<NCPixel>()
|
||||
.OrderBy(x => x.Position)
|
||||
.Select(x => x.Color)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
public async Task<uint[]> GetCanvas()
|
||||
{
|
||||
return await _cache.GetOrAddAsync(_canvasKey,
|
||||
async () => await InternalGetCanvas(),
|
||||
TimeSpan.FromSeconds(15))
|
||||
?? [];
|
||||
}
|
||||
|
||||
public async Task<SetPixelResult> SetPixel(
|
||||
int position,
|
||||
uint color,
|
||||
string text,
|
||||
ulong userId,
|
||||
long price)
|
||||
{
|
||||
if (position < 0 || position >= CANVAS_WIDTH * CANVAS_HEIGHT)
|
||||
return SetPixelResult.InvalidInput;
|
||||
|
||||
var wallet = await _cs.GetWalletAsync(userId);
|
||||
|
||||
var paid = await wallet.Take(price, new("canvas", "pixel", $"Bought pixel #{position}"));
|
||||
if (!paid)
|
||||
{
|
||||
return SetPixelResult.NotEnoughMoney;
|
||||
}
|
||||
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var updates = await uow.GetTable<NCPixel>()
|
||||
.Where(x => x.Position == position && x.Price <= price)
|
||||
.UpdateAsync(old => new NCPixel()
|
||||
{
|
||||
Position = position,
|
||||
Color = color,
|
||||
Text = text,
|
||||
OwnerId = userId,
|
||||
Price = price + 1
|
||||
});
|
||||
success = updates > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel #{position} purchase"));
|
||||
}
|
||||
|
||||
return success ? SetPixelResult.Success : SetPixelResult.InsufficientPayment;
|
||||
}
|
||||
|
||||
public async Task<bool> SetImage(uint[] colors)
|
||||
{
|
||||
if (colors.Length != CANVAS_WIDTH * CANVAS_HEIGHT)
|
||||
return false;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<NCPixel>().DeleteAsync();
|
||||
await uow.GetTable<NCPixel>()
|
||||
.BulkCopyAsync(colors.Select((x, i) => new NCPixel()
|
||||
{
|
||||
Color = x,
|
||||
Price = INITIAL_PRICE,
|
||||
Position = i,
|
||||
Text = "",
|
||||
OwnerId = 0
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<NCPixel?> GetPixel(int x, int y)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(x);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(y);
|
||||
|
||||
if (x >= CANVAS_WIDTH || y >= CANVAS_HEIGHT)
|
||||
return Task.FromResult<NCPixel?>(null);
|
||||
|
||||
return GetPixel(x + (y * CANVAS_WIDTH));
|
||||
}
|
||||
|
||||
public async Task<NCPixel?> GetPixel(int position)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(position);
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.GetTable<NCPixel>().FirstOrDefaultAsync(x => x.Position == position);
|
||||
}
|
||||
|
||||
public async Task<NCPixel[]> GetPixelGroup(int position)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(position);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(position, CANVAS_WIDTH * CANVAS_HEIGHT);
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.GetTable<NCPixel>()
|
||||
.Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2
|
||||
&& x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2
|
||||
&& x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2
|
||||
&& x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2)
|
||||
.OrderBy(x => x.Position)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
}
|
||||
|
||||
public int GetHeight()
|
||||
=> CANVAS_HEIGHT;
|
||||
|
||||
public int GetWidth()
|
||||
=> CANVAS_WIDTH;
|
||||
}
|
9
src/NadekoBot/Modules/Games/NCanvas/SetPixelResult.cs
Normal file
9
src/NadekoBot/Modules/Games/NCanvas/SetPixelResult.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public enum SetPixelResult
|
||||
{
|
||||
Success,
|
||||
InsufficientPayment,
|
||||
NotEnoughMoney,
|
||||
InvalidInput
|
||||
}
|
Reference in New Issue
Block a user