.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:
parent
448624e543
commit
7cd0026c3e
22 changed files with 7955 additions and 86 deletions
47
src/EllieBot.GrpcApiBase/protos/canvas.proto
Normal file
47
src/EllieBot.GrpcApiBase/protos/canvas.proto
Normal file
|
@ -0,0 +1,47 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "EllieBot.GrpcApi";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
package ncanvas;
|
||||
|
||||
service GrpcNCanvas {
|
||||
rpc GetCanvas(google.protobuf.Empty) returns (CanvasReply);
|
||||
rpc GetPixel(GetPixelRequest) returns (GetPixelReply);
|
||||
rpc SetPixel(SetPixelRequest) returns (SetPixelReply);
|
||||
}
|
||||
|
||||
message CanvasReply {
|
||||
repeated uint32 pixels = 1;
|
||||
int32 width = 2;
|
||||
int32 height = 3;
|
||||
}
|
||||
|
||||
message GetPixelRequest {
|
||||
int32 x = 1;
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
message GetPixelReply {
|
||||
string color = 1;
|
||||
uint32 packedColor = 2;
|
||||
int32 positionX = 3;
|
||||
int32 positionY = 4;
|
||||
int64 price = 5;
|
||||
string text = 6;
|
||||
string position = 7;
|
||||
}
|
||||
|
||||
message SetPixelRequest {
|
||||
string position = 1;
|
||||
string color = 2;
|
||||
string text = 3;
|
||||
int64 price = 4;
|
||||
}
|
||||
|
||||
message SetPixelReply {
|
||||
string error = 1;
|
||||
bool success = 2;
|
||||
optional GetPixelReply pixel = 3;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "EllieBot.GrpcApi";
|
||||
|
||||
package econ;
|
||||
|
||||
service GrpcEcon {
|
||||
rpc GetEconomy(EconomyRequest) returns (EconomyReply);
|
||||
}
|
||||
|
||||
message EconomyRequest {
|
||||
string guildId = 1;
|
||||
}
|
||||
|
||||
message EconomyReply {
|
||||
uint64 totalOwned = 1;
|
||||
uint64 byTopOnePercent = 2;
|
||||
uint64 plantedAmount = 3;
|
||||
uint64 ownedByTheBot = 4;
|
||||
uint64 inTheBank = 5;
|
||||
uint64 totalEconomy = 6;
|
||||
}
|
||||
|
||||
message CurrencyLbRequest {
|
||||
int32 page = 1;
|
||||
}
|
|
@ -73,6 +73,16 @@ public abstract class EllieContext : DbContext
|
|||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
#region NCanvas
|
||||
|
||||
modelBuilder.Entity<NCPixel>()
|
||||
.HasAlternateKey(x => x.Position);
|
||||
|
||||
modelBuilder.Entity<NCPixel>()
|
||||
.HasIndex(x => x.OwnerId);
|
||||
|
||||
#endregion
|
||||
|
||||
#region QUOTES
|
||||
|
||||
var quoteEntity = modelBuilder.Entity<Quote>();
|
||||
|
|
19
src/EllieBot/Db/Models/NCanvas/NCanvas.cs
Normal file
19
src/EllieBot/Db/Models/NCanvas/NCanvas.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class NCPixel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public required int Position { get; init; }
|
||||
|
||||
public required long Price { get; init; }
|
||||
|
||||
public required ulong OwnerId { get; init; }
|
||||
public required uint Color { get; init; }
|
||||
|
||||
[MaxLength(256)]
|
||||
public required string Text { get; init; }
|
||||
}
|
3824
src/EllieBot/Migrations/PostgreSql/20241028033704_ncanvas.Designer.cs
generated
Normal file
3824
src/EllieBot/Migrations/PostgreSql/20241028033704_ncanvas.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
54
src/EllieBot/Migrations/PostgreSql/20241028033704_ncanvas.cs
Normal file
54
src/EllieBot/Migrations/PostgreSql/20241028033704_ncanvas.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EllieBot.Migrations.PostgreSql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ncanvas : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ncpixel",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
position = table.Column<int>(type: "integer", nullable: false),
|
||||
price = table.Column<long>(type: "bigint", nullable: false),
|
||||
ownerid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
color = table.Column<long>(type: "bigint", nullable: false),
|
||||
text = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_ncpixel", x => x.id);
|
||||
table.UniqueConstraint("ak_ncpixel_position", x => x.position);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_discorduser_username",
|
||||
table: "discorduser",
|
||||
column: "username");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_ncpixel_ownerid",
|
||||
table: "ncpixel",
|
||||
column: "ownerid");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ncpixel");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_discorduser_username",
|
||||
table: "discorduser");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -798,6 +798,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_discorduser_userid");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.HasDatabaseName("ix_discorduser_username");
|
||||
|
||||
b.ToTable("discorduser", (string)null);
|
||||
});
|
||||
|
@ -1627,6 +1630,49 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("muteduserid", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.NCPixel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<long>("Color")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("color");
|
||||
|
||||
b.Property<decimal>("OwnerId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("ownerid");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("position");
|
||||
|
||||
b.Property<long>("Price")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("price");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_ncpixel");
|
||||
|
||||
b.HasAlternateKey("Position")
|
||||
.HasName("ak_ncpixel_position");
|
||||
|
||||
b.HasIndex("OwnerId")
|
||||
.HasDatabaseName("ix_ncpixel_ownerid");
|
||||
|
||||
b.ToTable("ncpixel", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
2953
src/EllieBot/Migrations/Sqlite/20241028033656_ncanvas.Designer.cs
generated
Normal file
2953
src/EllieBot/Migrations/Sqlite/20241028033656_ncanvas.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
53
src/EllieBot/Migrations/Sqlite/20241028033656_ncanvas.cs
Normal file
53
src/EllieBot/Migrations/Sqlite/20241028033656_ncanvas.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EllieBot.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ncanvas : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NCPixel",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Position = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Price = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
OwnerId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Color = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
Text = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NCPixel", x => x.Id);
|
||||
table.UniqueConstraint("AK_NCPixel_Position", x => x.Position);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DiscordUser_Username",
|
||||
table: "DiscordUser",
|
||||
column: "Username");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NCPixel_OwnerId",
|
||||
table: "NCPixel",
|
||||
column: "OwnerId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NCPixel");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DiscordUser_Username",
|
||||
table: "DiscordUser");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -596,6 +596,8 @@ namespace EllieBot.Migrations
|
|||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("DiscordUser");
|
||||
});
|
||||
|
||||
|
@ -1213,6 +1215,38 @@ namespace EllieBot.Migrations
|
|||
b.ToTable("MutedUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.NCPixel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("Color")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("OwnerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("Position");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("NCPixel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
24
src/EllieBot/Modules/Games/NCanvas/INCanvasService.cs
Normal file
24
src/EllieBot/Modules/Games/NCanvas/INCanvasService.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using EllieBot.Db.Models;
|
||||
|
||||
namespace EllieBot.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();
|
||||
}
|
290
src/EllieBot/Modules/Games/NCanvas/NCanvasCommands.cs
Normal file
290
src/EllieBot/Modules/Games/NCanvas/NCanvasCommands.cs
Normal file
|
@ -0,0 +1,290 @@
|
|||
using EllieBot.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 System.Text.RegularExpressions;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public sealed class NCanvasCommands : EllieModule
|
||||
{
|
||||
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_ELLIE
|
||||
.WithDescription("This is not available yet.")
|
||||
#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/EllieBot/Modules/Games/NCanvas/NCanvasService.cs
Normal file
206
src/EllieBot/Modules/Games/NCanvas/NCanvasService.cs
Normal file
|
@ -0,0 +1,206 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using SixLabors.ImageSharp.ColorSpaces;
|
||||
using SixLabors.ImageSharp.ColorSpaces.Conversion;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
||||
{
|
||||
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/EllieBot/Modules/Games/NCanvas/SetPixelResult.cs
Normal file
9
src/EllieBot/Modules/Games/NCanvas/SetPixelResult.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public enum SetPixelResult
|
||||
{
|
||||
Success,
|
||||
InsufficientPayment,
|
||||
NotEnoughMoney,
|
||||
InvalidInput
|
||||
}
|
|
@ -22,9 +22,6 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, IEService
|
|||
public ServerServiceDefinition Bind()
|
||||
=> GrpcExprs.BindService(this);
|
||||
|
||||
private ulong GetUserId(Metadata meta)
|
||||
=> ulong.Parse(meta.FirstOrDefault(x => x.Key == "userid")!.Value);
|
||||
|
||||
public override async Task<AddExprReply> AddExpr(AddExprRequest request, ServerCallContext context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Expr.Trigger) || string.IsNullOrWhiteSpace(request.Expr.Response))
|
||||
|
@ -109,7 +106,7 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, IEService
|
|||
|
||||
public override async Task<AddQuoteReply> AddQuote(AddQuoteRequest request, ServerCallContext context)
|
||||
{
|
||||
var userId = GetUserId(context.RequestHeaders);
|
||||
var userId = context.RequestHeaders.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Quote.Trigger) || string.IsNullOrWhiteSpace(request.Quote.Response))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required"));
|
||||
|
@ -146,7 +143,7 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, IEService
|
|||
|
||||
public override async Task<Empty> DeleteQuote(DeleteQuoteRequest request, ServerCallContext context)
|
||||
{
|
||||
await _qs.DeleteQuoteAsync(request.GuildId, GetUserId(context.RequestHeaders), true, new kwum(request.Id));
|
||||
await _qs.DeleteQuoteAsync(request.GuildId, context.RequestHeaders.GetUserId(), true, new kwum(request.Id));
|
||||
return new Empty();
|
||||
}
|
||||
}
|
95
src/EllieBot/Services/GrpcApi/NCanvasSvc.cs
Normal file
95
src/EllieBot/Services/GrpcApi/NCanvasSvc.cs
Normal file
|
@ -0,0 +1,95 @@
|
|||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Games;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace EllieBot.GrpcApi;
|
||||
|
||||
public class NCanvasSvc : GrpcNCanvas.GrpcNCanvasBase, IGrpcSvc, IEService
|
||||
{
|
||||
private readonly INCanvasService _nCanvas;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public NCanvasSvc(INCanvasService nCanvas, DiscordSocketClient client)
|
||||
{
|
||||
_nCanvas = nCanvas;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public ServerServiceDefinition Bind()
|
||||
=> GrpcNCanvas.BindService(this);
|
||||
|
||||
[GrpcNoAuthRequired]
|
||||
public override async Task<CanvasReply> GetCanvas(Empty request, ServerCallContext context)
|
||||
{
|
||||
var pixels = await _nCanvas.GetCanvas();
|
||||
var reply = new CanvasReply()
|
||||
{
|
||||
Width = _nCanvas.GetWidth(),
|
||||
Height = _nCanvas.GetHeight()
|
||||
};
|
||||
reply.Pixels.AddRange(pixels);
|
||||
return reply;
|
||||
}
|
||||
|
||||
[GrpcNoAuthRequired]
|
||||
public override async Task<GetPixelReply> GetPixel(GetPixelRequest request, ServerCallContext context)
|
||||
{
|
||||
var pixel = await _nCanvas.GetPixel(request.X, request.Y);
|
||||
if (pixel is null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "Pixel not found"));
|
||||
|
||||
var reply = MapPixelToGrpcPixel(pixel);
|
||||
return reply;
|
||||
}
|
||||
|
||||
private GetPixelReply MapPixelToGrpcPixel(NCPixel pixel)
|
||||
{
|
||||
var reply = new GetPixelReply
|
||||
{
|
||||
Color = "#" + new Rgba32(pixel.Color).ToHex(),
|
||||
PackedColor = pixel.Color,
|
||||
Position = new kwum(pixel.Position).ToString(),
|
||||
PositionX = pixel.Position % _nCanvas.GetWidth(),
|
||||
PositionY = pixel.Position / _nCanvas.GetWidth(),
|
||||
// Owner = await ((IDiscordClient)_client).GetUserAsync(pixel.OwnerId)?.ToString() ?? string.Empty,
|
||||
// OwnerId = pixel.OwnerId.ToString(),
|
||||
Price = pixel.Price,
|
||||
Text = pixel.Text
|
||||
};
|
||||
return reply;
|
||||
}
|
||||
|
||||
[GrpcNoAuthRequired]
|
||||
public override async Task<SetPixelReply> SetPixel(SetPixelRequest request, ServerCallContext context)
|
||||
{
|
||||
if (!kwum.TryParse(request.Position, out var pos))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Position is invalid"));
|
||||
|
||||
if (!Rgba32.TryParseHex(request.Color, out var clr))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Color is invalid"));
|
||||
|
||||
var userId = context.RequestHeaders.GetUserId();
|
||||
var result = await _nCanvas.SetPixel(pos, clr.PackedValue, request.Text, userId, request.Price);
|
||||
var reply = new SetPixelReply()
|
||||
{
|
||||
Success = result == SetPixelResult.Success,
|
||||
Error = result switch
|
||||
{
|
||||
SetPixelResult.Success => string.Empty,
|
||||
SetPixelResult.InsufficientPayment => "You have to pay equal or more than the price.",
|
||||
SetPixelResult.NotEnoughMoney => "You don't have enough currency. ",
|
||||
SetPixelResult.InvalidInput =>
|
||||
$"Invalid input. Position has to be >= 0 and < {_nCanvas.GetWidth()}x{_nCanvas.GetHeight()}",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
}
|
||||
};
|
||||
|
||||
var pixel = await _nCanvas.GetPixel(pos);
|
||||
if (pixel is not null)
|
||||
reply.Pixel = MapPixelToGrpcPixel(pixel);
|
||||
|
||||
return reply;
|
||||
}
|
||||
}
|
9
src/EllieBot/Services/SvcExtensions.cs
Normal file
9
src/EllieBot/Services/SvcExtensions.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using Grpc.Core;
|
||||
|
||||
namespace EllieBot.GrpcApi;
|
||||
|
||||
public static class SvcExtensions
|
||||
{
|
||||
public static ulong GetUserId(this Metadata meta)
|
||||
=> ulong.Parse(meta.FirstOrDefault(x => x.Key == "userid")!.Value);
|
||||
}
|
|
@ -76,6 +76,9 @@ public readonly struct kwum : IEquatable<kwum>
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_value == 0)
|
||||
return VALID_CHARACTERS[0].ToString();
|
||||
|
||||
var count = VALID_CHARACTERS.Length;
|
||||
var localValue = _value;
|
||||
var arrSize = (int)Math.Log(localValue, count) + 1;
|
||||
|
|
|
@ -1424,4 +1424,24 @@ afk:
|
|||
keep:
|
||||
- keep
|
||||
leaveunkeptservers:
|
||||
- leaveunkeptservers
|
||||
- leaveunkeptservers
|
||||
ncanvas:
|
||||
- ncanvas
|
||||
- nc
|
||||
- ncanv
|
||||
ncsetimg:
|
||||
- ncsetimg
|
||||
- ncsi
|
||||
ncsetpixel:
|
||||
- ncsetpixel
|
||||
- ncsp
|
||||
- ncs
|
||||
nczoom:
|
||||
- nczoom
|
||||
- ncz
|
||||
ncpixel:
|
||||
- ncpixel
|
||||
- ncp
|
||||
- ncgp
|
||||
ncreset:
|
||||
- ncreset
|
|
@ -383,7 +383,7 @@
|
|||
"Aliases": [
|
||||
".keep"
|
||||
],
|
||||
"Description": "The current serve, won't be deleted from Ellie's database during the purge.",
|
||||
"Description": "The current server, won't be deleted from Ellie's database during the purge.",
|
||||
"Usage": [
|
||||
".keep"
|
||||
],
|
||||
|
@ -394,6 +394,21 @@
|
|||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".leaveunkeptservers"
|
||||
],
|
||||
"Description": "Leaves all servers whose owners didn't run .keep",
|
||||
"Usage": [
|
||||
".leaveunkeptservers"
|
||||
],
|
||||
"Submodule": "CleanupCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".sqlselect"
|
||||
|
@ -586,13 +601,11 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".greetdel",
|
||||
".grdel"
|
||||
".greet"
|
||||
],
|
||||
"Description": "Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set it to `0` to disable automatic deletion.",
|
||||
"Description": "Toggles announcements on the current channel when someone joins the server.",
|
||||
"Usage": [
|
||||
".greetdel 0",
|
||||
".greetdel 30"
|
||||
".greet"
|
||||
],
|
||||
"Submodule": "GreetCommands",
|
||||
"Module": "Administration",
|
||||
|
@ -603,11 +616,13 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".greet"
|
||||
".greetdel",
|
||||
".grdel"
|
||||
],
|
||||
"Description": "Toggles announcements on the current channel when someone joins the server.",
|
||||
"Description": "Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set it to `0` to disable automatic deletion.",
|
||||
"Usage": [
|
||||
".greet"
|
||||
".greetdel 0",
|
||||
".greetdel 30"
|
||||
],
|
||||
"Submodule": "GreetCommands",
|
||||
"Module": "Administration",
|
||||
|
@ -676,21 +691,6 @@
|
|||
"ManageServer Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".byemsg"
|
||||
],
|
||||
"Description": "Sets a new leave announcement message which will be shown in the current channel. \nUsing this command with no message will show the current bye message. \nSupports [placeholders](https://docs.elliebot.net/ellie/features/placeholders/) and [embeds](https://eb.elliebot.net/)",
|
||||
"Usage": [
|
||||
".byemsg %user.name% has left."
|
||||
],
|
||||
"Submodule": "GreetCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"ManageServer Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".byedel"
|
||||
|
@ -709,12 +709,11 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".byetest"
|
||||
".byemsg"
|
||||
],
|
||||
"Description": "Sends the bye message in the current channel as if you just left the server. You can optionally specify a different user.",
|
||||
"Description": "Sets a new leave announcement message which will be shown in the current channel. \nUsing this command with no message will show the current bye message. \nSupports [placeholders](https://docs.elliebot.net/ellie/features/placeholders/) and [embeds](https://eb.elliebot.net/)",
|
||||
"Usage": [
|
||||
".byetest",
|
||||
".byetest @SomeoneElse"
|
||||
".byemsg %user.name% has left."
|
||||
],
|
||||
"Submodule": "GreetCommands",
|
||||
"Module": "Administration",
|
||||
|
@ -755,6 +754,22 @@
|
|||
"ManageServer Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".byetest"
|
||||
],
|
||||
"Description": "Sends the bye message in the current channel as if you just left the server. You can optionally specify a different user.",
|
||||
"Usage": [
|
||||
".byetest",
|
||||
".byetest @SomeoneElse"
|
||||
],
|
||||
"Submodule": "GreetCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"ManageServer Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".boosttest"
|
||||
|
@ -1090,7 +1105,7 @@
|
|||
"Aliases": [
|
||||
".antialt"
|
||||
],
|
||||
"Description": "Applies a punishment action to any user whose account is younger than the specified threshold. Specify time after the punishment to have a timed punishment (not all punishments support timers).",
|
||||
"Description": "Applies a punishment action to any user whose account is younger than the specified threshold. \nAvailable Punishments are: Ban, Kick, Softban, Mute, VoiceMute, ChatMute, RemoveRoles, AddRole, Warn, TimeOut\nYou can specify an additional time argument to do a timed punishment for actions which support it (Ban, Mute, etc) up to 24h.\nMax message count is 10.\nProvide no parameters to disable.",
|
||||
"Usage": [
|
||||
".antialt 1h Ban",
|
||||
".antialt 3d Mute 1h"
|
||||
|
@ -1106,7 +1121,7 @@
|
|||
"Aliases": [
|
||||
".antiraid"
|
||||
],
|
||||
"Description": "Sets an anti-raid protection on the server. Provide no parameters to disable. First parameter is number of people which will trigger the protection. Second parameter is a time interval in which that number of people needs to join in order to trigger the protection, and third parameter is punishment for those people. You can specify an additional time argument to do a timed punishment for actions which support it (Ban, Mute, etc) up to 24h. Available punishments: Ban, Kick, Softban, Mute, VoiceMute, ChatMute, RemoveRoles, AddRole, Warn, TimeOut",
|
||||
"Description": "Sets an anti-raid protection on the server.\n\nFirst parameter is number of people which will trigger the protection.\n\nSecond parameter is a time interval in which that number of people needs to join in order to trigger the protection.\n\nThird parameter is punishment for those people.\nAvailable punishments: Ban, Kick, Softban, Mute, VoiceMute, ChatMute, RemoveRoles, AddRole, Warn, TimeOut\nYou can specify an additional time argument to do a timed punishment for actions which support it (Ban, Mute, etc) up to 24h.\n\nProvide no parameters to disable.",
|
||||
"Usage": [
|
||||
".antiraid 5 20 Kick",
|
||||
".antiraid 7 9 Ban",
|
||||
|
@ -1124,7 +1139,7 @@
|
|||
"Aliases": [
|
||||
".antispam"
|
||||
],
|
||||
"Description": "Stops people from repeating same message X times in a row. Provide no parameters to disable. You can specify to either mute, kick or ban the offenders. You can specify an additional time argument to do a timed punishment for actions which support it (Ban, Mute, etc) up to 24h. Max message count is 10. Available punishments: Ban, Kick, Softban, Mute, VoiceMute, ChatMute, AddRole, RemoveRoles, Warn, TimeOut",
|
||||
"Description": "Applies a Punishment to people who repeat the same message X times in a row.\nAvailable Punishments are: Ban, Kick, Softban, Mute, VoiceMute, ChatMute, RemoveRoles, AddRole, Warn, TimeOut\nYou can specify an additional time argument to do a timed punishment for actions which support it (Ban, Mute, etc) up to 24h.\nMax message count is 10. \nProvide no parameters to disable.",
|
||||
"Usage": [
|
||||
".antispam 3 Mute",
|
||||
".antispam 5 Ban",
|
||||
|
@ -1939,13 +1954,14 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".setactivity",
|
||||
".setgame"
|
||||
],
|
||||
"Description": "Sets the bots game status to either Playing, Listening, or Watching.",
|
||||
"Description": "Sets the bots game status to a Custom, Playing, Listening, or Watching status.",
|
||||
"Usage": [
|
||||
".setgame Playing with snakes.",
|
||||
".setgame Watching anime.",
|
||||
".setgame Listening music."
|
||||
".setactivity Just chilling",
|
||||
".setactivity Playing with canaries",
|
||||
".setactivity Listening music"
|
||||
],
|
||||
"Submodule": "SelfCommands",
|
||||
"Module": "Administration",
|
||||
|
@ -3674,7 +3690,7 @@
|
|||
"Aliases": [
|
||||
".choose"
|
||||
],
|
||||
"Description": "Chooses a thing from a list of things",
|
||||
"Description": "Chooses a thing from a list of things. Separate items with a semicolon ;",
|
||||
"Usage": [
|
||||
".choose Get up;Sleep;Sleep more"
|
||||
],
|
||||
|
@ -3772,6 +3788,98 @@
|
|||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".ncanvas",
|
||||
".nc",
|
||||
".ncanv"
|
||||
],
|
||||
"Description": "Shows the current nCanvas.\nThe canvas allows users to set each pixel's color and text using currency.",
|
||||
"Usage": [
|
||||
".ncanvas"
|
||||
],
|
||||
"Submodule": "NCanvasCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".nczoom",
|
||||
".ncz"
|
||||
],
|
||||
"Description": "Zooms in on the nCanvas.\nBot will show the 10x10 grid with the position of each cell for use with `ncset`.\nYou can either use alphanumeric position (ex. s4u) or pixel x and y (ex. 123 123)",
|
||||
"Usage": [
|
||||
".nczoom sgu",
|
||||
".nczoom 123 123"
|
||||
],
|
||||
"Submodule": "NCanvasCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".ncsetpixel",
|
||||
".ncsp",
|
||||
".ncs"
|
||||
],
|
||||
"Description": "Sets a pixel's color and text on the nCanvas.\nYou must specify the position of the pixel to set in alphanumeric format.\nYou can obtain alphanumeric position of the pixel by using `nczoom` or `ncp <x> <y>`",
|
||||
"Usage": [
|
||||
".ncsetpixel sgu #ff0000 Some text"
|
||||
],
|
||||
"Submodule": "NCanvasCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".ncpixel",
|
||||
".ncp",
|
||||
".ncgp"
|
||||
],
|
||||
"Description": "Shows the pixel at the specified position.\nYou can get pixel positions by using `nczoom`",
|
||||
"Usage": [
|
||||
".ncpixel sgu",
|
||||
".ncpixel 123 123"
|
||||
],
|
||||
"Submodule": "NCanvasCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".ncsetimg",
|
||||
".ncsi"
|
||||
],
|
||||
"Description": "Attach the image to the message sending the command to overwrite the nCanvas with it.\nAll prices and colors will be reset.\nThe image must be equal to the size of the nCanvas (default is 500x350)\nThis command is dangerous and irreversible.",
|
||||
"Usage": [
|
||||
".ncsetimg"
|
||||
],
|
||||
"Submodule": "NCanvasCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".ncreset"
|
||||
],
|
||||
"Description": "Clears the nCanvas.\nAll prices and colors will be reset.\nThis command is dangerous and irreversible.",
|
||||
"Usage": [
|
||||
".ncreset"
|
||||
],
|
||||
"Submodule": "NCanvasCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".nunchi"
|
||||
|
@ -5171,7 +5279,7 @@
|
|||
],
|
||||
"Description": "Toggles whether a module can be used on any server.",
|
||||
"Usage": [
|
||||
".globalmodule nsfw"
|
||||
".globalmodule Gambling"
|
||||
],
|
||||
"Submodule": "GlobalPermissionCommands",
|
||||
"Module": "Permissions",
|
||||
|
@ -6297,11 +6405,11 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".listservers"
|
||||
".serverlist"
|
||||
],
|
||||
"Description": "Lists servers the bot is on with some basic info. 15 per page.",
|
||||
"Usage": [
|
||||
".listservers 3"
|
||||
".serverlist 3"
|
||||
],
|
||||
"Submodule": "Utility",
|
||||
"Module": "Utility",
|
||||
|
@ -6719,13 +6827,15 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".listquotes",
|
||||
".liqu"
|
||||
".quotelist",
|
||||
".qli",
|
||||
".quli",
|
||||
".qulist"
|
||||
],
|
||||
"Description": "Lists all quotes on the server ordered alphabetically or by ID. 15 Per page.",
|
||||
"Usage": [
|
||||
".listquotes 3",
|
||||
".listquotes 3 id"
|
||||
".quotelist 3",
|
||||
".quotelist 3 id"
|
||||
],
|
||||
"Submodule": "QuoteCommands",
|
||||
"Module": "Utility",
|
||||
|
@ -6735,7 +6845,10 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quoteprint",
|
||||
"..."
|
||||
".qp",
|
||||
".qup",
|
||||
"...",
|
||||
".qprint"
|
||||
],
|
||||
"Description": "Prints a random quote with a specified name.",
|
||||
"Usage": [
|
||||
|
@ -6749,7 +6862,9 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quoteshow",
|
||||
".qshow"
|
||||
".qsh",
|
||||
".qshow",
|
||||
".qushow"
|
||||
],
|
||||
"Description": "Shows information about a quote with the specified ID.",
|
||||
"Usage": [
|
||||
|
@ -6763,6 +6878,7 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quotesearch",
|
||||
".qse",
|
||||
".qsearch"
|
||||
],
|
||||
"Description": "Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author",
|
||||
|
@ -6782,7 +6898,7 @@
|
|||
".quoteid",
|
||||
".qid"
|
||||
],
|
||||
"Description": "Displays the quote with the specified ID number. Quote ID numbers can be found by typing `.liqu [num]` where `[num]` is a number of a page which contains 15 quotes.",
|
||||
"Description": "-| Displays the quote with the specified ID number.",
|
||||
"Usage": [
|
||||
".quoteid 123456"
|
||||
],
|
||||
|
@ -6794,6 +6910,9 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quoteadd",
|
||||
".qa",
|
||||
".qadd",
|
||||
".quadd",
|
||||
".."
|
||||
],
|
||||
"Description": "Adds a new quote with the specified name and message.",
|
||||
|
@ -6808,6 +6927,8 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quoteedit",
|
||||
".qe",
|
||||
".que",
|
||||
".qedit"
|
||||
],
|
||||
"Description": "Edits a quote with the specified ID.",
|
||||
|
@ -6822,7 +6943,9 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quotedelete",
|
||||
".qdel"
|
||||
".qd",
|
||||
".qdel",
|
||||
".qdelete"
|
||||
],
|
||||
"Description": "Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it.",
|
||||
"Usage": [
|
||||
|
@ -6836,6 +6959,7 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quotedeleteauthor",
|
||||
".qda",
|
||||
".qdelauth"
|
||||
],
|
||||
"Description": "Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required.",
|
||||
|
@ -6849,13 +6973,13 @@
|
|||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".delallquotes",
|
||||
".daq",
|
||||
".delallq"
|
||||
".quotesdeleteall",
|
||||
".qdall",
|
||||
".qdeleteall"
|
||||
],
|
||||
"Description": "Deletes all quotes on a specified keyword.",
|
||||
"Description": "Deletes all quotes with the specified keyword.",
|
||||
"Usage": [
|
||||
".delallquotes kek"
|
||||
".quotesdeleteall kek"
|
||||
],
|
||||
"Submodule": "QuoteCommands",
|
||||
"Module": "Utility",
|
||||
|
@ -6867,6 +6991,7 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quotesexport",
|
||||
".qex",
|
||||
".qexport"
|
||||
],
|
||||
"Description": "Exports quotes from the current server into a .yml file",
|
||||
|
@ -6883,6 +7008,8 @@
|
|||
{
|
||||
"Aliases": [
|
||||
".quotesimport",
|
||||
".qim",
|
||||
".qimp",
|
||||
".qimport"
|
||||
],
|
||||
"Description": "Upload the file or send the raw .yml data with this command to import all quotes from the specified string or file into the current server.",
|
||||
|
|
|
@ -4567,4 +4567,70 @@ leaveunkeptservers:
|
|||
- shardId:
|
||||
desc: "Shard id from which to start leaving unkept servers."
|
||||
- delay:
|
||||
desc: "Delay in miliseconds between leaves"
|
||||
desc: "Delay in miliseconds between leaves"
|
||||
ncanvas:
|
||||
desc: |-
|
||||
Shows the current nCanvas.
|
||||
The canvas allows users to set each pixel's color and text using currency.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
nczoom:
|
||||
desc: |-
|
||||
Zooms in on the nCanvas.
|
||||
Bot will show the 10x10 grid with the position of each cell for use with `ncset`.
|
||||
You can either use alphanumeric position (ex. s4u) or pixel x and y (ex. 123 123)
|
||||
ex:
|
||||
- 'sgu'
|
||||
- '123 123'
|
||||
params:
|
||||
- position:
|
||||
desc: "The position of the pixel to set in alphanumeric format."
|
||||
- position:
|
||||
desc: "The position of the pixel to set in pixel x and y format."
|
||||
ncsetpixel:
|
||||
desc: |-
|
||||
Sets a pixel's color and text on the nCanvas.
|
||||
You must specify the position of the pixel to set in alphanumeric format.
|
||||
You can obtain alphanumeric position of the pixel by using `nczoom` or `ncp <x> <y>`
|
||||
ex:
|
||||
- 'sgu #ff0000 Some text'
|
||||
params:
|
||||
- position:
|
||||
desc: "The position of the pixel to set in alphanumeric format."
|
||||
- color:
|
||||
desc: "The color of the pixel to set in HEX."
|
||||
- text:
|
||||
desc: "The optional text to set on the pixel."
|
||||
ncsetimg:
|
||||
desc: |-
|
||||
Attach the image to the message sending the command to overwrite the nCanvas with it.
|
||||
All prices and colors will be reset.
|
||||
The image must be equal to the size of the nCanvas (default is 500x350)
|
||||
This command is dangerous and irreversible.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
ncpixel:
|
||||
desc: |-
|
||||
Shows the pixel at the specified position.
|
||||
You can get pixel positions by using `nczoom`
|
||||
ex:
|
||||
- 'sgu'
|
||||
- '123 123'
|
||||
params:
|
||||
- position:
|
||||
desc: "The position of the pixel to retrieve in alphanumeric format."
|
||||
- position:
|
||||
desc: "The position of the pixel to retrieve in pixel x and y format."
|
||||
ncreset:
|
||||
desc: |-
|
||||
Clears the nCanvas.
|
||||
All prices and colors will be reset.
|
||||
This command is dangerous and irreversible.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
|
@ -1101,5 +1101,14 @@
|
|||
"honeypot_on": "Honeypot enabled on this channel.",
|
||||
"honeypot_off": "Honeypot disabled.",
|
||||
"afk_set": "AFK message set. Type a message in any channel to clear.",
|
||||
"rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel."
|
||||
"rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel.",
|
||||
"nc_pixel_not_found": "Pixel not found",
|
||||
"nc_pixel": "Pixel {0}",
|
||||
"nc_position": "Position",
|
||||
"nc_pixel_set": "Pixel {0} successfully set!",
|
||||
"invalid_color": "Color you've specified is invalid.",
|
||||
"nc_pixel_set_confirm": "Are you sure you want to set pixel {0}? It will cost you {1}",
|
||||
"nc_hint": "Use `{0}nczoom x y` command to zoom in. Image is {1}x{2} pixels.",
|
||||
"invalid_img_size": "Image must to be {0}x{1} pixels.",
|
||||
"no_attach_found": "No attachment found. Please send the image along with this command."
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue