finalized rewritten xp loop, updated xp.yml
This commit is contained in:
parent
dbc312dd9d
commit
06970eb9d3
15 changed files with 487 additions and 526 deletions
src/EllieBot
Modules/Xp
Services/GrpcApi
_common/Abstractions/Cache
data
14
src/EllieBot/Modules/Xp/Db/UserXpBatch.cs
Normal file
14
src/EllieBot/Modules/Xp/Db/UserXpBatch.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
#nullable disable warnings
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EllieBot.Modules.Xp.Services;
|
||||
|
||||
public sealed class UserXpBatch
|
||||
{
|
||||
[Key]
|
||||
public ulong UserId { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string AvatarId { get; set; } = string.Empty;
|
||||
}
|
|
@ -31,11 +31,13 @@ public partial class Xp : EllieModule<XpService>
|
|||
|
||||
private readonly DownloadTracker _tracker;
|
||||
private readonly ICurrencyProvider _gss;
|
||||
private readonly XpTemplateService _templateService;
|
||||
|
||||
public Xp(DownloadTracker tracker, ICurrencyProvider gss)
|
||||
public Xp(DownloadTracker tracker, ICurrencyProvider gss, XpTemplateService templateService)
|
||||
{
|
||||
_tracker = tracker;
|
||||
_gss = gss;
|
||||
_templateService = templateService;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -325,7 +327,7 @@ public partial class Xp : EllieModule<XpService>
|
|||
[OwnerOnly]
|
||||
public async Task XpTemplateReload()
|
||||
{
|
||||
_service.ReloadXpTemplate();
|
||||
_templateService.ReloadXpTemplate();
|
||||
await Task.Delay(1000);
|
||||
await Response().Confirm(strs.template_reloaded).SendAsync();
|
||||
}
|
||||
|
|
|
@ -10,25 +10,19 @@ namespace EllieBot.Modules.Xp;
|
|||
public sealed partial class XpConfig : ICloneable<XpConfig>
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; } = 7;
|
||||
public int Version { get; set; } = 10;
|
||||
|
||||
[Comment("""How much XP will the users receive per message""")]
|
||||
public int XpPerMessage { get; set; } = 3;
|
||||
public int TextXpPerMessage { get; set; } = 3;
|
||||
|
||||
[Comment("""How often can the users receive XP, in seconds""")]
|
||||
public float MessageXpCooldown { get; set; } = 300;
|
||||
public int TextXpCooldown { get; set; } = 300;
|
||||
|
||||
[Comment("""Amount of xp users gain from posting an image""")]
|
||||
public int XpFromImage { get; set; } = 0;
|
||||
public int TextXpFromImage { get; set; } = 3;
|
||||
|
||||
[Comment("""Average amount of xp earned per minute in VC""")]
|
||||
public double VoiceXpPerMinute { get; set; } = 0;
|
||||
|
||||
[Comment("""The maximum amount of minutes the bot will keep track of a user in a voice channel""")]
|
||||
public int VoiceMaxMinutes { get; set; } = 720;
|
||||
|
||||
[Comment("""The amount of currency users will receive for each point of global xp that they earn""")]
|
||||
public float CurrencyPerXp { get; set; } = 0;
|
||||
public int VoiceXpPerMinute { get; set; } = 3;
|
||||
|
||||
[Comment("""Xp Shop config""")]
|
||||
public ShopConfig Shop { get; set; } = new();
|
||||
|
@ -82,7 +76,8 @@ public sealed partial class XpConfig : ICloneable<XpConfig>
|
|||
[Comment("""Visible name of the item""")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Comment("""Price of the item. Set to -1 if you no longer want to sell the item but want the users to be able to keep their old purchase""")]
|
||||
[Comment(
|
||||
"""Price of the item. Set to -1 if you no longer want to sell the item but want the users to be able to keep their old purchase""")]
|
||||
public int Price { get; set; }
|
||||
|
||||
[Comment("""Direct url to the .png image which will be applied to the user's XP card""")]
|
||||
|
|
|
@ -15,24 +15,29 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig>
|
|||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
AddParsedProp("txt.cooldown",
|
||||
conf => conf.MessageXpCooldown,
|
||||
float.TryParse,
|
||||
conf => conf.TextXpCooldown,
|
||||
int.TryParse,
|
||||
(f) => f.ToString("F2"),
|
||||
x => x > 0);
|
||||
AddParsedProp("txt.per_msg", conf => conf.XpPerMessage, int.TryParse, ConfigPrinters.ToString, x => x >= 0);
|
||||
AddParsedProp("txt.per_image", conf => conf.XpFromImage, int.TryParse, ConfigPrinters.ToString, x => x > 0);
|
||||
|
||||
AddParsedProp("voice.per_minute",
|
||||
conf => conf.VoiceXpPerMinute,
|
||||
double.TryParse,
|
||||
AddParsedProp("txt.permsg",
|
||||
conf => conf.TextXpPerMessage,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
x => x >= 0);
|
||||
AddParsedProp("voice.max_minutes",
|
||||
conf => conf.VoiceMaxMinutes,
|
||||
|
||||
AddParsedProp("txt.perimage",
|
||||
conf => conf.TextXpFromImage,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
x => x > 0);
|
||||
|
||||
AddParsedProp("voice.perminute",
|
||||
conf => conf.VoiceXpPerMinute,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
x => x >= 0);
|
||||
|
||||
AddParsedProp("shop.is_enabled",
|
||||
conf => conf.Shop.IsEnabled,
|
||||
bool.TryParse,
|
||||
|
@ -43,21 +48,11 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig>
|
|||
|
||||
private void Migrate()
|
||||
{
|
||||
if (data.Version < 2)
|
||||
if (data.Version < 10)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 2;
|
||||
c.XpFromImage = 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 7)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 7;
|
||||
c.MessageXpCooldown *= 60;
|
||||
c.Version = 10;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#nullable disable warnings
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LinqToDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
|
@ -11,12 +10,13 @@ using SixLabors.ImageSharp.Drawing.Processing;
|
|||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Threading.Channels;
|
||||
using LinqToDB.Data;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using LinqToDB.Tools;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using EllieBot.Modules.Administration;
|
||||
using EllieBot.Modules.Patronage;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Exception = System.Exception;
|
||||
|
@ -42,12 +42,11 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
private XpTemplate _template = new();
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
private readonly TypedKey<bool> _xpTemplateReloadKey;
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly IBotCache _c;
|
||||
|
||||
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
|
||||
private readonly INotifySubscriber _notifySub;
|
||||
private readonly IMemoryCache _memCache;
|
||||
private readonly ShardData _shardData;
|
||||
|
||||
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
|
||||
|
@ -65,6 +64,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
IPubSub pubSub,
|
||||
IPatronageService ps,
|
||||
INotifySubscriber notifySub,
|
||||
IMemoryCache memCache,
|
||||
ShardData shardData)
|
||||
{
|
||||
_db = db;
|
||||
|
@ -76,44 +76,22 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
_xpConfig = xpConfig;
|
||||
_pubSub = pubSub;
|
||||
_notifySub = notifySub;
|
||||
_memCache = memCache;
|
||||
_shardData = shardData;
|
||||
_excludedServers = new();
|
||||
_excludedServers = [];
|
||||
_excludedChannels = new();
|
||||
_client = client;
|
||||
_xpTemplateReloadKey = new("xp.template.reload");
|
||||
_ps = ps;
|
||||
_c = c;
|
||||
|
||||
InternalReloadXpTemplate();
|
||||
|
||||
if (client.ShardId == 0)
|
||||
{
|
||||
_pubSub.Sub(_xpTemplateReloadKey,
|
||||
_ =>
|
||||
{
|
||||
InternalReloadXpTemplate();
|
||||
return default;
|
||||
});
|
||||
}
|
||||
|
||||
//load settings
|
||||
#if !GLOBAL_ELLIE
|
||||
_client.UserVoiceStateUpdated += Client_OnUserVoiceStateUpdated;
|
||||
|
||||
// Scan guilds on startup.
|
||||
_client.GuildAvailable += Client_OnGuildAvailable;
|
||||
foreach (var guild in _client.Guilds)
|
||||
Client_OnGuildAvailable(guild);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
// initialize ignored
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(_xpConfig.Data.MessageXpCooldown,
|
||||
1,
|
||||
nameof(_xpConfig.Data.MessageXpCooldown));
|
||||
|
||||
await using (var ctx = _db.GetDbContext())
|
||||
{
|
||||
var xps = await ctx.GetTable<XpSettings>()
|
||||
|
@ -136,7 +114,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(UpdateTimer(), _levelUpQueue.RunAsync());
|
||||
await Task.WhenAll(UpdateTimer(), VoiceUpdateTimer(), _levelUpQueue.RunAsync());
|
||||
|
||||
return;
|
||||
|
||||
|
@ -158,7 +136,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
async Task UpdateTimer()
|
||||
{
|
||||
// todo a bigger loop that runs once every XpTimer
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
{
|
||||
|
@ -178,29 +155,50 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
/// <summary>
|
||||
/// The current batch of users that will gain xp
|
||||
/// </summary>
|
||||
private readonly ConcurrentHashSet<IGuildUser> _usersBatch = new();
|
||||
private readonly ConcurrentHashSet<IGuildUser> _usersBatch = [];
|
||||
|
||||
private readonly ConcurrentHashSet<IGuildUser> _voiceXPBatch = new();
|
||||
/// <summary>
|
||||
/// The current batch of users that will gain voice xp
|
||||
/// </summary>
|
||||
private readonly ConcurrentHashSet<IGuildUser> _voiceXpBatch = [];
|
||||
|
||||
private async Task UpdateVoiceXp()
|
||||
{
|
||||
var xpAmount = (int)_xpConfig.Data.VoiceXpPerMinute;
|
||||
var oldBatch = _voiceXPBatch.ToArray();
|
||||
_voiceXPBatch.Clear();
|
||||
var xpAmount = _xpConfig.Data.VoiceXpPerMinute;
|
||||
|
||||
if (xpAmount <= 0)
|
||||
return;
|
||||
|
||||
var oldBatch = _voiceXpBatch.ToArray();
|
||||
_voiceXpBatch.Clear();
|
||||
var validUsers = new HashSet<IGuildUser>();
|
||||
|
||||
var guilds = _client.Guilds;
|
||||
|
||||
foreach (var g in guilds)
|
||||
foreach (var vc in g.VoiceChannels)
|
||||
foreach (var u in vc.ConnectedUsers)
|
||||
if (!u.IsMuted && !u.IsDeafened
|
||||
&& vc.ConnectedUsers.Count(x => !x.IsBot) > 1)
|
||||
{
|
||||
if (IsServerExcluded(g.Id))
|
||||
continue;
|
||||
|
||||
foreach (var vc in g.VoiceChannels)
|
||||
{
|
||||
if (!IsVoiceChannelActive(vc))
|
||||
continue;
|
||||
|
||||
if (IsChannelExcluded(vc))
|
||||
continue;
|
||||
|
||||
foreach (var u in vc.ConnectedUsers)
|
||||
{
|
||||
if (IsServerOrRoleExcluded(u) || !UserParticipatingInVoiceChannel(u))
|
||||
continue;
|
||||
|
||||
if (oldBatch.Contains(u))
|
||||
validUsers.Add(u);
|
||||
|
||||
_voiceXPBatch.Add(u);
|
||||
_voiceXpBatch.Add(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await UpdateXpInternalAsync(validUsers.DistinctBy(x => x.Id).ToArray(), xpAmount);
|
||||
|
@ -208,7 +206,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
private async Task UpdateXp()
|
||||
{
|
||||
var xpAmount = _xpConfig.Data.XpPerMessage;
|
||||
var xpAmount = _xpConfig.Data.TextXpPerMessage;
|
||||
var currentBatch = _usersBatch.ToArray();
|
||||
_usersBatch.Clear();
|
||||
|
||||
|
@ -223,7 +221,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
await using var ctx = _db.GetDbContext();
|
||||
await using var lctx = ctx.CreateLinqToDBConnection();
|
||||
|
||||
var tempTableName = "xp_batch_" + _shardData.ShardId;
|
||||
var tempTableName = "xptemp_" + Guid.NewGuid().ToString().Replace("-", string.Empty);
|
||||
await using var batchTable = await lctx.CreateTempTableAsync<UserXpBatch>(tempTableName);
|
||||
|
||||
await batchTable.BulkCopyAsync(currentBatch.Select(x => new UserXpBatch()
|
||||
|
@ -245,51 +243,34 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
Xp = UserXpStats.Xp + EXCLUDED.Xp;
|
||||
""");
|
||||
|
||||
await lctx.ExecuteAsync(
|
||||
$"""
|
||||
INSERT INTO DiscordUser (UserId, AvatarId, Username, TotalXp)
|
||||
SELECT "{tempTableName}"."UserId", "{tempTableName}"."AvatarId", "{tempTableName}"."Username", {xpAmount}
|
||||
FROM {tempTableName}
|
||||
WHERE TRUE
|
||||
ON CONFLICT (UserId) DO UPDATE
|
||||
SET
|
||||
Username = EXCLUDED.Username,
|
||||
AvatarId = EXCLUDED.AvatarId,
|
||||
TotalXp = DiscordUser.TotalXp + {xpAmount};
|
||||
""");
|
||||
|
||||
foreach (var (guildId, users) in currentBatch.GroupBy(x => x.GuildId)
|
||||
.ToDictionary(x => x.Key, x => x.AsEnumerable()))
|
||||
var updated = await batchTable
|
||||
.InnerJoin(lctx.GetTable<UserXpStats>(),
|
||||
(u, s) => u.GuildId == s.GuildId && u.UserId == s.UserId,
|
||||
(batch, stats) => stats)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
foreach (var u in updated)
|
||||
{
|
||||
var userIds = users.Select(x => x.Id).ToArray();
|
||||
|
||||
var dbStats = await ctx.GetTable<UserXpStats>()
|
||||
.Where(x => x.GuildId == guildId && userIds.Contains(x.UserId))
|
||||
.OrderByDescending(x => x.Xp)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
|
||||
for (var i = 0; i < dbStats.Length; i++)
|
||||
{
|
||||
var oldStats = new LevelStats(dbStats[i].Xp - xpAmount);
|
||||
var newStats = new LevelStats(dbStats[i].Xp);
|
||||
var oldStats = new LevelStats(u.Xp - xpAmount);
|
||||
var newStats = new LevelStats(u.Xp);
|
||||
|
||||
Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}",
|
||||
dbStats[i].UserId,
|
||||
u.UserId,
|
||||
oldStats.TotalXp,
|
||||
newStats.TotalXp);
|
||||
|
||||
if (oldStats.Level < newStats.Level)
|
||||
{
|
||||
await _levelUpQueue.EnqueueAsync(NotifyUser(guildId,
|
||||
await _levelUpQueue.EnqueueAsync(NotifyUser(u.GuildId,
|
||||
0,
|
||||
dbStats[i].UserId,
|
||||
u.UserId,
|
||||
true,
|
||||
oldStats.Level,
|
||||
newStats.Level));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Func<Task> NotifyUser(
|
||||
ulong guildId,
|
||||
|
@ -410,50 +391,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
}
|
||||
}
|
||||
|
||||
private const string XP_TEMPLATE_PATH = "./data/xp_template.json";
|
||||
|
||||
private void InternalReloadXpTemplate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new RequireObjectPropertiesContractResolver()
|
||||
};
|
||||
|
||||
if (!File.Exists(XP_TEMPLATE_PATH))
|
||||
{
|
||||
var newTemp = new XpTemplate();
|
||||
newTemp.Version = 1;
|
||||
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(newTemp, Formatting.Indented));
|
||||
}
|
||||
|
||||
_template = JsonConvert.DeserializeObject<XpTemplate>(
|
||||
File.ReadAllText(XP_TEMPLATE_PATH),
|
||||
settings)!;
|
||||
|
||||
if (_template.Version < 1)
|
||||
{
|
||||
Log.Warning("Loaded default xp_template.json values as the old one was version 0. "
|
||||
+ "Old one was renamed to xp_template.json.old");
|
||||
File.WriteAllText("./data/xp_template.json.old",
|
||||
JsonConvert.SerializeObject(_template, Formatting.Indented));
|
||||
_template = new();
|
||||
_template.Version = 1;
|
||||
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(_template, Formatting.Indented));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "xp_template.json is invalid. Loaded default values");
|
||||
_template = new();
|
||||
_template.Version = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadXpTemplate()
|
||||
=> _pubSub.Pub(_xpTemplateReloadKey, true);
|
||||
|
||||
public async Task SetCurrencyReward(ulong guildId, int level, int amount)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
@ -484,7 +421,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
}
|
||||
}
|
||||
|
||||
uow.SaveChanges();
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<XpSettings> GetFullXpSettingsFor(ulong guildId)
|
||||
|
@ -586,77 +523,44 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
.ToArrayAsyncLinqToDB();
|
||||
}
|
||||
|
||||
private Task Client_OnGuildAvailable(SocketGuild guild)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
foreach (var channel in guild.VoiceChannels)
|
||||
await ScanChannelForVoiceXp(channel);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Client_OnUserVoiceStateUpdated(SocketUser socketUser, SocketVoiceState before,
|
||||
SocketVoiceState after)
|
||||
{
|
||||
if (socketUser is not SocketGuildUser user || user.IsBot)
|
||||
return;
|
||||
|
||||
if (after.VoiceChannel is not null)
|
||||
{
|
||||
await ScanChannelForVoiceXp(after.VoiceChannel);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanChannelForVoiceXp(SocketVoiceChannel channel)
|
||||
{
|
||||
if (ShouldTrackVoiceChannel(channel))
|
||||
private bool IsVoiceChannelActive(SocketVoiceChannel channel)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var user in channel.ConnectedUsers)
|
||||
{
|
||||
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel))
|
||||
await UserJoinedVoiceChannel(user);
|
||||
}
|
||||
if (UserParticipatingInVoiceChannel(user))
|
||||
{
|
||||
count++;
|
||||
if (count >= 2)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
|
||||
=> channel.ConnectedUsers.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2;
|
||||
|
||||
private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
||||
private static bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
||||
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
|
||||
|
||||
private TypedKey<long> GetVoiceXpKey(ulong userId)
|
||||
=> new($"xp:{_client.CurrentUser.Id}:vc_join:{userId}");
|
||||
|
||||
private async Task UserJoinedVoiceChannel(SocketGuildUser user)
|
||||
private bool IsServerOrRoleExcluded(SocketGuildUser user)
|
||||
{
|
||||
var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
await _c.AddAsync(GetVoiceXpKey(user.Id),
|
||||
value,
|
||||
TimeSpan.FromMinutes(1),
|
||||
overwrite: true);
|
||||
}
|
||||
|
||||
private bool ShouldTrackXp(SocketGuildUser user, IMessageChannel channel)
|
||||
{
|
||||
var channelId = channel.Id;
|
||||
|
||||
if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans)
|
||||
&& (chans.Contains(channelId)
|
||||
|| (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id))))
|
||||
return false;
|
||||
|
||||
if (_excludedServers.Contains(user.Guild.Id))
|
||||
return false;
|
||||
return true;
|
||||
|
||||
if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) && user.Roles.Any(x => roles.Contains(x.Id)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsChannelExcluded(IGuildChannel channel)
|
||||
{
|
||||
if (_excludedChannels.TryGetValue(channel.Guild.Id, out var chans)
|
||||
&& (chans.Contains(channel.Id)
|
||||
|| (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id))))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage arg)
|
||||
|
@ -664,27 +568,34 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (arg.Author is not SocketGuildUser user || user.IsBot)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (arg.Channel is not IGuildChannel gc)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (!ShouldTrackXp(user, arg.Channel))
|
||||
if (IsChannelExcluded(gc))
|
||||
return;
|
||||
|
||||
if (IsServerOrRoleExcluded(user))
|
||||
return;
|
||||
|
||||
var xpConf = _xpConfig.Data;
|
||||
var xp = 0;
|
||||
if (arg.Attachments.Any(a => a.Height >= 128 && a.Width >= 128))
|
||||
xp = xpConf.XpFromImage;
|
||||
xp = xpConf.TextXpFromImage;
|
||||
|
||||
if (arg.Content.Contains(' ') || arg.Content.Length >= 5)
|
||||
xp = Math.Max(xp, xpConf.XpPerMessage);
|
||||
xp = Math.Max(xp, xpConf.TextXpPerMessage);
|
||||
|
||||
if (xp <= 0)
|
||||
return;
|
||||
|
||||
if (!await SetUserRewardedAsync(user.Id))
|
||||
if (!await TryAddUserGainedXpAsync(user.Id, xpConf.TextXpCooldown))
|
||||
return;
|
||||
|
||||
_usersBatch.Add(user);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
@ -706,7 +617,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
usr.Xp += amount;
|
||||
|
||||
uow.SaveChanges();
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public bool IsServerExcluded(ulong id)
|
||||
|
@ -717,7 +628,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (_excludedRoles.TryGetValue(id, out var val))
|
||||
return val.ToArray();
|
||||
|
||||
return Enumerable.Empty<ulong>();
|
||||
return [];
|
||||
}
|
||||
|
||||
public IEnumerable<ulong> GetExcludedChannels(ulong id)
|
||||
|
@ -725,17 +636,24 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (_excludedChannels.TryGetValue(id, out var val))
|
||||
return val.ToArray();
|
||||
|
||||
return Enumerable.Empty<ulong>();
|
||||
return [];
|
||||
}
|
||||
|
||||
private TypedKey<bool> GetUserRewKey(ulong userId)
|
||||
=> new($"xp:{_client.CurrentUser.Id}:user_gain:{userId}");
|
||||
private async Task<bool> TryAddUserGainedXpAsync(ulong userId, int cdInSeconds)
|
||||
{
|
||||
if (cdInSeconds <= 0)
|
||||
return true;
|
||||
|
||||
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
||||
=> await _c.AddAsync(GetUserRewKey(userId),
|
||||
true,
|
||||
expiry: TimeSpan.FromSeconds(_xpConfig.Data.MessageXpCooldown),
|
||||
overwrite: false);
|
||||
if (_memCache.TryGetValue(userId, out _))
|
||||
return false;
|
||||
|
||||
using var entry = _memCache.CreateEntry(userId);
|
||||
entry.Value = true;
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cdInSeconds);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<FullUserStats> GetUserStatsAsync(IGuildUser user)
|
||||
{
|
||||
|
@ -774,7 +692,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
public async Task<bool> ToggleExcludeRoleAsync(ulong guildId, ulong rId)
|
||||
{
|
||||
var roles = _excludedRoles.GetOrAdd(guildId, _ => new());
|
||||
var roles = _excludedRoles.GetOrAdd(guildId, _ => []);
|
||||
await using var uow = _db.GetDbContext();
|
||||
var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
|
||||
var excludeObj = new ExcludedItem
|
||||
|
@ -805,7 +723,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
public async Task<bool> ToggleExcludeChannelAsync(ulong guildId, ulong chId)
|
||||
{
|
||||
var channels = _excludedChannels.GetOrAdd(guildId, _ => new());
|
||||
var channels = _excludedChannels.GetOrAdd(guildId, _ => []);
|
||||
await using var uow = _db.GetDbContext();
|
||||
var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
|
||||
var excludeObj = new ExcludedItem
|
||||
|
@ -837,6 +755,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
}
|
||||
|
||||
|
||||
private int _lastKnownTemplateHashCode;
|
||||
|
||||
public Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(FullUserStats stats)
|
||||
=> Task.Run(async () =>
|
||||
{
|
||||
|
@ -848,9 +768,39 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
throw new ArgumentNullException(nameof(bgBytes));
|
||||
}
|
||||
|
||||
var avatarUrl = stats.User.RealAvatarUrl();
|
||||
byte[] avatarImageData = null;
|
||||
|
||||
if (avatarUrl is not null)
|
||||
{
|
||||
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||
if (!result.TryPickT0(out avatarImageData, out _))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _c.SetImageDataAsync(avatarUrl, avatarImageData);
|
||||
}
|
||||
}
|
||||
|
||||
var outlinePen = new SolidPen(Color.Black, 1f);
|
||||
|
||||
using var img = Image.Load<Rgba32>(bgBytes);
|
||||
|
||||
if (_template.User.Name.Show)
|
||||
{
|
||||
var fontSize = (int)(_template.User.Name.FontSize * 0.9);
|
||||
|
@ -874,8 +824,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
"@" + username,
|
||||
Brushes.Solid(_template.User.Name.Color),
|
||||
outlinePen);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//club name
|
||||
|
||||
|
@ -885,7 +834,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
var clubFont = _fonts.NotoSans.CreateFont(_template.Club.Name.FontSize, FontStyle.Regular);
|
||||
|
||||
img.Mutate(x => x.DrawText(new RichTextOptions(clubFont)
|
||||
x.DrawText(new RichTextOptions(clubFont)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
|
@ -894,7 +843,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
},
|
||||
clubName,
|
||||
Brushes.Solid(_template.Club.Name.Color),
|
||||
outlinePen));
|
||||
outlinePen);
|
||||
}
|
||||
|
||||
Font GetTruncatedFont(
|
||||
|
@ -925,13 +874,11 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
stats.Global.Level.ToString(),
|
||||
75);
|
||||
|
||||
img.Mutate(x =>
|
||||
{
|
||||
|
||||
x.DrawText(stats.Global.Level.ToString(),
|
||||
globalLevelFont,
|
||||
_template.User.GlobalLevel.Color,
|
||||
new(_template.User.GlobalLevel.Pos.X, _template.User.GlobalLevel.Pos.Y)); //level
|
||||
});
|
||||
}
|
||||
|
||||
if (_template.User.GuildLevel.Show)
|
||||
|
@ -943,13 +890,11 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
stats.Guild.Level.ToString(),
|
||||
75);
|
||||
|
||||
img.Mutate(x =>
|
||||
{
|
||||
|
||||
x.DrawText(stats.Guild.Level.ToString(),
|
||||
guildLevelFont,
|
||||
_template.User.GuildLevel.Color,
|
||||
new(_template.User.GuildLevel.Pos.X, _template.User.GuildLevel.Pos.Y));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -967,8 +912,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
if (_template.User.Xp.Global.Show)
|
||||
{
|
||||
img.Mutate(x => x.DrawText(
|
||||
new RichTextOptions(_fonts.NotoSans.CreateFont(_template.User.Xp.Global.FontSize, FontStyle.Bold))
|
||||
x.DrawText(
|
||||
new RichTextOptions(_fonts.NotoSans.CreateFont(_template.User.Xp.Global.FontSize,
|
||||
FontStyle.Bold))
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
@ -976,13 +922,14 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
},
|
||||
$"{global.LevelXp}/{global.RequiredXp}",
|
||||
Brushes.Solid(_template.User.Xp.Global.Color),
|
||||
outlinePen));
|
||||
outlinePen);
|
||||
}
|
||||
|
||||
if (_template.User.Xp.Guild.Show)
|
||||
{
|
||||
img.Mutate(x => x.DrawText(
|
||||
new RichTextOptions(_fonts.NotoSans.CreateFont(_template.User.Xp.Guild.FontSize, FontStyle.Bold))
|
||||
x.DrawText(
|
||||
new RichTextOptions(_fonts.NotoSans.CreateFont(_template.User.Xp.Guild.FontSize,
|
||||
FontStyle.Bold))
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
@ -990,7 +937,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
},
|
||||
$"{guild.LevelXp}/{guild.RequiredXp}",
|
||||
Brushes.Solid(_template.User.Xp.Guild.Color),
|
||||
outlinePen));
|
||||
outlinePen);
|
||||
}
|
||||
|
||||
var rankPen = new SolidPen(Color.White, 1);
|
||||
|
@ -1006,7 +953,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
globalRankStr,
|
||||
68);
|
||||
|
||||
img.Mutate(x => x.DrawText(
|
||||
x.DrawText(
|
||||
new RichTextOptions(globalRankFont)
|
||||
{
|
||||
Origin = new(_template.User.GlobalRank.Pos.X, _template.User.GlobalRank.Pos.Y)
|
||||
|
@ -1014,7 +961,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
globalRankStr,
|
||||
Brushes.Solid(_template.User.GlobalRank.Color),
|
||||
rankPen
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
if (_template.User.GuildRank.Show)
|
||||
|
@ -1028,7 +975,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
guildRankStr,
|
||||
43);
|
||||
|
||||
img.Mutate(x => x.DrawText(
|
||||
x.DrawText(
|
||||
new RichTextOptions(guildRankFont)
|
||||
{
|
||||
Origin = new(_template.User.GuildRank.Pos.X, _template.User.GuildRank.Pos.Y)
|
||||
|
@ -1036,63 +983,35 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
guildRankStr,
|
||||
Brushes.Solid(_template.User.GuildRank.Color),
|
||||
rankPen
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
//avatar
|
||||
if (_template.User.Icon.Show)
|
||||
{
|
||||
try
|
||||
{
|
||||
var avatarUrl = stats.User.RealAvatarUrl();
|
||||
|
||||
if (avatarUrl is not null)
|
||||
{
|
||||
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||
if (!result.TryPickT0(out var data, out _))
|
||||
{
|
||||
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())
|
||||
{
|
||||
data = stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _c.SetImageDataAsync(avatarUrl, data);
|
||||
}
|
||||
|
||||
using var toDraw = Image.Load(data);
|
||||
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));
|
||||
toDraw.Mutate(x
|
||||
=> x.Resize(_template.User.Icon.Size.X, _template.User.Icon.Size.Y));
|
||||
|
||||
img.Mutate(x => x.DrawImage(toDraw,
|
||||
x.DrawImage(toDraw,
|
||||
new Point(_template.User.Icon.Pos.X, _template.User.Icon.Pos.Y),
|
||||
1));
|
||||
}
|
||||
1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error drawing avatar image");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//club image
|
||||
if (_template.Club.Icon.Show)
|
||||
await DrawClubImage(img, stats);
|
||||
|
||||
// #if GLOBAL_ELLIE
|
||||
await DrawFrame(img, stats.User.UserId);
|
||||
// #endif
|
||||
|
||||
var outputSize = _template.OutputSize;
|
||||
if (outputSize.X != img.Width || outputSize.Y != img.Height)
|
||||
|
@ -1122,7 +1041,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
return await _images.GetXpBackgroundImageAsync();
|
||||
}
|
||||
|
||||
// #if GLOBAL_ELLIE
|
||||
private async Task DrawFrame(Image<Rgba32> img, ulong userId)
|
||||
{
|
||||
var patron = await _ps.GetPatronAsync(userId);
|
||||
|
@ -1150,7 +1068,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (frame is not null)
|
||||
img.Mutate(x => x.DrawImage(frame, new Point(0, 0), new GraphicsOptions()));
|
||||
}
|
||||
// #endif
|
||||
|
||||
private void DrawXpBar(float percent, XpBar info, Image<Rgba32> img)
|
||||
{
|
||||
|
@ -1164,34 +1081,18 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
float x3, x4, y3, y4;
|
||||
|
||||
if (info.Direction == XpTemplateDirection.Down)
|
||||
var matrix = info.Direction switch
|
||||
{
|
||||
x3 = x1;
|
||||
x4 = x2;
|
||||
y3 = y1 + length;
|
||||
y4 = y2 + length;
|
||||
}
|
||||
else if (info.Direction == XpTemplateDirection.Up)
|
||||
{
|
||||
x3 = x1;
|
||||
x4 = x2;
|
||||
y3 = y1 - length;
|
||||
y4 = y2 - length;
|
||||
}
|
||||
else if (info.Direction == XpTemplateDirection.Left)
|
||||
{
|
||||
x3 = x1 - length;
|
||||
x4 = x2 - length;
|
||||
y3 = y1;
|
||||
y4 = y2;
|
||||
}
|
||||
else
|
||||
{
|
||||
x3 = x1 + length;
|
||||
x4 = x2 + length;
|
||||
y3 = y1;
|
||||
y4 = y2;
|
||||
}
|
||||
XpTemplateDirection.Down => new float[,] { { 0, 1 }, { 0, 1 } },
|
||||
XpTemplateDirection.Up => new float[,] { { 0, -1 }, { 0, -1 } },
|
||||
XpTemplateDirection.Left => new float[,] { { -1, 0 }, { -1, 0 } },
|
||||
_ => new float[,] { { 1, 0 }, { 1, 0 } },
|
||||
};
|
||||
|
||||
x3 = x1 + matrix[0, 0] * length;
|
||||
x4 = x2 + matrix[1, 0] * length;
|
||||
y3 = y1 + matrix[0, 1] * length;
|
||||
y4 = y2 + matrix[1, 1] * length;
|
||||
|
||||
img.Mutate(x => x.FillPolygon(info.Color,
|
||||
new PointF(x1, y1),
|
||||
|
@ -1278,7 +1179,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (!data.Shop.IsEnabled)
|
||||
return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
|
||||
|
||||
return new(_xpConfig.Data.Shop.Bgs?.Where(x => x.Value.Price >= 0).ToDictionary(x => x.Key, x => x.Value));
|
||||
return new(_xpConfig.Data.Shop.Bgs?.Where(x => x.Value.Price >= 0)
|
||||
.ToDictionary(x => x.Key, x => x.Value));
|
||||
}
|
||||
|
||||
public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopFrames()
|
||||
|
@ -1287,7 +1189,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (!data.Shop.IsEnabled)
|
||||
return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
|
||||
|
||||
return new(_xpConfig.Data.Shop.Frames?.Where(x => x.Value.Price >= 0).ToDictionary(x => x.Key, x => x.Value));
|
||||
return new(_xpConfig.Data.Shop.Frames?.Where(x => x.Value.Price >= 0)
|
||||
.ToDictionary(x => x.Key, x => x.Value));
|
||||
}
|
||||
|
||||
public async Task<BuyResult> BuyShopItemAsync(ulong userId, XpShopItemType type, string key)
|
||||
|
@ -1321,7 +1224,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (item is null || item.Price < 0)
|
||||
return BuyResult.UnknownItem;
|
||||
|
||||
if (item.Price > 0 && !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}")))
|
||||
if (item.Price > 0 &&
|
||||
!await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}")))
|
||||
return BuyResult.InsufficientFunds;
|
||||
|
||||
|
||||
|
@ -1445,7 +1349,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
public bool IsShopEnabled()
|
||||
=> _xpConfig.Data.Shop.IsEnabled;
|
||||
|
||||
public async Task<int> GetTotalGuildUsers(ulong requestGuildId, List<ulong>? guildUsers = null)
|
||||
public async Task<int> GetGuildXpUsersCountAsync(ulong requestGuildId, List<ulong>? guildUsers = null)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<UserXpStats>()
|
||||
|
@ -1478,12 +1382,64 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
}
|
||||
}
|
||||
|
||||
public sealed class UserXpBatch
|
||||
public sealed class XpTemplateService : IEService, IReadyExecutor
|
||||
{
|
||||
[Key]
|
||||
public ulong UserId { get; set; }
|
||||
private const string XP_TEMPLATE_PATH = "./data/xp_template.json";
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string AvatarId { get; set; } = string.Empty;
|
||||
private readonly IPubSub _pubSub;
|
||||
private XpTemplate _template = new();
|
||||
private readonly TypedKey<bool> _xpTemplateReloadKey = new("xp.template.reload");
|
||||
|
||||
public XpTemplateService(IPubSub pubSub)
|
||||
{
|
||||
_pubSub = pubSub;
|
||||
}
|
||||
|
||||
private void InternalReloadXpTemplate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new RequireObjectPropertiesContractResolver()
|
||||
};
|
||||
|
||||
if (!File.Exists(XP_TEMPLATE_PATH))
|
||||
{
|
||||
var newTemp = new XpTemplate();
|
||||
newTemp.Version = 2;
|
||||
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(newTemp, Formatting.Indented));
|
||||
}
|
||||
|
||||
_template = JsonConvert.DeserializeObject<XpTemplate>(
|
||||
File.ReadAllText(XP_TEMPLATE_PATH),
|
||||
settings)!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "xp_template.json is invalid. Loaded default values");
|
||||
_template = new();
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadXpTemplate()
|
||||
=> _pubSub.Pub(_xpTemplateReloadKey, true);
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
InternalReloadXpTemplate();
|
||||
await _pubSub.Sub(_xpTemplateReloadKey,
|
||||
_ =>
|
||||
{
|
||||
InternalReloadXpTemplate();
|
||||
return default;
|
||||
});
|
||||
}
|
||||
|
||||
public XpTemplate GetTemplate()
|
||||
=> _template;
|
||||
}
|
||||
|
||||
public sealed class ReadyXpTempalte(XpTemplate template)
|
||||
{
|
||||
}
|
|
@ -7,7 +7,7 @@ namespace EllieBot.Modules.Xp;
|
|||
|
||||
public class XpTemplate
|
||||
{
|
||||
public int Version { get; set; } = 0;
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
[JsonProperty("output_size")]
|
||||
public XpTemplatePos OutputSize { get; set; } = new()
|
|
@ -217,7 +217,7 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
|
|||
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
|
||||
|
||||
var data = await _xp.GetGuildUserXps(request.GuildId, request.Page - 1);
|
||||
var total = await _xp.GetTotalGuildUsers(request.GuildId);
|
||||
var total = await _xp.GetGuildXpUsersCountAsync(request.GuildId);
|
||||
|
||||
var reply = new GetXpLbReply
|
||||
{
|
||||
|
|
|
@ -9,12 +9,12 @@ namespace Ellie.Common;
|
|||
public sealed class MemoryBotCache : IBotCache
|
||||
{
|
||||
// needed for overwrites and Delete return value
|
||||
private readonly object _cacheLock = new object();
|
||||
private readonly ConcurrentDictionary<string, object> _locks = new();
|
||||
private readonly MemoryCache _cache;
|
||||
|
||||
public MemoryBotCache()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
_cache = new(new MemoryCacheOptions());
|
||||
}
|
||||
|
||||
public ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||
|
@ -27,7 +27,9 @@ public sealed class MemoryBotCache : IBotCache
|
|||
return new(true);
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
var cacheLock = _locks.GetOrAdd(key.Key, static _ => new());
|
||||
|
||||
lock (cacheLock)
|
||||
{
|
||||
if (_cache.TryGetValue(key.Key, out var old) && old is not null)
|
||||
return new(false);
|
||||
|
@ -61,7 +63,8 @@ public sealed class MemoryBotCache : IBotCache
|
|||
|
||||
public ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
var cacheLock = _locks.GetOrAdd(key.Key, static _ => new());
|
||||
lock (cacheLock)
|
||||
{
|
||||
var toReturn = _cache.TryGetValue(key.Key, out var old) && old is not null;
|
||||
_cache.Remove(key.Key);
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
# DO NOT CHANGE
|
||||
version: 7
|
||||
version: 10
|
||||
# How much XP will the users receive per message
|
||||
xpPerMessage: 3
|
||||
textXpPerMessage: 3
|
||||
# How often can the users receive XP, in seconds
|
||||
messageXpCooldown: 300
|
||||
textXpCooldown: 300
|
||||
# Amount of xp users gain from posting an image
|
||||
xpFromImage: 0
|
||||
textXpFromImage: 3
|
||||
# Average amount of xp earned per minute in VC
|
||||
voiceXpPerMinute: 0
|
||||
# The maximum amount of minutes the bot will keep track of a user in a voice channel
|
||||
voiceMaxMinutes: 720
|
||||
# The amount of currency users will receive for each point of global xp that they earn
|
||||
currencyPerXp: 0
|
||||
# Xp Shop config
|
||||
shop:
|
||||
# Whether the xp shop is enabled
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"Version": 1,
|
||||
"Version": 2,
|
||||
"output_size": {
|
||||
"X": 800,
|
||||
"Y": 392
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue