re-added .xpex and .xpexl commands as there was no way to exclude users and roles from the xp system anymore

This commit is contained in:
Toastie 2025-03-19 11:22:58 +13:00
parent ca46786c5e
commit 1d667db598
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
19 changed files with 631 additions and 169 deletions

View file

@ -1,14 +0,0 @@
namespace EllieBot.Db.Models;
public class ExcludedItem : DbEntity
{
public int? XpSettingsId { get; set; }
public ulong ItemId { get; set; }
public ExcludedItemType ItemType { get; set; }
public override int GetHashCode()
=> ItemId.GetHashCode() ^ ItemType.GetHashCode();
public override bool Equals(object? obj)
=> obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType;
}

View file

@ -1,3 +1,7 @@
namespace EllieBot.Db.Models;
public enum ExcludedItemType { Channel, Role }
public enum XpExcludedItemType
{
User,
Role
}

View file

@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace EllieBot.Db.Models;
public class XpExcludedItem
{
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; }
public XpExcludedItemType ItemType { get; set; }
public ulong ItemId { get; set; }
}
public sealed class XpExclusionEntityConfig : IEntityTypeConfiguration<XpExcludedItem>
{
public void Configure(EntityTypeBuilder<XpExcludedItem> builder)
{
builder.HasIndex(x => x.GuildId);
builder.HasAlternateKey(x => new
{
x.GuildId,
x.ItemType,
x.ItemId
});
}
}

View file

@ -0,0 +1,17 @@
START TRANSACTION;
CREATE TABLE xpexcludeditem (
id integer GENERATED BY DEFAULT AS IDENTITY,
guildid numeric(20,0) NOT NULL,
itemtype integer NOT NULL,
itemid numeric(20,0) NOT NULL,
CONSTRAINT pk_xpexcludeditem PRIMARY KEY (id),
CONSTRAINT ak_xpexcludeditem_guildid_itemtype_itemid UNIQUE (guildid, itemtype, itemid)
);
CREATE INDEX ix_xpexcludeditem_guildid ON xpexcludeditem (guildid);
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
VALUES ('20250318221943_xpexclusion', '9.0.1');
COMMIT;

View file

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace EllieBot.Migrations.PostgreSql
{
[DbContext(typeof(PostgreSqlContext))]
[Migration("20250317063309_init")]
[Migration("20250318222207_init")]
partial class init
{
/// <inheritdoc />
@ -3263,6 +3263,39 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("xpcurrencyreward", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<decimal>("ItemId")
.HasColumnType("numeric(20,0)")
.HasColumnName("itemid");
b.Property<int>("ItemType")
.HasColumnType("integer")
.HasColumnName("itemtype");
b.HasKey("Id")
.HasName("pk_xpexcludeditem");
b.HasAlternateKey("GuildId", "ItemType", "ItemId")
.HasName("ak_xpexcludeditem_guildid_itemtype_itemid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_xpexcludeditem_guildid");
b.ToTable("xpexcludeditem", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{
b.Property<int>("Id")

View file

@ -1179,6 +1179,22 @@ namespace EllieBot.Migrations.PostgreSql
table.PrimaryKey("pk_warnings", x => x.id);
});
migrationBuilder.CreateTable(
name: "xpexcludeditem",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
itemtype = table.Column<int>(type: "integer", nullable: false),
itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_xpexcludeditem", x => x.id);
table.UniqueConstraint("ak_xpexcludeditem_guildid_itemtype_itemid", x => new { x.guildid, x.itemtype, x.itemid });
});
migrationBuilder.CreateTable(
name: "xpsettings",
columns: table => new
@ -2280,6 +2296,11 @@ namespace EllieBot.Migrations.PostgreSql
table: "xpcurrencyreward",
column: "xpsettingsid");
migrationBuilder.CreateIndex(
name: "ix_xpexcludeditem_guildid",
table: "xpexcludeditem",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_xprolereward_xpsettingsid_level",
table: "xprolereward",
@ -2574,6 +2595,9 @@ namespace EllieBot.Migrations.PostgreSql
migrationBuilder.DropTable(
name: "xpcurrencyreward");
migrationBuilder.DropTable(
name: "xpexcludeditem");
migrationBuilder.DropTable(
name: "xprolereward");

View file

@ -3260,6 +3260,39 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("xpcurrencyreward", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<decimal>("ItemId")
.HasColumnType("numeric(20,0)")
.HasColumnName("itemid");
b.Property<int>("ItemType")
.HasColumnType("integer")
.HasColumnName("itemtype");
b.HasKey("Id")
.HasName("pk_xpexcludeditem");
b.HasAlternateKey("GuildId", "ItemType", "ItemId")
.HasName("ak_xpexcludeditem_guildid_itemtype_itemid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_xpexcludeditem_guildid");
b.ToTable("xpexcludeditem", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{
b.Property<int>("Id")

View file

@ -0,0 +1,16 @@
BEGIN TRANSACTION;
CREATE TABLE "XpExcludedItem" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_XpExcludedItem" PRIMARY KEY AUTOINCREMENT,
"GuildId" INTEGER NOT NULL,
"ItemType" INTEGER NOT NULL,
"ItemId" INTEGER NOT NULL,
CONSTRAINT "AK_XpExcludedItem_GuildId_ItemType_ItemId" UNIQUE ("GuildId", "ItemType", "ItemId")
);
CREATE INDEX "IX_XpExcludedItem_GuildId" ON "XpExcludedItem" ("GuildId");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20250318221922_xpexclusion', '9.0.1');
COMMIT;

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace EllieBot.Migrations.Sqlite
{
[DbContext(typeof(SqliteContext))]
[Migration("20250317063300_init")]
[Migration("20250318222152_init")]
partial class init
{
/// <inheritdoc />
@ -2428,6 +2428,30 @@ namespace EllieBot.Migrations.Sqlite
b.ToTable("XpCurrencyReward");
});
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("ItemType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "ItemType", "ItemId");
b.HasIndex("GuildId");
b.ToTable("XpExcludedItem");
});
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{
b.Property<int>("Id")

View file

@ -1181,6 +1181,22 @@ namespace EllieBot.Migrations.Sqlite
table.PrimaryKey("PK_Warnings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "XpExcludedItem",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ItemType = table.Column<int>(type: "INTEGER", nullable: false),
ItemId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_XpExcludedItem", x => x.Id);
table.UniqueConstraint("AK_XpExcludedItem_GuildId_ItemType_ItemId", x => new { x.GuildId, x.ItemType, x.ItemId });
});
migrationBuilder.CreateTable(
name: "XpSettings",
columns: table => new
@ -2282,6 +2298,11 @@ namespace EllieBot.Migrations.Sqlite
table: "XpCurrencyReward",
column: "XpSettingsId");
migrationBuilder.CreateIndex(
name: "IX_XpExcludedItem_GuildId",
table: "XpExcludedItem",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_XpRoleReward_XpSettingsId_Level",
table: "XpRoleReward",
@ -2576,6 +2597,9 @@ namespace EllieBot.Migrations.Sqlite
migrationBuilder.DropTable(
name: "XpCurrencyReward");
migrationBuilder.DropTable(
name: "XpExcludedItem");
migrationBuilder.DropTable(
name: "XpRoleReward");

View file

@ -2425,6 +2425,30 @@ namespace EllieBot.Migrations.Sqlite
b.ToTable("XpCurrencyReward");
});
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("ItemType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "ItemType", "ItemId");
b.HasIndex("GuildId");
b.ToTable("XpExcludedItem");
});
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{
b.Property<int>("Id")

View file

@ -0,0 +1,84 @@
using EllieBot.Db.Models;
namespace EllieBot.Modules.Xp;
public partial class Xp
{
[RequireUserPermission(GuildPermission.Administrator)]
public class XpExclusionCommands : EllieModule<XpExclusionService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task XpExclusion()
{
var exclusions = await _service.GetExclusionsAsync(ctx.Guild.Id);
if (!exclusions.Any())
{
await Response().Pending(strs.xp_exclusion_none).SendAsync();
return;
}
await Response()
.Paginated()
.Items(exclusions.OrderBy(x => x.ItemType).ToList())
.PageSize(10)
.Page((items, _) =>
{
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.xp_exclusion_title));
foreach (var item in items)
{
var itemType = item.ItemType;
var mention = GetMention(itemType, item.ItemId);
eb.AddField(itemType.ToString(), mention);
}
return eb;
})
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task XpExclude([Leftover] IRole role)
=> await XpExclude(XpExcludedItemType.Role, role.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task XpExclude([Leftover] IUser user)
=> await XpExclude(XpExcludedItemType.User, user.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task XpExclude(XpExcludedItemType type, ulong itemId)
{
var isExcluded = await _service.ToggleExclusionAsync(ctx.Guild.Id, type, itemId);
if (isExcluded)
await Response()
.Confirm(strs.xp_exclude_added(type.ToString(), GetMention(type, itemId)))
.SendAsync();
else
await Response()
.Confirm(strs.xp_exclude_removed(type.ToString(), GetMention(type, itemId)))
.SendAsync();
}
private string GetMention(XpExcludedItemType itemType, ulong itemId)
=> itemType switch
{
XpExcludedItemType.Role => ctx.Guild.GetRole(itemId)?.ToString() ?? itemId.ToString(),
XpExcludedItemType.User => (ctx.Guild as SocketGuild)?.GetUser(itemId)?.ToString() ??
itemId.ToString(),
_ => itemId.ToString()
};
}
}

View file

@ -0,0 +1,99 @@
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using LinqToDB;
using Microsoft.EntityFrameworkCore;
namespace EllieBot.Modules.Xp;
public class XpExclusionService(DbService db, ShardData shardData) : IReadyExecutor, IEService
{
private ConcurrentHashSet<(ulong GuildId, XpExcludedItemType ItemType, ulong ItemId)> _exclusions = new();
public async Task OnReadyAsync()
{
await using var uow = db.GetDbContext();
_exclusions = await uow.GetTable<XpExcludedItem>()
.Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
.ToListAsyncLinqToDB()
.Fmap(x => x
.Select(x => (x.GuildId, x.ItemType, x.ItemId))
.ToHashSet()
.ToConcurrentSet());
}
/// <summary>
/// Toggles exclusion for the specified item. If the item was excluded, it will be included
/// and vice versa.
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="itemType">Type of the item to toggle exclusion for</param>
/// <param name="itemId">ID of the item to toggle exclusion for</param>
/// <returns>True if the item is now excluded, false if it's no longer excluded</returns>
public async Task<bool> ToggleExclusionAsync(ulong guildId, XpExcludedItemType itemType, ulong itemId)
{
var key = (guildId, itemType, itemId);
var isExcluded = false;
await using var uow = db.GetDbContext();
if (_exclusions.Contains(key))
{
isExcluded = false;
// item exists, remove it
await uow.GetTable<XpExcludedItem>()
.Where(x => x.GuildId == guildId
&& x.ItemType == itemType
&& x.ItemId == itemId)
.DeleteAsync();
_exclusions.TryRemove(key);
}
else
{
isExcluded = true;
// item doesn't exist, add it
await uow.GetTable<XpExcludedItem>()
.InsertOrUpdateAsync(() => new XpExcludedItem
{
GuildId = guildId,
ItemType = itemType,
ItemId = itemId
},
_ => new(),
() => new XpExcludedItem
{
GuildId = guildId,
ItemType = itemType,
ItemId = itemId
});
_exclusions.Add(key);
}
return isExcluded;
}
/// <summary>
/// Gets a list of all excluded items for a guild.
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <returns>List of excluded items in the guild</returns>
public async Task<IReadOnlyList<XpExcludedItem>> GetExclusionsAsync(ulong guildId)
{
await using var uow = db.GetDbContext();
return await uow.GetTable<XpExcludedItem>()
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.ToListAsyncLinqToDB();
}
/// <summary>
/// Checks if the specified item is excluded from XP gain.
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="itemType">Type of the item</param>
/// <param name="itemId">ID of the item</param>
/// <returns>True if the item is excluded, otherwise false</returns>
public bool IsExcluded(ulong guildId, XpExcludedItemType itemType, ulong itemId)
=> _exclusions.Contains((guildId, itemType, itemId));
}

View file

@ -9,7 +9,7 @@ namespace EllieBot.Modules.Xp;
using GuildXpRates = (IReadOnlyList<GuildXpConfig> GuildRates, IReadOnlyList<ChannelXpConfig> ChannelRates);
public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigService xcs) : IReadyExecutor, IEService
public class XpRateService(DbService db, ShardData shardData, XpConfigService xcs) : IReadyExecutor, IEService
{
private ConcurrentDictionary<(XpRateType RateType, ulong GuildId), XpRate> _guildRates = new();
private ConcurrentDictionary<ulong, ConcurrentDictionary<(XpRateType, ulong), XpRate>> _channelRates = new();

View file

@ -3,7 +3,7 @@ namespace EllieBot.Modules.Xp;
public partial class Xp
{
[RequireUserPermission(GuildPermission.ManageGuild)]
public class XpRateCommands : EllieModule<GuildConfigXpService>
public class XpRateCommands : EllieModule<XpRateService>
{
[Cmd]
[RequireContext(ContextType.Guild)]

View file

@ -40,7 +40,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private readonly INotifySubscriber _notifySub;
private readonly IMemoryCache _memCache;
private readonly XpTemplateService _templateService;
private readonly GuildConfigXpService _xpRateService;
private readonly XpRateService _xpRateRateService;
private readonly XpExclusionService _xpExcl;
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
@ -60,7 +61,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
IMemoryCache memCache,
ShardData shardData,
XpTemplateService templateService,
GuildConfigXpService xpRateService)
XpRateService xpRateRateService,
XpExclusionService xpExcl
)
{
_db = db;
_images = images;
@ -72,7 +75,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
_notifySub = notifySub;
_memCache = memCache;
_templateService = templateService;
_xpRateService = xpRateService;
_xpRateRateService = xpRateRateService;
_xpExcl = xpExcl;
_client = client;
_ps = ps;
_c = c;
@ -143,7 +147,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (!IsVoiceChannelActive(vc))
continue;
var rate = _xpRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
var rate = _xpRateRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
if (rate.IsExcluded())
continue;
@ -153,6 +157,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (!UserParticipatingInVoiceChannel(u))
continue;
if (IsUserExcluded(g, u))
continue;
if (oldBatch.Contains(u))
{
validUsers.Add(new(u, rate.Amount, vc.Id));
@ -509,15 +516,18 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
_ = Task.Run(async () =>
{
if (IsUserExcluded(guild, user))
return;
var isImage = arg.Attachments.Any(a => a.Height >= 16 && a.Width >= 16);
var isText = arg.Content.Contains(' ') || arg.Content.Length >= 5;
var textRate = _xpRateService.GetXpRate(XpRateType.Text, guild.Id, gc.Id);
var textRate = _xpRateRateService.GetXpRate(XpRateType.Text, guild.Id, gc.Id);
XpRate rate;
if (isImage)
{
var imageRate = _xpRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
var imageRate = _xpRateRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
if (imageRate.IsExcluded())
return;
@ -544,6 +554,20 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
return Task.CompletedTask;
}
private bool IsUserExcluded(IGuild guild, SocketGuildUser user)
{
if (_xpExcl.IsExcluded(guild.Id, XpExcludedItemType.User, user.Id))
return true;
foreach (var role in user.Roles)
{
if (_xpExcl.IsExcluded(guild.Id, XpExcludedItemType.Role, role.Id))
return true;
}
return false;
}
public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
{
await using var ctx = _db.GetDbContext();
@ -614,49 +638,54 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
}
var avatarUrl = stats.User.RealAvatarUrl();
byte[] avatarImageData = null;
if (avatarUrl is not null)
var avatarFetchTask = Task.Run(async () =>
{
var result = await _c.GetImageDataAsync(avatarUrl);
if (!result.TryPickT0(out avatarImageData, out _))
try
{
using (var http = _httpFactory.CreateClient())
{
var avatarData = await http.GetByteArrayAsync(avatarUrl);
using (var tempDraw = Image.Load<Rgba32>(avatarData))
{
tempDraw.Mutate(x => x
.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
.ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
template.User.Icon.Size.Y)
/ 2.0f));
await using (var stream = await tempDraw.ToStreamAsync())
{
avatarImageData = stream.ToArray();
}
}
}
if (avatarUrl is null)
return null;
await _c.SetImageDataAsync(avatarUrl, avatarImageData);
var result = await _c.GetImageDataAsync(avatarUrl);
if (result.TryPickT0(out var imgData, out _))
return imgData;
using var http = _httpFactory.CreateClient();
var avatarData = await http.GetByteArrayAsync(avatarUrl);
using var tempDraw = Image.Load<Rgba32>(avatarData);
tempDraw.Mutate(x => x
.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
.ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
template.User.Icon.Size.Y)
/ 2.0f));
await using var stream = await tempDraw.ToStreamAsync();
var data = stream.ToArray();
await _c.SetImageDataAsync(avatarUrl, data);
return data;
}
}
catch (Exception)
{
return null;
}
});
using var img = Image.Load<Rgba32>(bgBytes);
if (template.User.Name.Show)
img.Mutate(x =>
{
var fontSize = (int)(template.User.Name.FontSize * 0.9);
var username = stats.User.ToString();
var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
var scale = 400f / size.Width;
if (scale < 1)
usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
img.Mutate(x =>
if (template.User.Name.Show)
{
var fontSize = (int)(template.User.Name.FontSize * 0.9);
var username = stats.User.ToString();
var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
var scale = 400f / size.Width;
if (scale < 1)
usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
x.DrawText(new RichTextOptions(usernameFont)
{
HorizontalAlignment = HorizontalAlignment.Left,
@ -666,126 +695,129 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
},
"@" + username,
Brushes.Solid(template.User.Name.Color));
}
//club name
//club name
if (template.Club.Name.Show)
if (template.Club.Name.Show)
{
var clubName = stats.User.Club?.ToString() ?? "-";
var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
x.DrawText(new RichTextOptions(clubFont)
{
var clubName = stats.User.Club?.ToString() ?? "-";
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
},
clubName,
Brushes.Solid(template.Club.Name.Color));
}
var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
Font GetTruncatedFont(
FontFamily fontFamily,
int fontSize,
FontStyle style,
string text,
int maxSize)
{
var font = fontFamily.CreateFont(fontSize, style);
var size = TextMeasurer.MeasureSize(text, new(font));
var scale = maxSize / size.Width;
if (scale < 1)
font = fontFamily.CreateFont(fontSize * scale, style);
x.DrawText(new RichTextOptions(clubFont)
return font;
}
if (template.User.Level.Show)
{
var guildLevelFont = GetTruncatedFont(
_fonts.NotoSans,
template.User.Level.FontSize,
FontStyle.Bold,
stats.Guild.Level.ToString(),
33);
x.DrawText(stats.Guild.Level.ToString(),
guildLevelFont,
template.User.Level.Color,
new(template.User.Level.Pos.X, template.User.Level.Pos.Y));
}
var guild = stats.Guild;
//xp bar
if (template.User.Xp.Bar.Show)
{
var xpPercent = guild.LevelXp / (float)guild.RequiredXp;
DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
}
if (template.User.Xp.Guild.Show)
{
x.DrawText(
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
FontStyle.Bold))
{
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
},
clubName,
Brushes.Solid(template.Club.Name.Color));
}
$"{guild.LevelXp}/{guild.RequiredXp}",
Brushes.Solid(template.User.Xp.Guild.Color));
}
Font GetTruncatedFont(
FontFamily fontFamily,
int fontSize,
FontStyle style,
string text,
int maxSize)
{
var font = fontFamily.CreateFont(fontSize, style);
var size = TextMeasurer.MeasureSize(text, new(font));
var scale = maxSize / size.Width;
if (scale < 1)
font = fontFamily.CreateFont(fontSize * scale, style);
var rankPen = new SolidPen(Color.White, 1);
//ranking
return font;
}
if (template.User.Rank.Show)
{
var guildRankStr = stats.GuildRanking.ToString();
var guildRankFont = GetTruncatedFont(
_fonts.NotoSans,
template.User.Rank.FontSize,
FontStyle.Bold,
guildRankStr,
22);
if (template.User.Level.Show)
{
var guildLevelFont = GetTruncatedFont(
_fonts.NotoSans,
template.User.Level.FontSize,
FontStyle.Bold,
stats.Guild.Level.ToString(),
33);
x.DrawText(stats.Guild.Level.ToString(),
guildLevelFont,
template.User.Level.Color,
new(template.User.Level.Pos.X, template.User.Level.Pos.Y));
}
var guild = stats.Guild;
//xp bar
if (template.User.Xp.Bar.Show)
{
var xpPercent = guild.LevelXp / (float)guild.RequiredXp;
DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
}
if (template.User.Xp.Guild.Show)
{
x.DrawText(
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
FontStyle.Bold))
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
},
$"{guild.LevelXp}/{guild.RequiredXp}",
Brushes.Solid(template.User.Xp.Guild.Color));
}
var rankPen = new SolidPen(Color.White, 1);
//ranking
if (template.User.Rank.Show)
{
var guildRankStr = stats.GuildRanking.ToString();
var guildRankFont = GetTruncatedFont(
_fonts.NotoSans,
template.User.Rank.FontSize,
FontStyle.Bold,
guildRankStr,
22);
x.DrawText(
new RichTextOptions(guildRankFont)
{
Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
},
guildRankStr,
Brushes.Solid(template.User.Rank.Color),
rankPen
);
}
if (template.User.Icon.Show)
{
try
x.DrawText(
new RichTextOptions(guildRankFont)
{
using var toDraw = Image.Load(avatarImageData);
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
toDraw.Mutate(x
=> x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
},
guildRankStr,
Brushes.Solid(template.User.Rank.Color),
rankPen
);
}
});
x.DrawImage(toDraw,
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
1);
}
catch (Exception ex)
{
Log.Warning(ex, "Error drawing avatar image");
}
if (template.User.Icon.Show)
{
var avImageData = await avatarFetchTask;
img.Mutate(mut =>
{
try
{
using var toDraw = Image.Load(avImageData);
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
toDraw.Mutate(x => x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
mut.DrawImage(toDraw,
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
1);
}
catch (Exception ex)
{
Log.Warning(ex, "Error drawing avatar image");
}
});
}

View file

@ -1626,4 +1626,10 @@ scheduledelete:
scheduleadd:
- scheduleadd
- scha
- schadd
- schadd
xpexclusion:
- xpexclusion
- xpexl
xpexclude:
- xpexclude
- xpex

View file

@ -1854,7 +1854,7 @@ playlistload:
- 5
params:
- id:
desc: "The id of the playlist to be loaded."
desc: "The id of the playlist to be loaded."
playlistsave:
desc: Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes.
ex:
@ -5100,4 +5100,25 @@ scheduleadd:
- time:
desc: "How long it takes for the command to execute. Example: 1h30m = 1 hour and 30 minutes"
- command:
desc: "Command that will be executed after the specified time has elapsed"
desc: "Command that will be executed after the specified time has elapsed"
xpexclusion:
desc: |-
Shows a list of all XP exclusions in the server.
ex:
- ''
params:
- { }
xpexclude:
desc: |-
Toggles XP gain exclusion for a specified item.
Item types can be Role or User.
ex:
- '@CoolRole'
- '@User'
- 'role 123123123'
- 'user 123123123'
params:
- type:
desc: "Type of the item to exclude: role or user"
itemId:
desc: "ID or mention of the item to exclude."

View file

@ -1217,5 +1217,9 @@
"schedule_command": "Command",
"schedule_when": "Executes At",
"schedule_add_success": "Scheduled command successfully added.",
"schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more."
"schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more.",
"xp_exclusion_none": "There are no exclusions set for this server.",
"xp_exclusion_title": "XP Exclusions",
"xp_exclude_added": "{0}: {1} has been excluded from the XP system.",
"xp_exclude_removed": "{0}: {1} is no longer excluded from the XP system."
}